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   27 use strictures;
  3         8  
  3         30  
2              
3             package WebService::GoogleAPI::Client::Discovery;
4              
5             our $VERSION = '0.26'; # VERSION
6              
7             # ABSTRACT: Google API discovery service
8              
9              
10 3     3   781 use Moo;
  3         19  
  3         21  
11 3     3   1028 use Carp;
  3         17  
  3         207  
12 3     3   21 use WebService::GoogleAPI::Client::UserAgent;
  3         9  
  3         20  
13 3     3   133 use List::Util qw/uniq reduce/;
  3         7  
  3         228  
14 3     3   1717 use List::SomeUtils qw/pairwise/;
  3         28648  
  3         293  
15 3     3   26 use Data::Dump qw/pp/;
  3         8  
  3         135  
16 3     3   1357 use CHI;
  3         79244  
  3         7834  
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 123 my ($self, $key, $force, $authorized) = @_;
34              
35 32   100     878 my $expiration = $self->chi->get_expires_at($key) // 0;
36              
37 32         22355 my $will_expire = $expiration - time();
38 32 100 100     248 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         684 my $ret = $self->chi->get($key);
42 24 50       39468 croak 'was expecting a HASHREF!' unless ref $ret eq 'HASH';
43 24         145 $self->stats->{cache}{get}++;
44 24         179 return $ret;
45             } else {
46 8         15 my $ret;
47 8 100       29 if ($authorized) {
48 1         8 $ret = $self->ua->validated_api_query($key);
49 1         17 $self->stats->{network}{authorized}++;
50             } else {
51 7         91 $ret = $self->ua->get($key)->res;
52             }
53 8 50       1741581 if ($ret->is_success) {
54 8         179 my $all = $ret->json;
55 8         1040269 $self->stats->{network}{get}++;
56 8         431 $self->chi->set($key, $all, '30d');
57 8         28524 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 152874 sub discover_key { 'https://www.googleapis.com/discovery/v1/apis' }
70              
71             sub discover_all {
72 10     10 1 311359 my $self = shift;
73 10         44 $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 282332 return $available if $available;
91              
92 3         11 my ($self) = @_;
93 3         13 my $d_all = $self->discover_all;
94 3 50       19 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         9 my @relevant;
100 3         10 for my $i (@{ $d_all->{items} }) {
  3         13  
101 1003 100       1635 next unless @keys == grep { exists $i->{$_} } @keys;
  4012         7443  
102 997         1469 push @relevant, { %{$i}{@keys} };
  997         3183  
103             }
104              
105             my $reduced = reduce {
106 997     997   1549 for my $key (qw/version documentationLink discoveryRestUrl/) {
107 2991   100     10675 $a->{ $b->{name} }->{$key} //= [];
108 2991         3997 push @{ $a->{ $b->{name} }->{$key} }, $b->{$key};
  2991         7338  
109             }
110 997         1429 $a;
111             }
112 3         67 {}, @relevant;
113              
114             #store it away globally
115 3         1131 $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 155083 my ($self, $api_spec) = @_;
130              
131 1         8 my $all = $self->discover_all();
132              
133             ## fail if any of the expected fields are not provided
134 1         4 for my $field (
135             qw/version title description id kind documentationLink
136             discoveryRestUrl name/
137             ) {
138 8 50       18 if (not defined $api_spec->{$field}) {
139 0         0 carp("required $field in provided api spec missing");
140             }
141             }
142              
143 1         3 push @{ $all->{items} }, $api_spec;
  1         4  
144 1         25 $self->chi->set($self->discover_key, $all, '30d');
145 1         1937 $self->_invalidate_available;
146 1         300 return $all;
147             }
148              
149              
150              
151             sub service_exists {
152 43     43 1 3790 my ($self, $api) = @_;
153 43 50       104 return unless $api;
154 43         136 return $self->available_APIs->{$api};
155             }
156              
157              
158             sub available_versions {
159 28     28 1 2555 my ($self, $api) = @_;
160 28 50       70 return [] unless $api;
161 28   100     63 return $self->available_APIs->{$api}->{version} // [];
162             }
163              
164              
165             sub latest_stable_version {
166 23     23 1 1650 my ($self, $api) = @_;
167 23 50       73 return '' unless $api;
168 23         73 my $versions = $self->available_versions($api);
169 23 50       69 return '' unless $versions;
170 23 50       48 return '' unless @{$versions} > 0;
  23         81  
171              
172             #remove alpha or beta versions
173 23         64 my @stable = grep { !/beta|alpha/ } @$versions;
  60         306  
174 23   50     129 return $stable[-1] || '';
175             }
176              
177              
178             sub process_api_version {
179 28     28 1 13350 my ($self, $params) = @_;
180              
181             # scalar parameter not hashref - so assume is intended to be $params->{api}
182 28 100       143 $params = { api => $params } if ref $params eq '';
183              
184 28 50       134 croak "'api' must be defined" unless $params->{api};
185              
186             ## trim any resource, method or version details in api id
187 28 100       132 if ($params->{api} =~ /([^:]+):(v[^\.]+)/ixsm) {
188 3         11 $params->{api} = $1;
189 3         13 $params->{version} = $2;
190             }
191 28 100       125 if ($params->{api} =~ /^(.*?)\./xsm) {
192 10         43 $params->{api} = $1;
193             }
194              
195 28 100       95 unless ($self->service_exists($params->{api})) {
196 2         308 croak "$params->{api} does not seem to be a valid Google API";
197             }
198              
199 26   66     147 $params->{version} //= $self->latest_stable_version($params->{api});
200 26         83 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 228 my ($self, $arg) = @_;
215              
216 23         83 my $params = $self->process_api_version($arg);
217 22         54 my $apis = $self->available_APIs();
218              
219 22         64 my $api = $apis->{ $params->{api} };
220 22 50       83 croak "No versions found for $params->{api}" unless $api->{version};
221              
222 22         51 my @versions = @{ $api->{version} };
  22         106  
223 22         87 my @urls = @{ $api->{discoveryRestUrl} };
  22         90  
224 22 100   56   321 my ($url) = pairwise { $a eq $params->{version} ? $b : () } @versions, @urls;
  56         214  
225              
226 22 50       124 croak "Couldn't find correct url for $params->{api} $params->{version}"
227             unless $url;
228              
229 22         91 $self->get_with_cache($url);
230             }
231              
232             #TODO- HERE - we are here in refactoring
233             sub _extract_resource_methods_from_api_spec {
234 144     144   325 my ($self, $tree, $api_spec, $ret) = @_;
235 144 100       328 $ret = {} unless defined $ret;
236 144 50       336 croak("ret not a hash - $tree, $api_spec, $ret") unless ref($ret) eq 'HASH';
237              
238 144 100 66     539 if (defined $api_spec->{methods} && ref($api_spec->{methods}) eq 'HASH') {
239 108         207 foreach my $method (keys %{ $api_spec->{methods} }) {
  108         393  
240             $ret->{"$tree.$method"} = $api_spec->{methods}{$method}
241 494 50       2291 if ref($api_spec->{methods}{$method}) eq 'HASH';
242             }
243             }
244 144 100       356 if (defined $api_spec->{resources}) {
245 54         111 foreach my $resource (keys %{ $api_spec->{resources} }) {
  54         200  
246             ## NB - recursive traversal down tree of api_spec resources
247             $self->_extract_resource_methods_from_api_spec("$tree.$resource",
248 124         506 $api_spec->{resources}{$resource}, $ret);
249             }
250             }
251 144         344 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 428 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       46 croak 'You must ask for a method!' unless defined $tree;
269              
270 11         79 my @nodes = split /\./smx, $tree;
271 11 50       49 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         31 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       58 if ($api_id =~ /([^:]+):([^\.]+)$/ixsm) {
283 2         7 $api_id = $1;
284 2         7 $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       45 $api_version = $self->latest_stable_version($api_id) unless $api_version;
293              
294              
295             ## TODO: confirm that spec available for api version
296 11         65 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         50 my $schemas = {};
302 11         26 foreach my $schema_key (sort keys %{ $api_spec->{schemas} }) {
  11         630  
303 926         2106 $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         87 my $api_spec_fix = $self->_fix_ref($api_spec, $schemas)
312             ; ## first level ( '$ref' in the method params and return values etc )
313 11         41 $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         2324 my $all_api_methods =
318             $self->_extract_resource_methods_from_api_spec("$api_id:$api_version",
319             $api_spec);
320              
321 11 100       65 unless (defined $all_api_methods->{$tree}) {
322 9         30 $all_api_methods =
323             $self->_extract_resource_methods_from_api_spec($api_id, $api_spec);
324             }
325 11 100       56 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         64 %{ $all_api_methods->{$tree}{parameters} },
332 10         32 %{ $api_spec->{parameters} }
  10         204  
333             };
334 10         21854 return $all_api_methods->{$tree};
335             }
336              
337             croak(
338 1         259 "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 127726     127726   235163 my ($self, $node, $schemas) = @_;
365 127726         185912 my $ret = undef;
366 127726         199629 my $r = ref($node);
367              
368              
369 127726 100       252950 if ($r eq 'ARRAY') {
    100          
370 5209         9086 $ret = [];
371 5209         9919 foreach my $el (@$node) {
372 31317         57206 push @$ret, $self->_fix_ref($el, $schemas);
373             }
374             } elsif ($r eq 'HASH') {
375 31827         52420 $ret = {};
376 31827         97049 foreach my $key (keys %$node) {
377 101289 100       187296 if ($key eq '$ref') {
378              
379             #say $node->{'$ref'};
380 4902         13663 $ret = $schemas->{ $node->{'$ref'} };
381             } else {
382 96387         200076 $ret->{$key} = $self->_fix_ref($node->{$key}, $schemas);
383             }
384             }
385             } else {
386 90690         140537 $ret = $node;
387             }
388              
389 127726         336536 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 16714 my ($self) = @_;
434 6         8 my @api_list = keys %{ $self->available_APIs };
  6         15  
435 6 100       328 return wantarray ? @api_list : join(',', @api_list);
436             }
437              
438              
439             1;
440              
441             __END__