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   31 use strictures;
  3         21  
  3         43  
2              
3             package WebService::GoogleAPI::Client::Discovery;
4              
5             our $VERSION = '0.27'; # VERSION
6              
7             # ABSTRACT: Google API discovery service
8              
9              
10 3     3   775 use Moo;
  3         9  
  3         27  
11 3     3   1086 use Carp;
  3         27  
  3         195  
12 3     3   26 use WebService::GoogleAPI::Client::UserAgent;
  3         10  
  3         21  
13 3     3   104 use List::Util qw/uniq reduce/;
  3         7  
  3         307  
14 3     3   1694 use List::SomeUtils qw/pairwise/;
  3         29084  
  3         267  
15 3     3   27 use Data::Dump qw/pp/;
  3         8  
  3         148  
16 3     3   1309 use CHI;
  3         75239  
  3         8541  
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 114 my ($self, $key, $force, $authorized) = @_;
34              
35 32   100     906 my $expiration = $self->chi->get_expires_at($key) // 0;
36              
37 32         21495 my $will_expire = $expiration - time();
38 32 100 100     225 if ($will_expire > 0 && not $force) {
39 24 50       166 carp "discovery_data cached data expires in $will_expire seconds"
40             if $self->debug > 2;
41 24         631 my $ret = $self->chi->get($key);
42 24 50       43213 croak 'was expecting a HASHREF!' unless ref $ret eq 'HASH';
43 24         149 $self->stats->{cache}{get}++;
44 24         177 return $ret;
45             } else {
46 8         20 my $ret;
47 8 100       29 if ($authorized) {
48 1         10 $ret = $self->ua->validated_api_query($key);
49 1         18 $self->stats->{network}{authorized}++;
50             } else {
51 7         127 $ret = $self->ua->get($key)->res;
52             }
53 8 50       1735968 if ($ret->is_success) {
54 8         193 my $all = $ret->json;
55 8         1324049 $self->stats->{network}{get}++;
56 8         487 $self->chi->set($key, $all, '30d');
57 8         37297 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 220119 sub discover_key {'https://www.googleapis.com/discovery/v1/apis'}
70              
71             sub discover_all {
72 10     10 1 450200 my $self = shift;
73 10         58 $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   3 $available = undef;
85             }
86              
87             sub available_APIs {
88              
89             #cache this crunch
90 100 100   100 1 420352 return $available if $available;
91              
92 3         9 my ($self) = @_;
93 3         15 my $d_all = $self->discover_all;
94 3 50       22 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         18 my @keys = qw/name version documentationLink discoveryRestUrl/;
99 3         8 my @relevant;
100 3         8 for my $i (@{ $d_all->{items} }) {
  3         15  
101 1228 100       2102 next unless @keys == grep { exists $i->{$_} } @keys;
  4912         9432  
102 1225         1852 push @relevant, { %{$i}{@keys} };
  1225         4087  
103             }
104              
105             my $reduced = reduce {
106 1225     1225   1921 for my $key (qw/version documentationLink discoveryRestUrl/) {
107 3675   100     14647 $a->{ $b->{name} }->{$key} //= [];
108 3675         5306 push @{ $a->{ $b->{name} }->{$key} }, $b->{$key};
  3675         9438  
109             }
110 1225         1879 $a;
111 3         77 } {}, @relevant;
112              
113             #store it away globally
114 3         1264 $available = $reduced;
115             }
116              
117              
118             sub augment_discover_all_with_unlisted_experimental_api {
119 0     0 0 0 my ($self, $api_spec) = @_;
120 0         0 carp <
121             This lengthy function name (augment_discover_all_with_unlisted_experimental_api)
122             will be removed soon. Please use 'augment_with' instead.
123             DEPRECATION
124 0         0 $self->augment_with($api_spec);
125             }
126              
127             sub augment_with {
128 1     1 1 230629 my ($self, $api_spec) = @_;
129              
130 1         6 my $all = $self->discover_all();
131              
132             ## fail if any of the expected fields are not provided
133 1         7 for my $field (
134             qw/version title description id kind documentationLink
135             discoveryRestUrl name/
136             ) {
137 8 50       23 if (not defined $api_spec->{$field}) {
138 0         0 carp("required $field in provided api spec missing");
139             }
140             }
141              
142 1         6 push @{ $all->{items} }, $api_spec;
  1         4  
143 1         30 $self->chi->set($self->discover_key, $all, '30d');
144 1         2535 $self->_invalidate_available;
145 1         421 return $all;
146             }
147              
148              
149              
150             sub service_exists {
151 43     43 1 5212 my ($self, $api) = @_;
152 43 50       134 return unless $api;
153 43         140 return $self->available_APIs->{$api};
154             }
155              
156              
157             sub available_versions {
158 28     28 1 3154 my ($self, $api) = @_;
159 28 50       80 return [] unless $api;
160 28   100     75 return $self->available_APIs->{$api}->{version} // [];
161             }
162              
163              
164             sub latest_stable_version {
165 23     23 1 2014 my ($self, $api) = @_;
166 23 50       87 return '' unless $api;
167 23         78 my $versions = $self->available_versions($api);
168 23 50       88 return '' unless $versions;
169 23 50       47 return '' unless @{$versions} > 0;
  23         75  
170              
171             #remove alpha or beta versions
172 23         64 my @stable = grep { !/beta|alpha/ } @$versions;
  59         370  
173 23   50     139 return $stable[-1] || '';
174             }
175              
176              
177             sub process_api_version {
178 28     28 1 14659 my ($self, $params) = @_;
179              
180             # scalar parameter not hashref - so assume is intended to be $params->{api}
181 28 100       139 $params = { api => $params } if ref $params eq '';
182              
183 28 50       114 croak "'api' must be defined" unless $params->{api};
184              
185             ## trim any resource, method or version details in api id
186 28 100       145 if ($params->{api} =~ /([^:]+):(v[^\.]+)/ixsm) {
187 3         36 $params->{api} = $1;
188 3         15 $params->{version} = $2;
189             }
190 28 100       127 if ($params->{api} =~ /^(.*?)\./xsm) {
191 10         40 $params->{api} = $1;
192             }
193              
194 28 100       94 unless ($self->service_exists($params->{api})) {
195 2         428 croak "$params->{api} does not seem to be a valid Google API";
196             }
197              
198 26   66     163 $params->{version} //= $self->latest_stable_version($params->{api});
199 26         102 return $params;
200             }
201              
202              
203              
204             sub get_api_discovery_for_api_id {
205 0     0 0 0 carp <
206             This long method name (get_api_discovery_for_api_id) is being deprecated
207             in favor of get_api_document. Please switch your code soon
208             DEPRECATION
209 0         0 shift->get_api_document(@_);
210             }
211              
212             sub get_api_document {
213 23     23 1 229 my ($self, $arg) = @_;
214              
215 23         91 my $params = $self->process_api_version($arg);
216 22         61 my $apis = $self->available_APIs();
217              
218 22         89 my $api = $apis->{ $params->{api} };
219 22 50       89 croak "No versions found for $params->{api}" unless $api->{version};
220              
221 22         50 my @versions = @{ $api->{version} };
  22         79  
222 22         51 my @urls = @{ $api->{discoveryRestUrl} };
  22         69  
223 22 100   56   291 my ($url) = pairwise { $a eq $params->{version} ? $b : () } @versions, @urls;
  56         210  
224              
225 22 50       124 croak "Couldn't find correct url for $params->{api} $params->{version}"
226             unless $url;
227              
228 22         82 $self->get_with_cache($url);
229             }
230              
231             #TODO- HERE - we are here in refactoring
232             sub _extract_resource_methods_from_api_spec {
233 144     144   326 my ($self, $tree, $api_spec, $ret) = @_;
234 144 100       304 $ret = {} unless defined $ret;
235 144 50       332 croak("ret not a hash - $tree, $api_spec, $ret") unless ref($ret) eq 'HASH';
236              
237 144 100 66     518 if (defined $api_spec->{methods} && ref($api_spec->{methods}) eq 'HASH') {
238 108         192 foreach my $method (keys %{ $api_spec->{methods} }) {
  108         408  
239             $ret->{"$tree.$method"} = $api_spec->{methods}{$method}
240 498 50       2696 if ref($api_spec->{methods}{$method}) eq 'HASH';
241             }
242             }
243 144 100       364 if (defined $api_spec->{resources}) {
244 54         83 foreach my $resource (keys %{ $api_spec->{resources} }) {
  54         218  
245             ## NB - recursive traversal down tree of api_spec resources
246 124         467 $self->_extract_resource_methods_from_api_spec("$tree.$resource", $api_spec->{resources}{$resource}, $ret);
247             }
248             }
249 144         364 return $ret;
250             }
251              
252              
253              
254             sub extract_method_discovery_detail_from_api_spec {
255 0     0 0 0 carp <
256             This rather long method name (extract_method_discovery_detail_from_api_spec)
257             is being deprecated in favor of get_method_details. Please switch soon
258             DEPRECATION
259 0         0 shift->get_method_details(@_);
260             }
261              
262             sub get_method_details {
263 11     11 1 562 my ($self, $tree, $api_version) = @_;
264             ## where tree is the method in format from _extract_resource_methods_from_api_spec() like projects.models.versions.get
265             ## the root is the api id - further '.' sep levels represent resources until the tailing label that represents the method
266 11 50       44 croak 'You must ask for a method!' unless defined $tree;
267              
268 11         92 my @nodes = split /\./smx, $tree;
269 11 50       60 croak(
270             "tree structure '$tree' must contain at least 2 nodes including api id, [list of hierarchical resources ] and method - not "
271             . scalar(@nodes))
272             unless @nodes > 1;
273              
274 11         36 my $api_id = shift(@nodes); ## api was head
275 11         29 my $method = pop(@nodes); ## method was tail
276              
277             ## split out version if is defined as part of $tree
278             ## trim any resource, method or version details in api id
279             ## we have already isolated head from api tree children
280 11 100       60 if ($api_id =~ /([^:]+):([^\.]+)$/ixsm) {
281 2         7 $api_id = $1;
282 2         7 $api_version = $2;
283             }
284              
285             ## handle incorrect api_id
286 11 50       61 if (!$self->service_exists($api_id)) {
287 0         0 croak("unable to confirm that '$api_id' is a valid Google API service id");
288             }
289              
290 11 100       66 $api_version = $self->latest_stable_version($api_id) unless $api_version;
291              
292              
293             ## TODO: confirm that spec available for api version
294 11         71 my $api_spec = $self->get_api_document({ api => $api_id, version => $api_version });
295              
296              
297             ## we use the schemas to substitute into '$ref' keyed placeholders
298 11         38 my $schemas = {};
299 11         33 foreach my $schema_key (sort keys %{ $api_spec->{schemas} }) {
  11         716  
300 935         2064 $schemas->{$schema_key} = $api_spec->{'schemas'}{$schema_key};
301             }
302              
303             ## recursive walk through the structure in _fix_ref
304             ## substitute the schema keys into the total spec to include
305             ## '$ref' values within the schema structures themselves
306             ## including within the schema spec structures (NB assumes no cyclic structures )
307             ## otherwise would could recursive chaos
308 11         106 my $api_spec_fix
309             = $self->_fix_ref($api_spec, $schemas); ## first level ( '$ref' in the method params and return values etc )
310 11         51 $api_spec = $self->_fix_ref($api_spec_fix, $schemas)
311             ; ## second level ( '$ref' in the interpolated schemas from first level )
312              
313             ## now extract all the methods (recursive )
314 11         1988 my $all_api_methods = $self->_extract_resource_methods_from_api_spec("$api_id:$api_version", $api_spec);
315              
316 11 100       71 unless (defined $all_api_methods->{$tree}) {
317 9         30 $all_api_methods = $self->_extract_resource_methods_from_api_spec($api_id, $api_spec);
318             }
319 11 100       72 if ($all_api_methods->{$tree}) {
320              
321             #add in the global parameters to the endpoint,
322             #stored in the top level of the api_spec
323             # TODO - why are we mutating the main hash?
324             $all_api_methods->{$tree}{parameters}
325 10         35 = { %{ $all_api_methods->{$tree}{parameters} }, %{ $api_spec->{parameters} } };
  10         49  
  10         148  
326 10         19160 return $all_api_methods->{$tree};
327             }
328              
329 1         288 croak("Unable to find method detail for '$tree' within Google Discovery Spec for $api_id version $api_version");
330             }
331             ########################################################
332              
333             ########################################################
334             ########################################################
335              
336             #=head2 C
337             #
338             #This sub walks through the structure and replaces any hashes keyed with '$ref' with
339             #the value defined in $schemas->{ }
340             #
341             #eg
342             # ->{'response'}{'$ref'}{'Buckets'}
343             # is replaced with
344             # ->{response}{ $schemas->{Buckets} }
345             #
346             # It assumes that the schemas have been extracted from the original discover for the API
347             # and is typically applied to the method ( api endpoint ) to provide a fully descriptive
348             # structure without external references.
349             #
350             #=cut
351              
352             ########################################################
353             sub _fix_ref {
354 130470     130470   235409 my ($self, $node, $schemas) = @_;
355 130470         184419 my $ret = undef;
356 130470         200458 my $r = ref($node);
357              
358              
359 130470 100       248833 if ($r eq 'ARRAY') {
    100          
360 5352         9377 $ret = [];
361 5352         10266 foreach my $el (@$node) {
362 32184         56393 push @$ret, $self->_fix_ref($el, $schemas);
363             }
364             } elsif ($r eq 'HASH') {
365 32449         54442 $ret = {};
366 32449         101207 foreach my $key (keys %$node) {
367 103217 100       177115 if ($key eq '$ref') {
368              
369             #say $node->{'$ref'};
370 4953         13753 $ret = $schemas->{ $node->{'$ref'} };
371             } else {
372 98264         199455 $ret->{$key} = $self->_fix_ref($node->{$key}, $schemas);
373             }
374             }
375             } else {
376 92669         137834 $ret = $node;
377             }
378              
379 130470         337525 return $ret;
380             }
381             ########################################################
382              
383              
384              
385             #TODO: consider ? refactor to allow parameters either as a single api id such as 'gmail'
386             # as well as the currently accepted hash keyed on the api and version
387             #
388             #SEE ALSO:
389             # The following methods are delegated through to Client::Discovery - see perldoc WebService::Client::Discovery for detils
390             #
391             # get_method_meta
392             # discover_all
393             # extract_method_discovery_detail_from_api_spec
394             # get_api_discovery_for_api_id
395              
396             ########################################################
397             #TODO- give short name and deprecate
398             sub methods_available_for_google_api_id {
399 0     0 1 0 my ($self, $api_id, $version) = @_;
400              
401 0 0       0 $version = $self->latest_stable_version($api_id) unless $version;
402             ## TODO: confirm that spec available for api version
403 0         0 my $api_spec = $self->get_api_discovery_for_api_id({ api => $api_id, version => $version });
404 0         0 my $methods = $self->_extract_resource_methods_from_api_spec($api_id, $api_spec);
405 0         0 return $methods;
406             }
407             ########################################################
408              
409              
410              
411             sub list_of_available_google_api_ids {
412 0     0 0 0 carp <
413             This rather long function name (list_of_available_google_api_ids)
414             is being deprecated in favor of the shorter list_api_ids. Please
415             update your code accordingly.
416             DEPRECATION
417 0         0 shift->list_api_ids;
418             }
419             ## returns a list of all available API Services
420             sub list_api_ids {
421 6     6 1 28904 my ($self) = @_;
422 6         16 my @api_list = keys %{ $self->available_APIs };
  6         25  
423 6 100       556 return wantarray ? @api_list : join(',', @api_list);
424             }
425              
426              
427             1;
428              
429             __END__