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