File Coverage

lib/Google/RestApi.pm
Criterion Covered Total %
statement 150 156 96.1
branch 36 46 78.2
condition 19 24 79.1
subroutine 33 33 100.0
pod 6 6 100.0
total 244 265 92.0


line stmt bran cond sub pod time code
1             package Google::RestApi;
2              
3             our $VERSION = '1.0.3';
4              
5 1     1   4218 use Google::RestApi::Setup;
  1         3  
  1         8  
6              
7 1     1   2788 use File::Basename qw( dirname );
  1         2  
  1         52  
8 1     1   573 use Furl ();
  1         36446  
  1         57  
9 1     1   737 use JSON::MaybeXS qw( decode_json encode_json JSON );
  1         6242  
  1         78  
10 1     1   8 use List::Util ();
  1         3  
  1         21  
11 1     1   6 use Log::Log4perl qw( get_logger );
  1         34  
  1         12  
12 1     1   42 use Module::Load qw( load );
  1         2  
  1         10  
13 1     1   96 use Scalar::Util qw( blessed );
  1         1  
  1         46  
14 1     1   568 use Retry::Backoff qw( retry );
  1         2931  
  1         77  
15 1     1   8 use Storable qw( dclone );
  1         2  
  1         46  
16 1     1   7 use Try::Tiny qw( catch try );
  1         3  
  1         49  
17 1     1   628 use URI ();
  1         4827  
  1         25  
18 1     1   445 use URI::QueryParam ();
  1         833  
  1         2456  
19              
20             sub new {
21 159     159 1 4135014 my $class = shift;
22              
23 159         829 my $self = merge_config_file(@_);
24             state $check = compile_named(
25             config_file => ReadableFile, { optional => 1 }, # specify any of the below in a yaml file instead.
26             auth => HashRef | HasMethods[qw(headers params)], # a string that will be used to construct an auth obj, or the obj itself.
27             throttle => PositiveOrZeroInt, { default => 0 }, # mostly used for integration testing, to ensure we don't blow our rate limit.
28             timeout => Int, { default => 120 },
29 158     158   484 max_attempts => PositiveInt->where(sub { $_ < 10; }), { default => 4 },
  158         9567  
30             );
31 158         12951 $self = $check->(%$self);
32              
33 158         3338 $self->{ua} = Furl->new(timeout => $self->{timeout});
34              
35 158         9585 return bless $self, $class;
36             }
37              
38             # this is the actual call to the google api endpoint. handles retries, error checking etc.
39             # this would normally be called via Drive or Sheets objects.
40             sub api {
41 254     254 1 74161 my $self = shift;
42              
43 254         561 state $check = compile_named(
44             uri => StrMatch[qr(^https://)],
45             method => StrMatch[qr/^(get|head|put|patch|post|delete)$/i], { default => 'get' },
46             params => HashRef[Str|ArrayRef[Str]], { default => {} }, # uri param string.
47             headers => ArrayRef[Str], { default => [] }, # http headers.
48             content => 0, # rest payload.
49             );
50 254         16349 my $request = $check->(@_);
51              
52             # reset our transaction for this new one.
53 252         17346 $self->{transaction} = {};
54              
55 252         1209 $self->_stat( $request->{method}, 'total' );
56 252         1073 $request->{method} = uc($request->{method});
57 252         644 $request->{caller_internal} = _caller_internal();
58 252         678 $request->{caller_external} = _caller_external();
59              
60 252         953 my $request_content = $request->{content};
61 252 100       1135 my $request_json = defined $request_content ? encode_json($request_content) : (),
62              
63             my @headers;
64 252 100       845 push(@headers, 'Content-Type' => 'application/json') if $request_json;
65 252         406 push(@headers, @{ $request->{headers} });
  252         728  
66 252         433 push(@headers, @{ $self->auth()->headers() });
  252         804  
67              
68             # some (outdated) auth mechanisms may allow auth info in the params.
69 252         6178 my %params = (%{ $request->{params} }, %{ $self->auth()->params() });
  252         945  
  252         672  
70 252         2122 my $uri = URI->new($request->{uri});
71 252         32440 $uri->query_form_hash(\%params);
72 252         28463 $request->{uri} = $uri->as_string();
73 252         2037 DEBUG("Rest API request:\n", Dump($request));
74              
75             my $req = HTTP::Request->new(
76 252         905269 $request->{method}, $request->{uri}, \@headers, $request_json
77             );
78             # this is where the action is.
79 252         37454 my ($response, $tries, $last_error) = $self->_api($req);
80             # save the api call details so that the user can examine it in detail if necessary.
81             # further information is also recorded below in this routine.
82             $self->{transaction} = {
83 252 100       2084 request => $request,
    100          
84             tries => $tries,
85             ($response ? (response => $response) : ()),
86             ($last_error ? (error => $last_error) : ()),
87             };
88              
89 252 100       820 if ($response) {
90 250         1172 my $decoded_response = $response->decoded_content();
91 250 100       94775 $decoded_response = $decoded_response ? decode_json($decoded_response) : 1;
92 250         934 $self->{transaction}->{decoded_response} = $decoded_response;
93 250         903 DEBUG("Rest API response:\n", Dump( $decoded_response ));
94             }
95              
96             # calls the callback when an api call is madem, if any.
97 252         745173 $self->_api_callback();
98              
99             # this is for capturing request/responses for unit tests. copy/paste the results
100             # in the log into t/etc/uri_responses/* for unit testing. log appender 'UnitTestCapture'
101             # is defined in t/etc/log4perl.conf. you can use this logger by pointing to it via
102             # GOOGLE_RESTAPI_LOGGER env var.
103 252 50 66     1285 if ($response && Log::Log4perl::appenders->{'UnitTestCapture'}) {
104             # special log category for this. it should be tied to the UnitTestCapture appender.
105             # we want to dump this in the same format as what we need to store in
106             # t/etc/uri_responses.
107 0         0 my %request_response;
108 0         0 my $json = JSON->new->ascii->pretty->canonical;
109 0 0       0 my $pretty_request_json = $request_json ?
110             $json->encode(decode_json($request_json)) : '';
111 0 0       0 my $pretty_response_json = $response->content() ?
112             $json->encode(decode_json($response->content())) : '';
113             $request_response{ $request->{method} } = {
114             $request->{uri} => {
115 0 0       0 ($pretty_request_json) ? (content => $pretty_request_json) : (),
116             response => $pretty_response_json,
117             },
118             };
119 0         0 get_logger('unit.test.capture')->info(Dump(\%request_response) . "\n\n");
120             }
121              
122 252 100 100     2402 if (!$response || !$response->is_success()) {
123 8         76 $self->_stat('error');
124 8         29 LOGDIE("Rest API failure:\n", Dump( $self->transaction() ));
125             }
126              
127             # used for to avoid google 403's and 429's as with integration tests.
128 244 50       2832 sleep($self->{throttle}) if $self->{throttle};
129              
130 244         2328 return $self->{transaction}->{decoded_response};
131             }
132              
133             # this wraps the http api call around retries.
134             sub _api {
135 252     252   761 my ($self, $req) = @_;
136              
137             # default is exponential backoff, initial delay 1.
138 252         418 my $tries = 0;
139 252         515 my $last_error;
140             my $response = retry sub {
141 270     270   47066 $self->{ua}->request($req);
142             },
143             retry_if => sub {
144 270     270   306895 my $h = shift;
145 270         677 my $r = $h->{attempt_result}; # Furl::Response
146 270 100       968 if (!$r) {
147 8   50     21 $last_error = $@ || "Unknown error";
148 8         37 WARN("API call error: $last_error");
149 8         68 return 1;
150             }
151 262 100       998 $last_error = $r->status_line() if !$r->is_success();
152 262 100       2843 if ($r->code() =~ /^(403|429|50[0234])$/) {
153 16         156 WARN("Retrying: $last_error");
154 16         139 return 1;
155             }
156 246         2298 return; # we're accepting the response.
157             },
158 246     246   2533 on_success => sub { $tries++; },
159 24     24   198 on_failure => sub { $tries++; },
160 252         3161 max_attempts => $self->max_attempts(); # override default max_attempts 10.
161 252         41199 return ($response, $tries, $last_error);
162             }
163              
164             # convert a plain hash auth to an object if a hash was passed in new() above.
165             sub auth {
166 508     508 1 1007 my $self = shift;
167              
168 508 100       2044 if (!blessed($self->{auth})) {
169             # turn OAuth2Client into Google::RestApi::Auth::OAuth2Client etc.
170 143         703 my $class = __PACKAGE__ . "::Auth::" . delete $self->{auth}->{class};
171 143         582 load $class;
172             # add the path to the base config file so auth hash doesn't have
173             # to store the full path name for things like token_file etc.
174             $self->{auth}->{config_dir} = dirname($self->{config_file})
175 142 100       18319 if $self->{config_file};
176 142         411 $self->{auth} = $class->new(%{ $self->{auth} });
  142         1988  
177             }
178              
179 505         3090 return $self->{auth};
180             }
181              
182             # if user registered a callback, notify them of an api call.
183             sub _api_callback {
184 252     252   618 my $self = shift;
185 252 100       1253 return if !$self->{api_callback};
186             try {
187 5     5   248 $self->{api_callback}->( $self->transaction() );
188             } catch {
189 1     1   32 my $err = $_;
190 1         10 FATAL("Post process died: $err");
191 5         45 };
192 5         84 return;
193             }
194              
195             # sets the api callback code.
196             sub api_callback {
197 5     5 1 15996 my $self = shift;
198 5         15 state $check = compile(CodeRef, { optional => 1 });
199 5         2195 my ($api_callback) = $check->(@_);
200 5         61 my $prev_api_callback = delete $self->{api_callback};
201 5 100       19 $self->{api_callback} = $api_callback if $api_callback;
202 5         30 return $prev_api_callback;
203             }
204              
205             # a simple record of how many gets, posts, deletes etc were completed.
206             # useful for tuning network calls.
207             sub _stat {
208 260     260   494 my $self = shift;
209 260         694 my @stats = @_;
210 260         682 foreach (@stats) {
211 512         1079 $_ = lc;
212 512   100     2489 $self->{stats}->{$_} //= 0;
213 512         1198 $self->{stats}->{$_}++;
214             }
215 260         634 return;
216             }
217              
218             # returns the stats recorded above.
219             sub stats {
220 6     6 1 12 my $self = shift;
221 6   50     125 my $stats = dclone($self->{stats} || {});
222 6         44 return $stats;
223             }
224              
225             # this is built for every api call so the entire api call can be examined,
226             # params, body content, http codes, etc etc.
227 24 50   24 1 5341 sub transaction { shift->{transaction} || {}; }
228              
229             # used for debugging/logging purposes. tries to dig out a useful caller
230             # so api calls can be traced back. skips some stuff that we use internally
231             # like cache.
232             sub _caller_internal {
233 252     252   720 my ($package, $subroutine, $line, $i) = ('', '', 0);
234 252   100     476 do {
      66        
235 1547         33660 ($package, undef, $line, $subroutine) = caller(++$i);
236             } while($package &&
237             ($package =~ m[^Cache::Memory] ||
238             $subroutine =~ m[api$] ||
239             $subroutine =~ m[^Cache|_cache])
240             );
241             # not usually going to happen, but during testing we call
242             # RestApi::api directly, so have to backtrack.
243 252 50       7968 ($package, undef, $line, $subroutine) = caller(--$i)
244             if !$package;
245 252         1331 return "$package:$line => $subroutine";
246             }
247              
248             sub _caller_external {
249 252     252   737 my ($package, $subroutine, $line, $i) = ('', '', 0);
250 252   66     420 do {
251 2322         42150 ($package, undef, $line, $subroutine) = caller(++$i);
252             } while($package && $package =~ m[^(Google::RestApi|Cache|Try)]);
253 252         5921 return "$package:$line => $subroutine";
254             }
255              
256             # the maximum number of attempts to call the google api endpoint before giving up.
257             # undef returns current value. postitive int sets and returns new value.
258             # 0 sets and returns default value.
259             sub max_attempts {
260 16     16   62 my $self = shift;
261 16     2   34 state $check = compile(PositiveOrZeroInt->where(sub { $_ < 10; }), { optional => 1 });
  2         38  
262 16         3521 my ($max_attempts) = $check->(@_);
263 16 100       169 $self->{max_attempts} = $max_attempts if $max_attempts;
264 16 100 100     63 $self->{max_attempts} = 4 if defined $max_attempts && $max_attempts == 0;
265 16         92 return $self->{max_attempts};
266             }
267              
268             1;
269              
270             __END__
271              
272             =head1 NAME
273              
274             Google::RestApi - Connection to Google REST APIs (currently Drive and Sheets).
275              
276             =head1 SYNOPSIS
277              
278             =over
279              
280             use Google::RestApi;
281             $rest_api = Google::RestApi->new(
282             config_file => <path_to_config_file>,
283             auth => <object|hashref>,
284             timeout => <int>,
285             throttle => <int>,
286             api_callback => <coderef>,
287             );
288              
289             $response = $rest_api->api(
290             uri => <google_api_url>,
291             method => get|head|put|patch|post|delete,
292             headers => [],
293             params => <query_params>,
294             content => <data_for_body>,
295             );
296              
297             use Google::RestApi::SheetsApi4;
298             $sheets_api = Google::RestApi::SheetsApi4->new(api => $rest_api);
299             $sheet = $sheets_api->open_spreadsheet(title => "payroll");
300              
301             use Google::RestApi::DriveApi3;
302             $drive = Google::RestApi::DriveApi3->new(api => $rest_api);
303             $file = $drive->file(id => 'xxxx');
304             $copy = $file->copy(title => 'my-copy-of-xxx');
305              
306             print YAML::Any::Dump($rest_api->stats());
307              
308             =back
309              
310             =head1 DESCRIPTION
311              
312             Google Rest API is the foundation class used by the included Drive (L<Google::RestApi::DriveApi3>) and Sheets (L<Google::RestApi::SheetsApi4>) APIs. It is used
313             to send API requests to the Google API endpoint on behalf of the underlying API classes.
314              
315             =head1 NAVIGATION
316              
317             =over
318              
319             =item * L<Google::RestApi::DriveApi3>
320              
321             =item * L<Google::RestApi::DriveApi3::File>
322              
323             =item * L<Google::RestApi::SheetsApi4>
324              
325             =item * L<Google::RestApi::SheetsApi4::Spreadsheet>
326              
327             =item * L<Google::RestApi::SheetsApi4::Worksheet>
328              
329             =item * L<Google::RestApi::SheetsApi4::Range>
330              
331             =item * L<Google::RestApi::SheetsApi4::Range::All>
332              
333             =item * L<Google::RestApi::SheetsApi4::Range::Col>
334              
335             =item * L<Google::RestApi::SheetsApi4::Range::Row>
336              
337             =item * L<Google::RestApi::SheetsApi4::Range::Cell>
338              
339             =item * L<Google::RestApi::SheetsApi4::RangeGroup>
340              
341             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Iterator>
342              
343             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Tie>
344              
345             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Tie::Iterator>
346              
347             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet>
348              
349             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet::Worksheet>
350              
351             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet::Worksheet::Range>
352              
353             =back
354              
355             =head1 SUBROUTINES
356              
357             =over
358              
359             =item new(%args); %args consists of:
360              
361             =over
362              
363             =item * C<config_file> <path_to_config_file>: Optional YAML configuration file that can specify any or all of the following args...
364              
365             =item * C<auth> <hash|object>: A hashref to create the specified auth class, or (outside the config file) an instance of the blessed class itself.
366             If this is an object, it must provide the 'params' and 'headers' subroutines to provide the appropriate Google authentication/authorization.
367             See below for more details.
368              
369             =item * C<api_callback> <coderef>: A coderef to call after each API call.
370              
371             =item * C<throttle> <int>: Used in development to sleep the number of seconds specified between API calls to avoid rate limit violations from Google.
372              
373             =back
374              
375             You can specify any of the arguments in the optional YAML config file. Any passed-in arguments will override what is in the config file.
376              
377             The 'auth' arg can specify a pre-blessed class of one of the Google::RestApi::Auth::* classes (e.g. 'OAuth2Client'), or, for convenience sake,
378             you may specify a hash of the required arguments to create an instance of that class:
379              
380             auth:
381             class: OAuth2Client
382             client_id: xxxxxx
383             client_secret: xxxxxx
384             token_file: <path_to_token_file>
385              
386             Note that the auth hash itself can also contain a config_file:
387              
388             auth:
389             class: OAuth2Client
390             config_file: <path_to_oauth_config_file>
391              
392             This allows you the option to keep the auth file in a separate, more secure place.
393              
394             =item api(%args);
395              
396             The ultimate Google API call for the underlying classes. Handles timeouts and retries etc. %args consists of:
397              
398             =over
399              
400             =item * C<uri> <uri_string>: The Google API endpoint such as https://www.googleapis.com/drive/v3 along with any path segments added.
401              
402             =item * C<method> <http_method_string>: The http method being used get|head|put|patch|post|delete.
403              
404             =item * C<headers> <headers_string_array>: Array ref of http headers.
405              
406             =item * C<params> <query_parameters_hash>: Http query params to be added to the uri.
407              
408             =item * C<content> <payload hash>: The body being sent for post/put etc. Will be encoded to JSON.
409              
410             =back
411              
412             You would not normally call this directly unless you were making a Google API call not currently supported by this API framework.
413              
414             Returns the response hash from Google API.
415              
416             =item api_callback(<coderef>);
417              
418             =over
419              
420             =item C<coderef> is user code that will be called back after each call to the Google API.
421              
422             =back
423              
424             The last transaction details are passed to the callback. What you do with this information is up to you. For an example of how this is used, see the
425             C<t/tutorial/sheets/*> scripts.
426              
427             Returns the previous callback, if any.
428              
429             =item transaction();
430              
431             Returns the transaction information from the last Google API call. This is the same information that is provided by the callback
432             above, but can be accessed directly if you have no need to provide a callback.
433              
434             =item stats();
435              
436             Returns some statistics on how many get/put/post etc calls were made. Useful for performance tuning during development.
437              
438             =back
439              
440             =head1 STATUS
441              
442             This api is currently in beta status. It is incomplete. There may be design flaws that need to be addressed in later releases. Later
443             releases may break this release. Not all api calls have been implemented.
444              
445             =head1 AUTHORS
446              
447             =over
448              
449             =item
450              
451             Robin Murray mvsjes@cpan.org
452              
453             =back
454              
455             =head1 COPYRIGHT
456              
457             Copyright (c) 2021, Robin Murray. All rights reserved.
458              
459             This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.