File Coverage

blib/lib/WebService/GoogleAPI/Client/Discovery.pm
Criterion Covered Total %
statement 172 192 89.5
branch 58 80 72.5
condition 14 20 70.0
subroutine 25 30 83.3
pod 12 17 70.5
total 281 339 82.8


line stmt bran cond sub pod time code
1 3     3   25 use strictures;
  3         22  
  3         31  
2              
3             package WebService::GoogleAPI::Client::Discovery;
4              
5             our $VERSION = '0.25'; # VERSION
6              
7             # ABSTRACT: Google API discovery service
8              
9              
10 3     3   792 use Moo;
  3         20  
  3         22  
11 3     3   1216 use Carp;
  3         7  
  3         220  
12 3     3   35 use WebService::GoogleAPI::Client::UserAgent;
  3         8  
  3         24  
13 3     3   119 use List::Util qw/uniq reduce/;
  3         8  
  3         265  
14 3     3   1824 use List::SomeUtils qw/pairwise/;
  3         31093  
  3         363  
15 3     3   34 use Data::Dump qw/pp/;
  3         9  
  3         139  
16 3     3   1560 use CHI;
  3         82984  
  3         8377  
17              
18             has ua => (
19             is => 'rw',
20             default => sub { WebService::GoogleAPI::Client::UserAgent->new }
21             );
22             has debug => (is => 'rw', default => 0);
23             has 'chi' => (
24             is => 'rw',
25             default => sub { CHI->new(driver => 'File', namespace => __PACKAGE__) },
26             lazy => 1
27             ); ## i believe that this gives priority to param if provided ?
28             has 'stats' => is => 'rw',
29             default => sub { { network => { get => 0 }, cache => { get => 0 } } };
30              
31              
32             sub get_with_cache {
33 32     32 1 119 my ($self, $key, $force, $authorized) = @_;
34              
35 32   100     949 my $expiration = $self->chi->get_expires_at($key) // 0;
36              
37 32         20517 my $will_expire = $expiration - time();
38 32 100 100     241 if ($will_expire > 0 && not $force) {
39 24 50       136 carp "discovery_data cached data expires in $will_expire seconds"
40             if $self->debug > 2;
41 24         639 my $ret = $self->chi->get($key);
42 24 50       38534 croak 'was expecting a HASHREF!' unless ref $ret eq 'HASH';
43 24         156 $self->stats->{cache}{get}++;
44 24         178 return $ret;
45             } else {
46 8         23 my $ret;
47 8 100       28 if ($authorized) {
48 1         13 $ret = $self->ua->validated_api_query($key);
49 1         21 $self->stats->{network}{authorized}++;
50             } else {
51 7         107 $ret = $self->ua->get($key)->res;
52             }
53 8 50       1704778 if ($ret->is_success) {
54 8         199 my $all = $ret->json;
55 8         1088060 $self->stats->{network}{get}++;
56 8         427 $self->chi->set($key, $all, '30d');
57 8         31687 return $all;
58             } else {
59 0 0 0     0 if ($ret->code == 403 && !$authorized) {
60 0         0 return $self->get_with_cache($key, $force, 1);
61             }
62 0         0 croak $ret->message;
63             }
64             }
65 0         0 return {};
66             }
67              
68              
69 12     12 0 179323 sub discover_key { 'https://www.googleapis.com/discovery/v1/apis' }
70              
71             sub discover_all {
72 10     10 1 366887 my $self = shift;
73 10         52 $self->get_with_cache($self->discover_key, @_);
74             }
75              
76             #TODO- double triple check that we didn't break anything with the
77             #hashref change
78              
79              
80             #TODO- maybe cache this on disk too?
81             my $available;
82              
83             sub _invalidate_available {
84 1     1   4 $available = undef;
85             }
86              
87             sub available_APIs {
88              
89             #cache this crunch
90 100 100   100 1 330734 return $available if $available;
91              
92 3         10 my ($self) = @_;
93 3         15 my $d_all = $self->discover_all;
94 3 50       20 croak 'no items in discovery data' unless defined $d_all->{items};
95              
96             #grab only entries with the four keys we want
97             #and strip other keys
98 3         16 my @keys = qw/name version documentationLink discoveryRestUrl/;
99 3         8 my @relevant;
100 3         9 for my $i (@{ $d_all->{items} }) {
  3         13  
101 970 100       1570 next unless @keys == grep { exists $i->{$_} } @keys;
  3880         7126  
102 964         1370 push @relevant, { %{$i}{@keys} };
  964         3183  
103             }
104              
105             my $reduced = reduce {
106 964     964   1493 for my $key (qw/version documentationLink discoveryRestUrl/) {
107 2892   100     10220 $a->{ $b->{name} }->{$key} //= [];
108 2892         3953 push @{ $a->{ $b->{name} }->{$key} }, $b->{$key};
  2892         6996  
109             }
110 964         1390 $a;
111             }
112 3         62 {}, @relevant;
113              
114             #store it away globally
115 3         1363 $available = $reduced;
116             }
117              
118              
119             sub augment_discover_all_with_unlisted_experimental_api {
120 0     0 0 0 my ($self, $api_spec) = @_;
121 0         0 carp <
122             This lengthy function name (augment_discover_all_with_unlisted_experimental_api)
123             will be removed soon. Please use 'augment_with' instead.
124             DEPRECATION
125 0         0 $self->augment_with($api_spec);
126             }
127              
128             sub augment_with {
129 1     1 1 179803 my ($self, $api_spec) = @_;
130              
131 1         16 my $all = $self->discover_all();
132              
133             ## fail if any of the expected fields are not provided
134 1         5 for my $field (
135             qw/version title description id kind documentationLink
136             discoveryRestUrl name/
137             ) {
138 8 50       23 if (not defined $api_spec->{$field}) {
139 0         0 carp("required $field in provided api spec missing");
140             }
141             }
142              
143 1         2 push @{ $all->{items} }, $api_spec;
  1         5  
144 1         27 $self->chi->set($self->discover_key, $all, '30d');
145 1         2229 $self->_invalidate_available;
146 1         351 return $all;
147             }
148              
149              
150              
151             sub service_exists {
152 43     43 1 4324 my ($self, $api) = @_;
153 43 50       114 return unless $api;
154 43         130 return $self->available_APIs->{$api};
155             }
156              
157              
158             sub available_versions {
159 28     28 1 3031 my ($self, $api) = @_;
160 28 50       75 return [] unless $api;
161 28   100     72 return $self->available_APIs->{$api}->{version} // [];
162             }
163              
164              
165             sub latest_stable_version {
166 23     23 1 2025 my ($self, $api) = @_;
167 23 50       69 return '' unless $api;
168 23         66 my $versions = $self->available_versions($api);
169 23 50       68 return '' unless $versions;
170 23 50       44 return '' unless @{$versions} > 0;
  23         78  
171              
172             #remove alpha or beta versions
173 23         66 my @stable = grep { !/beta|alpha/ } @$versions;
  60         304  
174 23   50     132 return $stable[-1] || '';
175             }
176              
177              
178             sub process_api_version {
179 28     28 1 2936 my ($self, $params) = @_;
180              
181             # scalar parameter not hashref - so assume is intended to be $params->{api}
182 28 100       136 $params = { api => $params } if ref $params eq '';
183              
184 28 50       110 croak "'api' must be defined" unless $params->{api};
185              
186             ## trim any resource, method or version details in api id
187 28 100       129 if ($params->{api} =~ /([^:]+):(v[^\.]+)/ixsm) {
188 3         14 $params->{api} = $1;
189 3         14 $params->{version} = $2;
190             }
191 28 100       143 if ($params->{api} =~ /^(.*?)\./xsm) {
192 10         38 $params->{api} = $1;
193             }
194              
195 28 100       103 unless ($self->service_exists($params->{api})) {
196 2         371 croak "$params->{api} does not seem to be a valid Google API";
197             }
198              
199 26   66     160 $params->{version} //= $self->latest_stable_version($params->{api});
200 26         82 return $params;
201             }
202              
203              
204              
205             sub get_api_discovery_for_api_id {
206 0     0 0 0 carp <
207             This long method name (get_api_discovery_for_api_id) is being deprecated
208             in favor of get_api_document. Please switch your code soon
209             DEPRECATION
210 0         0 shift->get_api_document(@_);
211             }
212              
213             sub get_api_document {
214 23     23 1 216 my ($self, $arg) = @_;
215              
216 23         101 my $params = $self->process_api_version($arg);
217 22         63 my $apis = $self->available_APIs();
218              
219 22         62 my $api = $apis->{ $params->{api} };
220 22 50       72 croak "No versions found for $params->{api}" unless $api->{version};
221              
222 22         50 my @versions = @{ $api->{version} };
  22         71  
223 22         43 my @urls = @{ $api->{discoveryRestUrl} };
  22         65  
224 22 100   56   315 my ($url) = pairwise { $a eq $params->{version} ? $b : () } @versions, @urls;
  56         215  
225              
226 22 50       124 croak "Couldn't find correct url for $params->{api} $params->{version}"
227             unless $url;
228              
229 22         89 $self->get_with_cache($url);
230             }
231              
232             #TODO- HERE - we are here in refactoring
233             sub _extract_resource_methods_from_api_spec {
234 128     128   290 my ($self, $tree, $api_spec, $ret) = @_;
235 128 100       288 $ret = {} unless defined $ret;
236 128 50       299 croak("ret not a hash - $tree, $api_spec, $ret") unless ref($ret) eq 'HASH';
237              
238 128 100 66     481 if (defined $api_spec->{methods} && ref($api_spec->{methods}) eq 'HASH') {
239 92         170 foreach my $method (keys %{ $api_spec->{methods} }) {
  92         377  
240             $ret->{"$tree.$method"} = $api_spec->{methods}{$method}
241 478 50       2316 if ref($api_spec->{methods}{$method}) eq 'HASH';
242             }
243             }
244 128 100       376 if (defined $api_spec->{resources}) {
245 54         104 foreach my $resource (keys %{ $api_spec->{resources} }) {
  54         189  
246             ## NB - recursive traversal down tree of api_spec resources
247             $self->_extract_resource_methods_from_api_spec("$tree.$resource",
248 108         420 $api_spec->{resources}{$resource}, $ret);
249             }
250             }
251 128         299 return $ret;
252             }
253              
254              
255              
256             sub extract_method_discovery_detail_from_api_spec {
257 0     0 0 0 carp <
258             This rather long method name (extract_method_discovery_detail_from_api_spec)
259             is being deprecated in favor of get_method_details. Please switch soon
260             DEPRECATION
261 0         0 shift->get_method_details(@_);
262             }
263              
264             sub get_method_details {
265 11     11 1 432 my ($self, $tree, $api_version) = @_;
266             ## where tree is the method in format from _extract_resource_methods_from_api_spec() like projects.models.versions.get
267             ## the root is the api id - further '.' sep levels represent resources until the tailing label that represents the method
268 11 50       53 croak 'You must ask for a method!' unless defined $tree;
269              
270 11         73 my @nodes = split /\./smx, $tree;
271 11 50       44 croak(
272             "tree structure '$tree' must contain at least 2 nodes including api id, [list of hierarchical resources ] and method - not "
273             . scalar(@nodes))
274             unless @nodes > 1;
275              
276 11         37 my $api_id = shift(@nodes); ## api was head
277 11         29 my $method = pop(@nodes); ## method was tail
278              
279             ## split out version if is defined as part of $tree
280             ## trim any resource, method or version details in api id
281             ## we have already isolated head from api tree children
282 11 100       57 if ($api_id =~ /([^:]+):([^\.]+)$/ixsm) {
283 2         9 $api_id = $1;
284 2         5 $api_version = $2;
285             }
286              
287             ## handle incorrect api_id
288 11 50       46 if ($self->service_exists($api_id) == 0) {
289 0         0 croak("unable to confirm that '$api_id' is a valid Google API service id");
290             }
291              
292 11 100       49 $api_version = $self->latest_stable_version($api_id) unless $api_version;
293              
294              
295             ## TODO: confirm that spec available for api version
296 11         72 my $api_spec =
297             $self->get_api_document({ api => $api_id, version => $api_version });
298              
299              
300             ## we use the schemas to substitute into '$ref' keyed placeholders
301 11         38 my $schemas = {};
302 11         31 foreach my $schema_key (sort keys %{ $api_spec->{schemas} }) {
  11         626  
303 926         2159 $schemas->{$schema_key} = $api_spec->{'schemas'}{$schema_key};
304             }
305              
306             ## recursive walk through the structure in _fix_ref
307             ## substitute the schema keys into the total spec to include
308             ## '$ref' values within the schema structures themselves
309             ## including within the schema spec structures (NB assumes no cyclic structures )
310             ## otherwise would could recursive chaos
311 11         95 my $api_spec_fix = $self->_fix_ref($api_spec, $schemas)
312             ; ## first level ( '$ref' in the method params and return values etc )
313 11         37 $api_spec = $self->_fix_ref($api_spec_fix, $schemas)
314             ; ## second level ( '$ref' in the interpolated schemas from first level )
315              
316             ## now extract all the methods (recursive )
317 11         2152 my $all_api_methods =
318             $self->_extract_resource_methods_from_api_spec("$api_id:$api_version",
319             $api_spec);
320              
321 11 100       61 unless (defined $all_api_methods->{$tree}) {
322 9         37 $all_api_methods =
323             $self->_extract_resource_methods_from_api_spec($api_id, $api_spec);
324             }
325 11 100       55 if ($all_api_methods->{$tree}) {
326              
327             #add in the global parameters to the endpoint,
328             #stored in the top level of the api_spec
329             # TODO - why are we mutating the main hash?
330             $all_api_methods->{$tree}{parameters} = {
331 10         60 %{ $all_api_methods->{$tree}{parameters} },
332 10         23 %{ $api_spec->{parameters} }
  10         152  
333             };
334 10         16817 return $all_api_methods->{$tree};
335             }
336              
337             croak(
338 1         326 "Unable to find method detail for '$tree' within Google Discovery Spec for $api_id version $api_version"
339             );
340             }
341             ########################################################
342              
343             ########################################################
344             ########################################################
345              
346             #=head2 C
347             #
348             #This sub walks through the structure and replaces any hashes keyed with '$ref' with
349             #the value defined in $schemas->{ }
350             #
351             #eg
352             # ->{'response'}{'$ref'}{'Buckets'}
353             # is replaced with
354             # ->{response}{ $schemas->{Buckets} }
355             #
356             # It assumes that the schemas have been extracted from the original discover for the API
357             # and is typically applied to the method ( api endpoint ) to provide a fully descriptive
358             # structure without external references.
359             #
360             #=cut
361              
362             ########################################################
363             sub _fix_ref {
364 127178     127178   223112 my ($self, $node, $schemas) = @_;
365 127178         179904 my $ret = undef;
366 127178         190829 my $r = ref($node);
367              
368              
369 127178 100       242865 if ($r eq 'ARRAY') {
    100          
370 5177         8788 $ret = [];
371 5177         9591 foreach my $el (@$node) {
372 31269         54640 push @$ret, $self->_fix_ref($el, $schemas);
373             }
374             } elsif ($r eq 'HASH') {
375 31667         51429 $ret = {};
376 31667         90290 foreach my $key (keys %$node) {
377 100773 100       180262 if ($key eq '$ref') {
378              
379             #say $node->{'$ref'};
380 4886         12243 $ret = $schemas->{ $node->{'$ref'} };
381             } else {
382 95887         186766 $ret->{$key} = $self->_fix_ref($node->{$key}, $schemas);
383             }
384             }
385             } else {
386 90334         132450 $ret = $node;
387             }
388              
389 127178         319091 return $ret;
390             }
391             ########################################################
392              
393              
394              
395             #TODO: consider ? refactor to allow parameters either as a single api id such as 'gmail'
396             # as well as the currently accepted hash keyed on the api and version
397             #
398             #SEE ALSO:
399             # The following methods are delegated through to Client::Discovery - see perldoc WebService::Client::Discovery for detils
400             #
401             # get_method_meta
402             # discover_all
403             # extract_method_discovery_detail_from_api_spec
404             # get_api_discovery_for_api_id
405              
406             ########################################################
407             #TODO- give short name and deprecate
408             sub methods_available_for_google_api_id {
409 0     0 1 0 my ($self, $api_id, $version) = @_;
410              
411 0 0       0 $version = $self->latest_stable_version($api_id) unless $version;
412             ## TODO: confirm that spec available for api version
413 0         0 my $api_spec = $self->get_api_discovery_for_api_id(
414             { api => $api_id, version => $version });
415 0         0 my $methods =
416             $self->_extract_resource_methods_from_api_spec($api_id, $api_spec);
417 0         0 return $methods;
418             }
419             ########################################################
420              
421              
422              
423             sub list_of_available_google_api_ids {
424 0     0 0 0 carp <
425             This rather long function name (list_of_available_google_api_ids)
426             is being deprecated in favor of the shorter list_api_ids. Please
427             update your code accordingly.
428             DEPRECATION
429 0         0 shift->list_api_ids;
430             }
431             ## returns a list of all available API Services
432             sub list_api_ids {
433 6     6 1 24018 my ($self) = @_;
434 6         12 my @api_list = keys %{ $self->available_APIs };
  6         14  
435 6 100       391 return wantarray ? @api_list : join(',', @api_list);
436             }
437              
438              
439             1;
440              
441             __END__