File Coverage

blib/lib/WebService/GoogleAPI/Client.pm
Criterion Covered Total %
statement 106 149 71.1
branch 46 86 53.4
condition 5 11 45.4
subroutine 16 21 76.1
pod 2 6 33.3
total 175 273 64.1


line stmt bran cond sub pod time code
1 3     3   4818 use strictures;
  3         8  
  3         19  
2 3     3   625 use 5.14.0;
  3         9  
3              
4             package WebService::GoogleAPI::Client;
5              
6             our $VERSION = '0.25'; # VERSION
7              
8 3     3   1687 use Data::Dump qw/pp/;
  3         16685  
  3         272  
9 3     3   31 use Moo;
  3         7  
  3         28  
10 3     3   2720 use WebService::GoogleAPI::Client::UserAgent;
  3         14  
  3         39  
11 3     3   2118 use WebService::GoogleAPI::Client::Discovery;
  3         11  
  3         133  
12 3     3   29 use WebService::GoogleAPI::Client::AuthStorage::GapiJSON;
  3         7  
  3         86  
13 3     3   1765 use WebService::GoogleAPI::Client::AuthStorage::ServiceAccount;
  3         13  
  3         113  
14 3     3   25 use Carp;
  3         7  
  3         190  
15 3     3   21 use CHI;
  3         6  
  3         63  
16 3     3   16 use Mojo::Util;
  3         6  
  3         7241  
17              
18             #TODO- batch requests. The only thing necessary is to send a
19             #multipart request as I wrote in that e-mail.
20             #
21             #TODO- implement auth for service accounts.
22             #
23             #TODO- allow using promises instead of just calling. Also allow
24             # for full access to the tx instead of assuming it's always
25             # returning the res. Perhaps mix in something that delegates the
26             # json method to the res?
27              
28             # ABSTRACT: Google API Discovery and SDK
29              
30             # FROM MCE POD --

Bank Queuing Model

31              
32              
33             has 'debug' => (is => 'rw', default => 0, lazy => 1);
34             has 'ua' => (
35             handles => [qw/do_autorefresh auth_storage get_access_token scopes user/],
36             is => 'ro',
37             default =>
38             sub { WebService::GoogleAPI::Client::UserAgent->new(debug => shift->debug) }
39             ,
40             lazy => 1,
41             );
42              
43             sub get_scopes_as_array {
44 0     0 0 0 carp 'get_scopes_as_array has deprecated in favor of the shorter "scopes"';
45 0         0 return $_[0]->scopes;
46             }
47              
48             has 'chi' => (
49             is => 'rw',
50             default => sub {
51             CHI->new(driver => 'File', max_key_length => 512, namespace => __PACKAGE__);
52             },
53             lazy => 1
54             );
55              
56             has 'discovery' => (
57             handles => [
58             qw/ discover_all
59             get_method_details
60             get_api_document service_exists
61             methods_available_for_google_api_id list_api_ids /
62             ],
63             is => 'ro',
64             default => sub {
65             my $self = shift;
66             return WebService::GoogleAPI::Client::Discovery->new(
67             debug => $self->debug,
68             ua => $self->ua,
69             chi => $self->chi
70             );
71             },
72             lazy => 1,
73             );
74              
75             ## provides a way of augmenting constructor (new) without overloading it
76             ## see https://metacpan.org/pod/distribution/Moose/lib/Moose/Manual/Construction.pod if like me you an new to Moose
77              
78              
79             #NOTE- in terms of implementing google's ADC
80             # (see https://cloud.google.com/docs/authentication/production and also
81             # https://github.com/googleapis/python-cloud-core/blob/master/google/cloud/client.py)
82             # I looked into it and based on that, it seems that every environment has different reqs,
83             # so it's a maintenance liability
84             sub BUILD {
85 2     2 0 6010 my ($self, $params) = @_;
86              
87 2         8 my ($storage, $file);
88 2 50       15 if ($params->{auth_storage}) {
    50          
    0          
    0          
89 0         0 $storage = $params->{auth_storage};
90             } elsif ($file = $params->{gapi_json}) {
91 2         22 $storage =
92             WebService::GoogleAPI::Client::AuthStorage::GapiJSON->new(path => $file);
93             } elsif ($file = $params->{service_account}) {
94             $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new(
95             path => $file,
96             scopes => $params->{scopes}
97 0         0 );
98             } elsif ($file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) {
99             $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new(
100             path => $file,
101             scopes => $params->{scopes}
102 0         0 );
103             }
104 2 50       58 $self->auth_storage($storage) if $storage;
105              
106 2 50       112 $self->user($params->{user}) if (defined $params->{user});
107             }
108              
109              
110             ## ASSUMPTIONS:
111             ## - no complex parameter checking ( eg required mediaUpload in endpoint gmail.users.messages.send ) so user assumes responsiiblity
112             ## TODO: Exceeding a rate limit will cause an HTTP 403 or HTTP 429 Too Many Requests response and your app should respond by retrying with exponential backoff. (https://developers.google.com/gmail/api/v1/reference/quota)
113             ## follows the method spec reqeust reference to the api spec schema api_discovery_struct->{schemas}{Message};
114             ## THE METHOD SPEC CONTAINS
115             ## 'request' => {
116             ## '$ref' => 'Message'
117             ## },
118             ## THE SCHEMA api_discovery_struct->{schemas}{Message} CONTAINS
119             ## 'id' => 'Message',
120             ## 'properties' => {
121             ## 'raw' => {
122             ## {annotations}{required}[ 'gmail.users.drafts.create','gmail.users.drafts.update','gmail.users.messages.insert','gmail.users.messages.send' ]
123              
124             ## NB - uses the ua api_query to execute the server request
125             ##################################################
126             sub api_query {
127 0     0 1 0 my ($self, @params_array) = @_;
128              
129             ## TODO - find a more elgant idiom to do this - pulled this off top of head for quick imeplementation
130 0         0 my $params = {};
131 0 0 0     0 if (scalar(@params_array) == 1 && ref($params_array[0]) eq 'HASH') {
132 0         0 $params = $params_array[0];
133             } else {
134 0         0 $params = {@params_array}; ## what happens if not even count
135             }
136 0 0       0 carp(pp $params) if $self->debug > 10;
137              
138             croak "Missing neccessary scopes to access $params->{api_endpoint_id}"
139 0 0       0 unless $self->has_scope_to_access_api_endpoint($params->{api_endpoint_id});
140              
141             ## used to collect pre-query validation errors - if set we return a response
142             # with 418 I'm a teapot
143 0         0 my @teapot_errors = ();
144             ## pre-query validation if api_id parameter is included
145             @teapot_errors = $self->_process_params($params)
146 0 0       0 if (defined $params->{api_endpoint_id});
147              
148             ## either as param or from discovery
149 0 0       0 if (not defined $params->{path}) {
150 0         0 push @teapot_errors, 'path is a required parameter';
151 0         0 $params->{path} = '';
152             }
153             push @teapot_errors,
154             "Path '$params->{path}' includes unfilled variable after processing"
155 0 0       0 if ($params->{path} =~ /\{.+\}/xms);
156             ## carp and include in 418 TEAPOT ERROR - response body with @teapot errors
157 0 0       0 if (@teapot_errors > 0) {
158 0 0       0 carp(join("\n", @teapot_errors)) if $self->debug;
159 0         0 return Mojo::Message::Response->new(
160             content_type => 'text/plain',
161             code => 418,
162             message =>
163             'Teapot Error - Reqeust blocked before submitting to server with pre-query validation errors',
164             body => join("\n", @teapot_errors)
165             );
166             } else {
167             ## query looks good - send to user agent to execute
168 0         0 return $self->ua->validated_api_query($params);
169             }
170             }
171             ##################################################
172              
173             ##################################################
174             ## _ensure_api_spec_has_defined_fields is really only used to allow carping without undef warnings if needed
175             sub _ensure_api_spec_has_defined_fields {
176 11     11   51 my ($self, $api_discovery_struct) = @_;
177             ## Ensure API Discovery has expected fields defined
178 11         47 foreach my $expected_key (
179             qw/path title ownerName version id discoveryVersion
180             revision description documentationLink rest/
181             ) {
182             $api_discovery_struct->{$expected_key} = ''
183 110 100       321 unless defined $api_discovery_struct->{$expected_key};
184             }
185             $api_discovery_struct->{canonicalName} = $api_discovery_struct->{title}
186 11 100       65 unless defined $api_discovery_struct->{canonicalName};
187 11         36 return $api_discovery_struct;
188             }
189             ##################################################
190              
191             ##################################################
192             sub _process_params_for_api_endpoint_and_return_errors {
193 0     0   0 warn
194             '_process_params_for_api_endpoint_and_return_errors has been deprecated. Please use _process_params';
195 0         0 _process_params(@_);
196             }
197              
198             sub _process_params {
199             ## nb - api_endpoint is a param - param key values are modified through this sub
200 12     12   24675 my ($self, $params) = @_;
201              
202             croak('this should never happen - this method is internal only!')
203 12 50       54 unless defined $params->{api_endpoint_id};
204              
205             ## $api_discovery_struct requried for service base URL
206             my $api_discovery_struct = $self->_ensure_api_spec_has_defined_fields(
207 12         441 $self->discovery->get_api_document($params->{api_endpoint_id}));
208             ## remove trailing '/' from baseUrl
209 11         122 $api_discovery_struct->{baseUrl} =~ s/\/$//sxmg;
210              
211             ## if can get discovery data for google api endpoint then continue to perform
212             # detailed checks
213             my $method_discovery_struct =
214 11         343 $self->get_method_details($params->{api_endpoint_id});
215              
216             #save away original path so we can know if it's fiddled with
217             #later
218 10         80 $method_discovery_struct->{origPath} = $method_discovery_struct->{path};
219              
220             ## allow optional user callback pre-processing of method_discovery_struct
221             $method_discovery_struct =
222 1         9 &{ $params->{cb_method_discovery_modify} }($method_discovery_struct)
223             if (defined $params->{cb_method_discovery_modify}
224 10 100 66     70 && ref($params->{cb_method_discovery_modify}) eq 'CODE');
225              
226 10         52 my @teapot_errors = (); ## errors are pushed into this as encountered
227             $params->{method} = $method_discovery_struct->{httpMethod} || 'GET'
228 10 100 50     75 if (not defined $params->{method});
229             push(@teapot_errors,
230             "method mismatch - you requested a $params->{method} which conflicts with discovery spec requirement for $method_discovery_struct->{httpMethod}"
231 10 50       408 ) if ($params->{method} !~ /^$method_discovery_struct->{httpMethod}$/sxim);
232              
233             ## Set default path iff not set by user - NB - will prepend baseUrl later
234 10 100       60 $params->{path} = $method_discovery_struct->{path} unless $params->{path};
235 10 50       44 push @teapot_errors, 'path is a required parameter' unless $params->{path};
236              
237 10         92 push @teapot_errors,
238             $self->_interpolate_path_parameters_append_query_params_and_return_errors(
239             $params, $method_discovery_struct);
240              
241 10         44 $params->{path} =~ s/^\///sxmg; ## remove leading '/' from path
242             $params->{path} = "$api_discovery_struct->{baseUrl}/$params->{path}"
243             unless $params->{path} =~
244 10 100       175 /^$api_discovery_struct->{baseUrl}/ixsmg; ## prepend baseUrl if required
245              
246             ## if errors - add detail available in the discovery struct for the method and service to aid debugging
247 10 100       99 push @teapot_errors,
248             qq{ $api_discovery_struct->{title} $api_discovery_struct->{rest} API into $api_discovery_struct->{ownerName} $api_discovery_struct->{canonicalName} $api_discovery_struct->{version} with id $method_discovery_struct->{id} as described by discovery document version $api_discovery_struct->{discoveryVersion} revision $api_discovery_struct->{revision} with documentation at $api_discovery_struct->{documentationLink} \nDescription: $method_discovery_struct->{description}\n}
249             if @teapot_errors;
250              
251 10         6404 return @teapot_errors;
252             }
253             ##################################################
254              
255             #small subs to convert between these_types to theseTypes of params
256             #TODO- should probs move this into a Util module
257 43 50   43 0 110 sub camel { shift if @_ > 1; $_[0] =~ s/ _(\w) /\u$1/grx }
  43         210  
258 0 0   0 0 0 sub snake { shift if @_ > 1; $_[0] =~ s/([[:upper:]])/_\l$1/grx }
  0         0  
259              
260             ##################################################
261             sub _interpolate_path_parameters_append_query_params_and_return_errors {
262 10     10   36 my ($self, $params, $discovery_struct) = @_;
263 10         31 my @teapot_errors = ();
264              
265 10         23 my @get_query_params = ();
266              
267             #create a hash of whatever the expected params may be
268 10         25 my %path_params;
269 10         352 my $param_regex = qr/\{ \+? ([^\}]+) \}/x;
270 10 100       53 if ($params->{path} ne $discovery_struct->{origPath}) {
271              
272             #check if the path was given as a custom path. If it is, just
273             #interpolate things directly, and assume the user is responsible
274 3         540 %path_params = map { $_ => 'custom' } ($params->{path} =~ /$param_regex/xg);
  3         15  
275             } else {
276              
277             #label which param names are from the normal path and from the
278             #flat path
279             %path_params =
280 7         164 map { $_ => 'plain' } ($discovery_struct->{path} =~ /$param_regex/xg);
  6         49  
281 7 100       39 if ($discovery_struct->{flatPath}) {
282             %path_params = (
283             %path_params,
284 6         78 map { $_ => 'flat' } ($discovery_struct->{flatPath} =~ /$param_regex/xg)
  13         60  
285             );
286             }
287             }
288              
289              
290             #switch the path we're dealing with to the flat path if any of
291             #the parameters match the flat path
292             $params->{path} = $discovery_struct->{flatPath}
293 9         45 if grep { $_ eq 'flat' }
294 10 100       26 map { $path_params{ camel $_} || () } keys %{ $params->{options} };
  17 100       55  
  10         69  
295              
296              
297             #loop through params given, placing them in the path or query,
298             #or leaving them for the request body
299 10         38 for my $param_name (keys %{ $params->{options} }) {
  10         41  
300              
301             #first check if it needs to be interpolated into the path
302 17 100 66     93 if ($path_params{$param_name} || $path_params{ camel $param_name}) {
303              
304             #pull out the value from the hash, and remove the key
305 9         103 my $param_value = delete $params->{options}{$param_name};
306              
307             #Camelize the param name if not passed in customly, allowing
308             #the user to pass in camelCase or snake_case param names.
309             #This implictly allows for a non camelCased param to be left
310             #alone in a custom param.
311 9 50       30 $param_name = camel $param_name if $path_params{ camel $param_name};
312              
313             #first deal with any param that doesn't have a plus, b/c
314             #those just get interpolated
315 9         175 $params->{path} =~ s/\{$param_name\}/$param_value/;
316              
317             #if there's a plus in the path spec, we need more work
318 9 100       124 if ($params->{path} =~ /\{ \+ $param_name \}/x) {
319 4         20 my $pattern = $discovery_struct->{parameters}{$param_name}{pattern};
320              
321             #if the given param matches google's pattern for the
322             #param, just interpolate it straight
323 4 100       151 if ($param_value =~ /$pattern/) {
324 1         17 $params->{path} =~ s/\{\+$param_name\}/$param_value/;
325             } else {
326              
327             #N.B. perhaps support passing an arrayref or csv for those
328             #params such as jobs.projects.jobs.delete which have two
329             #dynamic parts. But for now, unnecessary
330              
331             #remove the regexy parts of the pattern to interpolate it
332             #into the path, assuming the user has provided just the
333             #dynamic portion of the param.
334 3         30 $pattern =~ s/^\^ | \$$//gx;
335 3         15 my $placeholder = qr/ \[ \^ \/ \] \+ /x;
336 3         93 $params->{path} =~ s/\{\+$param_name\}/$pattern/x;
337 3         30 $params->{path} =~ s/$placeholder/$param_value/x;
338             push @teapot_errors, "Not enough parameters given for {+$param_name}."
339 3 100       37 if $params->{path} =~ /$placeholder/;
340             }
341             }
342              
343             #skip to the next run, so I don't need an else clause later
344 9         38 next; #I don't like nested if-elses
345             }
346              
347             #if it's not in the list of params, then it goes in the
348             #request body, and our work here is done
349 8 100       34 next unless $discovery_struct->{parameters}{$param_name};
350              
351             #it must be a GET type query param, so push the name and value
352             #on our param stack and take it off of the options list
353 3         14 push @get_query_params, $param_name, delete $params->{options}{$param_name};
354             }
355              
356             #if there are any query params...
357 10 100       39 if (@get_query_params) {
358              
359             #interpolate and escape the get query params built up in our
360             #former for loop
361 3 100       18 $params->{path} .= ($params->{path} =~ /\?/) ? '&' : '?';
362 3         49 $params->{path} .= Mojo::Parameters->new(@get_query_params);
363             }
364              
365             #interpolate default value for path params if not given. Needed
366             #for things like the gmail API, where userID is 'me' by default
367 10         1215 for my $param_name ($params->{path} =~ /$param_regex/g) {
368 2         10 my $param_value = $discovery_struct->{parameters}{$param_name}{default};
369 2 50       9 $params->{path} =~ s/\{$param_name\}/$param_value/ if $param_value;
370             }
371             push @teapot_errors, "Missing a parameter for {$_}."
372 10         246 for $params->{path} =~ /$param_regex/g;
373              
374             #print pp $params;
375             #exit;
376 10         60 return @teapot_errors;
377             }
378             ##################################################
379              
380              
381             sub has_scope_to_access_api_endpoint {
382 0     0 1   my ($self, $api_ep) = @_;
383 0           my $method_spec = $self->get_method_details($api_ep);
384              
385 0 0         if (keys %$method_spec) {
386 0           my $configured_scopes = $self->scopes; ## get user scopes arrayref
387             ## create a hashindex to facilitate quick lookups
388 0           my %configured_scopes_hash = map { (s/\/$//xr, 1) } @$configured_scopes;
  0            
389             ## NB r switch as per https://www.perlmonks.org/?node_id=613280 to filter
390             #out any trailing '/'
391 0           my $granted = 0;
392             ## assume permission not granted until we find a matching scope
393 0           my $required_scope_count = 0;
394             ## if the final count of scope constraints = 0 then we will assume permission is granted - this has proven necessary for the experimental Google My Business because scopes are not defined in the current discovery data as at 14/10/18
395 0           for my $method_scope (map { s/\/$//xr } @{ $method_spec->{scopes} }) {
  0            
  0            
396 0           $required_scope_count++;
397 0 0         $granted = 1 if defined $configured_scopes_hash{$method_scope};
398 0 0         last if $granted;
399             }
400 0 0         $granted = 1 if $required_scope_count == 0;
401 0           return $granted;
402             } else {
403 0           return 0;
404             }
405              
406             }
407              
408              
409             #TODO: Consider rename to return_fetched_google_v1_apis_discovery_structure
410             #
411             #TODO - handle auth required error and resubmit request with OAUTH headers if response indicates
412             # access requires auth ( when exceed free access limits )
413              
414              
415             1;
416              
417             __END__