File Coverage

blib/lib/Google/ContentAPI.pm
Criterion Covered Total %
statement 21 102 20.5
branch 0 52 0.0
condition 0 12 0.0
subroutine 7 16 43.7
pod 3 9 33.3
total 31 191 16.2


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