File Coverage

blib/lib/Google/ContentAPI.pm
Criterion Covered Total %
statement 21 103 20.3
branch 0 52 0.0
condition 0 14 0.0
subroutine 7 16 43.7
pod 3 9 33.3
total 31 194 15.9


line stmt bran cond sub pod time code
1             ##############################################################################
2             # Google::ContentAPI
3             #
4             # Add, modify and delete items from the Google Merchant Center platform via
5             # the Content API for Shopping.
6             #
7             # https://developers.google.com/shopping-content/v2/quickstart
8             #
9             # Authentication is done via Service Account credentials. For details see:
10             # https://developers.google.com/shopping-content/v2/how-tos/service-accounts
11             #
12             # AUTHOR
13             #
14             # Bill Gerrard
15             #
16             # VERSION HISTORY
17             #
18             # + v1.03 12/10/2020 Allow custom endpoint, update docs for v2.1 API
19             # + v1.02 04/11/2018 Add accountstatuses, productstatuses methods. Add "custom" resource
20             # + v1.01 03/27/2018 Added config_json, merchant_id options and switched to Crypt::JWT
21             # + v1.00 03/23/2018 initial release
22             #
23             # COPYRIGHT AND LICENSE
24             #
25             # Copyright (C) 2018,2020 Bill Gerrard
26             #
27             # This program is free software; you can redistribute it and/or modify
28             # it under the same terms as Perl itself, either Perl version 5.20.2 or,
29             # at your option, any later version of Perl 5 you may have available.
30             #
31             # Disclaimer of warranty: This program is provided by the copyright holder
32             # and contributors "As is" and without any express or implied warranties.
33             # The implied warranties of merchantability, fitness for a particular purpose,
34             # or non-infringement are disclaimed to the extent permitted by your local
35             # law. Unless required by law, no copyright holder or contributor will be
36             # liable for any direct, indirect, incidental, or consequential damages
37             # arising in any way out of the use of the package, even if advised of the
38             # possibility of such damage.
39             #
40             ################################################################################
41              
42             package Google::ContentAPI;
43              
44 1     1   71816 use strict;
  1         2  
  1         31  
45 1     1   6 use warnings;
  1         2  
  1         26  
46 1     1   8 use Carp;
  1         1  
  1         54  
47              
48 1     1   756 use JSON;
  1         12893  
  1         5  
49 1     1   788 use Crypt::JWT qw(encode_jwt);
  1         49422  
  1         102  
50 1     1   737 use REST::Client;
  1         49513  
  1         42  
51 1     1   602 use HTML::Entities;
  1         6024  
  1         1269  
52              
53             our $VERSION = '1.03';
54              
55             sub new {
56 0     0 1   my ($class, $params) = @_;
57 0           my $self = {};
58              
59 0 0         if ($params->{config_file}) {
    0          
60 0           $self->{config} = load_google_config($params->{config_file});
61             } elsif ($params->{config_json}) {
62 0           $self->{config} = decode_json $params->{config_json};
63             } else {
64 0           croak "config_file or config_json not provided in new()";
65             }
66             $self->{merchant_id} = $self->{config}->{merchant_id}
67             || $params->{merchant_id}
68 0   0       || croak "'merchant_id' not provided in json config in new()";
69              
70             $self->{endpoint} = $self->{config}->{endpoint}
71             || $params->{endpoint}
72 0   0       || 'https://www.googleapis.com/content/v2';
73              
74 0 0         $self->{debug} = 1 if $params->{debug};
75 0           $self->{google_auth_token} = get_google_auth_token($self);
76 0           $self->{rest} = init_rest_client($self);
77              
78 0           return bless $self, $class;
79             }
80              
81             sub get {
82 0     0 1   my $self = shift;
83 0 0         croak "Odd number of arguments for get()" if scalar(@_) % 2;
84 0           my $opt = {@_};
85 0           my $method = $self->prepare_method($opt);
86 0           return $self->request('GET', $method);
87             }
88              
89             sub post {
90 0     0 0   my $self = shift;
91 0 0         croak "Odd number of arguments for post()" if scalar(@_) % 2;
92 0           my $opt = {@_};
93 0           my $method = $self->prepare_method($opt);
94 0 0         $opt->{body} = encode_json $opt->{body} if $opt->{body};
95 0           return $self->request('POST', $method, $opt->{body});
96             }
97              
98             sub delete {
99 0     0 1   my $self = shift;
100 0 0         croak "Odd number of arguments for delete()" if scalar(@_) % 2;
101 0           my $opt = {@_};
102 0           my $method = $self->prepare_method($opt);
103 0           return $self->request('DELETE', $method);
104             }
105              
106             sub prepare_method {
107 0     0 0   my $self = shift;
108 0           my $opt = shift;
109              
110 0 0         $opt->{resource} = '' if $opt->{resource} eq 'custom';
111              
112 0 0 0       if ($opt->{resource} eq 'products'
      0        
113             || $opt->{resource} eq 'productstatuses'
114             || $opt->{resource} eq 'accountstatuses'
115             ) {
116             # add merchant ID to request URL for non-batch requests
117 0 0         $opt->{resource} = $self->{merchant_id} .'/'. $opt->{resource} if $opt->{method} ne 'batch';
118             # drop list/insert methods; these are for coding convenience only
119 0 0         $opt->{method} = '' if $opt->{method} eq 'list';
120 0 0         $opt->{method} = '' if $opt->{method} eq 'insert';
121             # append product ID to end of request URL for get and delete
122 0 0         $opt->{method} = $opt->{id} if $opt->{method} =~ /get|delete/;
123             }
124              
125 0 0         my $encoded_params = $self->{rest}->buildQuery($opt->{params}) if $opt->{params};
126              
127 0           my $method;
128 0 0         $method .= '/'. $opt->{resource} if $opt->{resource} ne '';
129 0 0         $method .= '/'. $opt->{method} if $opt->{method} ne '';
130 0 0         $method .= $encoded_params if $encoded_params;
131              
132 0           return $method;
133             }
134              
135             sub init_rest_client {
136 0     0 0   my $self = shift;
137 0           my $r = REST::Client->new();
138 0           $r->setHost($self->{endpoint});
139 0           $r->addHeader('Authorization', $self->{google_auth_token});
140 0           $r->addHeader('Content-type', 'application/json');
141 0           $r->addHeader('charset', 'UTF-8');
142 0           return $r;
143             }
144              
145             sub request {
146 0     0 0   my $self = shift;
147 0           my @command = @_;
148              
149 0 0         print join (' ', @command) . "\n" if $self->{debug};
150 0           my $rest = $self->{rest}->request(@command);
151              
152 0 0         unless ($rest->responseCode eq '200') {
153 0 0 0       if ($rest->responseCode eq '204' && $command[0] eq 'DELETE') {
    0          
154             # no-op: delete was successful
155             } elsif ($rest->responseCode eq '401') {
156             # access token expired, request new token and retry request
157 0           $self->{google_auth_token} = $self->get_google_auth_token();
158 0           $self->{rest} = $self->init_rest_client();
159 0           $rest = $self->{rest}->request(@command);
160             } else {
161 0           die("Error processing REST request:\n",
162             "Request: ", $rest->getHost , $command[1], "\n",
163             "Response Code: ", $rest->responseCode, "\n", $rest->responseContent, "\n");
164             }
165             }
166 0 0         print "Request Response: \n". $rest->responseContent if $self->{debug};
167              
168 0 0         my $response = $rest->responseContent ? decode_json $rest->responseContent : {};
169 0           return { code => $rest->responseCode, response => $response };
170             }
171              
172             sub get_google_auth_token {
173 0     0 0   my $self = shift;
174              
175 0           my $gapiTokenURI = 'https://www.googleapis.com/oauth2/v4/token';
176 0           my $gapiContentScope = 'https://www.googleapis.com/auth/content';
177 0           my $time = time();
178              
179             # 1) Create JSON Web Token
180             # https://developers.google.com/accounts/docs/OAuth2ServiceAccount
181             #
182             my $jwt = encode_jwt(
183             payload => {
184             iss => $self->{config}->{client_email},
185             scope => $gapiContentScope,
186             aud => $gapiTokenURI,
187             exp => $time + 3660, # max 60 minutes
188             iat => $time
189             },
190             key => \$self->{config}->{private_key},
191 0           alg => 'RS256'
192             );
193              
194             # 2) Request an access token
195             #
196 0           my $ua = LWP::UserAgent->new();
197 0           my $response = $ua->post($gapiTokenURI, {
198             grant_type => encode_entities('urn:ietf:params:oauth:grant-type:jwt-bearer'),
199             assertion => $jwt
200             });
201              
202 0 0         print "Request Access Token response:\n". $response->content if $self->{debug};
203              
204 0 0         unless($response->is_success()) {
205 0           die("Error receiving access token:\n", $response->code, "\n", $response->content, "\n");
206             }
207              
208 0           my $data = decode_json $response->content;
209 0           return 'Bearer '. $data->{access_token};
210             }
211              
212             sub load_google_config {
213 0     0 0   my $json_file = shift;
214 0 0         open my $fh, '<', $json_file or croak "Error reading config_file '$json_file': $!";
215 0           my $json_text;
216 0           { local $/; $json_text = <$fh>; }
  0            
  0            
217 0           close $fh;
218 0           return decode_json $json_text;
219             }
220              
221             1;
222              
223             __END__