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.4';
4              
5 1     1   4354 use Google::RestApi::Setup;
  1         2  
  1         12  
6              
7 1     1   2918 use File::Basename qw( dirname );
  1         3  
  1         57  
8 1     1   676 use Furl ();
  1         31155  
  1         36  
9 1     1   590 use JSON::MaybeXS qw( decode_json encode_json JSON );
  1         4407  
  1         71  
10 1     1   9 use List::Util ();
  1         3  
  1         23  
11 1     1   5 use Log::Log4perl qw( get_logger );
  1         2  
  1         12  
12 1     1   32 use Module::Load qw( load );
  1         3  
  1         17  
13 1     1   106 use Scalar::Util qw( blessed );
  1         6  
  1         387  
14 1     1   549 use Retry::Backoff qw( retry );
  1         3148  
  1         57  
15 1     1   7 use Storable qw( dclone );
  1         1  
  1         45  
16 1     1   6 use Try::Tiny qw( catch try );
  1         3  
  1         47  
17 1     1   652 use URI ();
  1         4969  
  1         26  
18 1     1   456 use URI::QueryParam ();
  1         901  
  1         2596  
19              
20             sub new {
21 159     159 1 4150384 my $class = shift;
22              
23 159         778 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   402 max_attempts => PositiveInt->where(sub { $_ < 10; }), { default => 4 },
  158         9400  
30             );
31 158         12568 $self = $check->(%$self);
32              
33 158         3205 $self->{ua} = Furl->new(timeout => $self->{timeout});
34              
35 158         8977 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 73612 my $self = shift;
42              
43 254         536 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         16186 my $request = $check->(@_);
51              
52             # reset our transaction for this new one.
53 252         16720 $self->{transaction} = {};
54              
55 252         1170 $self->_stat( $request->{method}, 'total' );
56 252         1245 $request->{method} = uc($request->{method});
57 252         688 $request->{caller_internal} = _caller_internal();
58 252         756 $request->{caller_external} = _caller_external();
59              
60 252         742 my $request_content = $request->{content};
61 252 100       1036 my $request_json = defined $request_content ? encode_json($request_content) : (),
62              
63             my @headers;
64 252 100       618 push(@headers, 'Content-Type' => 'application/json') if $request_json;
65 252         391 push(@headers, @{ $request->{headers} });
  252         751  
66 252         496 push(@headers, @{ $self->auth()->headers() });
  252         873  
67              
68             # some (outdated) auth mechanisms may allow auth info in the params.
69 252         6042 my %params = (%{ $request->{params} }, %{ $self->auth()->params() });
  252         1085  
  252         653  
70 252         1661 my $uri = URI->new($request->{uri});
71 252         32013 $uri->query_form_hash(\%params);
72 252         28177 $request->{uri} = $uri->as_string();
73 252         2048 DEBUG("Rest API request:\n", Dump($request));
74              
75             my $req = HTTP::Request->new(
76 252         909203 $request->{method}, $request->{uri}, \@headers, $request_json
77             );
78             # this is where the action is.
79 252         36629 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       1978 request => $request,
    100          
84             tries => $tries,
85             ($response ? (response => $response) : ()),
86             ($last_error ? (error => $last_error) : ()),
87             };
88              
89 252 100       751 if ($response) {
90 250         1036 my $decoded_response = $response->decoded_content();
91 250 100       92788 $decoded_response = $decoded_response ? decode_json($decoded_response) : 1;
92 250         926 $self->{transaction}->{decoded_response} = $decoded_response;
93 250         1016 DEBUG("Rest API response:\n", Dump( $decoded_response ));
94             }
95              
96             # calls the callback when an api call is madem, if any.
97 252         748691 $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     1289 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     2278 if (!$response || !$response->is_success()) {
123 8         72 $self->_stat('error');
124 8         36 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       2716 sleep($self->{throttle}) if $self->{throttle};
129              
130 244         2247 return $self->{transaction}->{decoded_response};
131             }
132              
133             # this wraps the http api call around retries.
134             sub _api {
135 252     252   867 my ($self, $req) = @_;
136              
137             # default is exponential backoff, initial delay 1.
138 252         458 my $tries = 0;
139 252         438 my $last_error;
140             my $response = retry sub {
141 270     270   46518 $self->{ua}->request($req);
142             },
143             retry_if => sub {
144 270     270   302762 my $h = shift;
145 270         1023 my $r = $h->{attempt_result}; # Furl::Response
146 270 100       812 if (!$r) {
147 8   50     24 $last_error = $@ || "Unknown error";
148 8         34 WARN("API call error: $last_error");
149 8         65 return 1;
150             }
151 262 100       849 $last_error = $r->status_line() if !$r->is_success();
152 262 100       2824 if ($r->code() =~ /^(403|429|50[0234])$/) {
153 16         151 WARN("Retrying: $last_error");
154 16         131 return 1;
155             }
156 246         1854 return; # we're accepting the response.
157             },
158 246     246   2455 on_success => sub { $tries++; },
159 24     24   205 on_failure => sub { $tries++; },
160 252         2789 max_attempts => $self->max_attempts(); # override default max_attempts 10.
161 252         40807 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 1037 my $self = shift;
167              
168 508 100       2136 if (!blessed($self->{auth})) {
169             # turn OAuth2Client into Google::RestApi::Auth::OAuth2Client etc.
170 143         801 my $class = __PACKAGE__ . "::Auth::" . delete $self->{auth}->{class};
171 143         626 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       18216 if $self->{config_file};
176 142         419 $self->{auth} = $class->new(%{ $self->{auth} });
  142         1904  
177             }
178              
179 505         3011 return $self->{auth};
180             }
181              
182             # if user registered a callback, notify them of an api call.
183             sub _api_callback {
184 252     252   600 my $self = shift;
185 252 100       1141 return if !$self->{api_callback};
186             try {
187 5     5   230 $self->{api_callback}->( $self->transaction() );
188             } catch {
189 1     1   24 my $err = $_;
190 1         4 FATAL("Post process died: $err");
191 5         35 };
192 5         93 return;
193             }
194              
195             # sets the api callback code.
196             sub api_callback {
197 5     5 1 16599 my $self = shift;
198 5         21 state $check = compile(CodeRef, { optional => 1 });
199 5         2182 my ($api_callback) = $check->(@_);
200 5         64 my $prev_api_callback = delete $self->{api_callback};
201 5 100       18 $self->{api_callback} = $api_callback if $api_callback;
202 5         25 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   545 my $self = shift;
209 260         675 my @stats = @_;
210 260         688 foreach (@stats) {
211 512         1175 $_ = lc;
212 512   100     2463 $self->{stats}->{$_} //= 0;
213 512         1197 $self->{stats}->{$_}++;
214             }
215 260         609 return;
216             }
217              
218             # returns the stats recorded above.
219             sub stats {
220 6     6 1 28 my $self = shift;
221 6   50     188 my $stats = dclone($self->{stats} || {});
222 6         54 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 5025 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   747 my ($package, $subroutine, $line, $i) = ('', '', 0);
234 252   100     473 do {
      66        
235 1547         33880 ($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       7555 ($package, undef, $line, $subroutine) = caller(--$i)
244             if !$package;
245 252         1212 return "$package:$line => $subroutine";
246             }
247              
248             sub _caller_external {
249 252     252   641 my ($package, $subroutine, $line, $i) = ('', '', 0);
250 252   66     423 do {
251 2322         41452 ($package, undef, $line, $subroutine) = caller(++$i);
252             } while($package && $package =~ m[^(Google::RestApi|Cache|Try)]);
253 252         5815 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   43 my $self = shift;
261 16     2   34 state $check = compile(PositiveOrZeroInt->where(sub { $_ < 10; }), { optional => 1 });
  2         43  
262 16         3132 my ($max_attempts) = $check->(@_);
263 16 100       147 $self->{max_attempts} = $max_attempts if $max_attempts;
264 16 100 100     53 $self->{max_attempts} = 4 if defined $max_attempts && $max_attempts == 0;
265 16         86 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.