File Coverage

lib/Google/RestApi/SheetsApi4/Spreadsheet.pm
Criterion Covered Total %
statement 175 184 95.1
branch 31 44 70.4
condition 15 20 75.0
subroutine 33 37 89.1
pod 21 25 84.0
total 275 310 88.7


line stmt bran cond sub pod time code
1              
2             our $VERSION = '1.0.2';
3              
4             use Google::RestApi::Setup;
5 1     1   527  
  1         3  
  1         7  
6             use Cache::Memory::Simple ();
7 1     1   19162  
  1         633  
  1         23  
8             use aliased 'Google::RestApi::SheetsApi4';
9 1     1   6 use aliased 'Google::RestApi::SheetsApi4::Worksheet';
  1         2  
  1         5  
10 1     1   161 use aliased 'Google::RestApi::SheetsApi4::RangeGroup';
  1         4  
  1         3  
11 1     1   105 use aliased 'Google::RestApi::SheetsApi4::RangeGroup::Tie';
  1         2  
  1         3  
12 1     1   148  
  1         2  
  1         3  
13             use parent "Google::RestApi::SheetsApi4::Request::Spreadsheet";
14 1     1   158  
  1         2  
  1         5  
15             my $class = shift;
16              
17 151     151 1 9181 my $qr_id = SheetsApi4->Spreadsheet_Id;
18             my $qr_uri = SheetsApi4->Spreadsheet_Uri;
19 151         749 # pass one of id/name/title/uri and this will work out the others.
20 151         572 state $check = compile_named(
21             sheets_api => HasApi,
22 151         309 # https://developers.google.com/sheets/api/guides/concepts
23             id => StrMatch[qr/^$qr_id$/], { optional => 1 },
24             name => Str, { optional => 1 },
25             title => Str, { optional => 1 },
26             uri => StrMatch[qr|^$qr_uri/$qr_id/?|], { optional => 1 },
27             cache_seconds => PositiveOrZeroNum, { default => 5 },
28             );
29             my $self = $check->(@_);
30              
31 151         4453 $self = bless $self, $class;
32             $self->{name} ||= $self->{title};
33 151         9712 delete $self->{title};
34 151   66     1603  
35 151         567 $self->{id} || $self->{name} || $self->{uri} or LOGDIE "At least one of id, name, or uri must be specified";
36              
37 151 100 100     990 return $self;
      100        
38             }
39 150         1125  
40             # take the passed uri from worksheet/range/rangegroup etc, and tack on the spreadsheet id,
41             # then pass it up to G::R::SheetsApi4 which will tack on the endpoint.
42             my $self = shift;
43             state $check = compile_named(
44             uri => Str, { default => '' },
45 216     216 1 408 _extra_ => slurpy Any, # we'll just pass the params/content etc up for processing.
46 216         324 );
47             my $p = named_extra($check->(@_));
48             $p->{uri} = $self->spreadsheet_id() . $p->{uri};
49             return $self->sheets_api()->api(%$p);
50 216         1589 }
51 216         1085  
52 216         899 # work out the id from the uri or the name/title.
53             my $self = shift;
54              
55             if (!$self->{id}) {
56             if ($self->{uri}) {
57 225     225 1 10260 my $qr_id = SheetsApi4->Spreadsheet_Id;
58             my $qr_uri = SheetsApi4->Spreadsheet_Uri;
59 225 100       891 ($self->{id}) = $self->{uri} =~ m|^$qr_uri/($qr_id)|; # can end with '/edit'
60 6 100       28 LOGDIE "Unable to extract a sheet id from uri" if !$self->{id};
61 2         9 DEBUG("Got sheet ID '$self->{id}' via URI '$self->{uri}'.");
62 2         7 } else {
63 2         52 my @spreadsheets = grep { $_->{name} eq $self->{name}; } $self->sheets_api()->spreadsheets();
64 2 50       10 LOGDIE "Sheet '$self->{name}' not found on Google Drive" if !@spreadsheets;
65 2         16 LOGDIE "More than one spreadsheet found with name '$self->{name}'. Specify 'id' or 'uri' instead."
66             if scalar @spreadsheets > 1;
67 4         19 $self->{id} = $spreadsheets[0]->{id};
  12         46  
68 4 100       23 DEBUG("Got sheet id '$self->{id}' via spreadsheet list.");
69 3 100       16 }
70             }
71 2         7  
72 2         13 return $self->{id};
73             }
74              
75             # when 'api' is eventually called, id will be worked out if we don't already have it.
76 223         1135 # the resolving of id/name/title/uri is deferred until the first api call.
77             my $self = shift;
78             $self->{name} ||= $self->properties('title')->{title}
79             or LOGDIE "No properties title present in properties";
80             return $self->{name};
81             }
82 5     5 1 10407  
83             # see above routine.
84 5 50 66     32 my $self = shift;
85 5         376 $self->{uri} ||= $self->attrs('spreadsheetUrl')->{spreadsheetUrl}
86             or LOGDIE "No spreadsheet URI found from get results";
87 1     1 1 280 $self->{uri} =~ s[/(edit|copy)$][]; # this isn't necessary but keeps things cleaner.
88             return $self->{uri};
89             }
90              
91 3     3 1 9102 # return one of the attributes of the spreadsheet.
92             my $self = shift;
93 3 50 66     27 my $fields = shift;
94 3         79 return $self->_cache($fields, sub {
95 3         33 $self->api(params => { fields => $fields })
96             });
97             }
98              
99             # return one of the property attributes of the spreadsheet.
100 137     137 1 1190 my $self = shift;
101 137         278 state $check = compile(Str);
102             my ($what) = $check->(@_);
103 134     134   2958 my $fields = _fields('properties', $what);
104 137         960 return $self->attrs($fields)->{properties};
105             }
106              
107             # GET https://sheets.googleapis.com/v4/spreadsheets/spreadsheetId?&fields=sheets.properties
108             # returns properties for each worksheet in the spreadsheet.
109 13     13 1 2805 my $self = shift;
110 13         39 state $check = compile(Str);
111 13         919 my ($what) = $check->(@_);
112 13         178 my $fields = _fields('sheets.properties', $what);
113 13         51 my $properties = $self->attrs($fields)->{sheets};
114             my @properties = map { $_->{properties}; } @$properties;
115             return \@properties;
116             }
117              
118             my ($fields, $what) = @_;
119 119     119 1 306 if ($what =~ /^\(/) {
120 119         294 $fields .= $what;
121 119         1112 } else {
122 119         1632 $fields .= ".$what";
123 119         584 }
124 119         3949 return $fields;
  119         699  
125 119         767 }
126              
127             # if multiple attributes are called for, it can hammer the api unnecessarily, so cache the results
128             # for a few seconds (default 5).
129 132     132   404 my $self = shift;
130 132 100       748  
131 117         301 state $check = compile(Str, CodeRef);
132             my ($key, $code) = $check->(@_);
133 15         52 return $code->() if !$self->{cache_seconds};
134              
135 132         372 $self->{_cache} ||= Cache::Memory::Simple->new();
136             # will run the code and store the result for x seconds.
137             return $self->{_cache}->get_or_set(
138             $key, $code, $self->{cache_seconds}
139             );
140             }
141 137     137   307  
142             my $self = shift;
143 137         318 delete $self->{_cache};
144 137         1384 return;
145 137 100       2435 }
146              
147 136   66     1617 # sets the number of seconds that attrs will be cached. this can be adjusted
148             # to suit your use-case. if there are lots of people updating things, then a
149             # lower cache value would suit. if you know you are the only one using it, then
150             # a higher cache value will prevent unnecessary calls to the api.
151 136         1388 my $self = shift;
152              
153             state $check = compile(PositiveOrZeroNum);
154             my ($cache_seconds) = $check->(@_);
155 8     8   25  
156 8         35 $self->{_cache}->delete_all() if $self->{_cache};
157 8         18  
158             if (!$cache_seconds) {
159             $self->_cache_delete();
160             delete $self->{cache_seconds};
161             } else {
162             $self->{cache_seconds} = $cache_seconds;
163             }
164              
165 2     2 1 64 return;
166             }
167 2         9  
168 2         748 my $self = shift;
169             return $self->sheets_api()->copy_spreadsheet(
170 2 100       31 spreadsheet_id => $self->spreadsheet_id(), @_,
171             );
172 2 100       14 }
173 1         4  
174 1         2 my $self = shift;
175             return $self->sheets_api()->delete_spreadsheet($self->spreadsheet_id());
176 1         3 }
177              
178             my $self = shift;
179 2         8 state $check = compile(slurpy ArrayRef[HasRange]);
180             my ($ranges) = $check->(@_);
181             return RangeGroup->new(
182             spreadsheet => $self,
183 3     3 1 20 ranges => $ranges,
184 3         16 );
185             }
186              
187             # ties a key => range set of pairs to this spreadsheet.
188             my $self = shift;
189             my %ranges = @_;
190 1     1 1 270 tie my %tie, Tie, $self;
191 1         5 tied(%tie)->add_ranges(%ranges);
192             return \%tie;
193             }
194              
195 24     24 1 68 # this is done simply to allow open_worksheet to return the same worksheet instance
196 24         50 # each time it's called for the same remote worksheet. this is to avoid working on
197 24         3051 # multiple local copies of the same remote worksheet.
198 24         705 # TODO: if worksheet is renamed, registration should be updated too.
199             my $self = shift;
200             state $check = compile(HasApi);
201             my ($worksheet) = $check->(@_);
202             my $name = $worksheet->worksheet_name();
203             return $self->{registered_worksheet}->{$name} if $self->{registered_worksheet}->{$name};
204             $self->{registered_worksheet}->{$name} = $worksheet;
205             return $worksheet;
206 16     16 1 28 }
207 16         41  
208 16         137 # sends batch values to the api.
209 16         96 my $self = shift;
210 16         55  
211             state $check = compile_named(
212             ranges => ArrayRef[HasMethods[qw(has_values batch_values values_response_from_api)]],
213             content => HashRef, { default => {} },
214             );
215             my $p = $check->(@_);
216              
217             # find out which ranges have something to send.
218 117     117   267 my @ranges = grep { $_->has_values(); } @{ delete $p->{ranges} };
219 117         389 my @values = map { $_->batch_values(); } @ranges;
220 117         1383 return if !@values;
221 117         1621  
222 117 50       957 $p->{content}->{data} = \@values;
223 117         618 $p->{content}->{valueInputOption} //= 'USER_ENTERED';
224 117         1197 $p->{method} = 'post';
225             $p->{uri} = "/values:batchUpdate";
226             my $api = $self->api(%$p); # this is where the rubber hits the road.
227              
228             # each range that had values should strip off the response from the api's
229 10     10 1 26 # responses array. if everything is in sync, there should be no responses left.
230             my $responses = delete $api->{responses};
231 10         40 $_->values_response_from_api($responses) foreach @ranges;
232             LOGDIE "Returned batch values update responses were not consumed" if @$responses;
233              
234             # return whatever's left over.
235 10         1823 return $api;
236             }
237              
238 10         400 # sends batch requests (formatting etc) to the api.
  10         39  
  10         59  
239 10         28 my $self = shift;
  10         46  
240 10 50       37  
241             state $check = compile_named(
242 10         45 ranges => ArrayRef[HasMethods[qw(batch_requests requests_response_from_api)]], { default => [] }, # might just be self.
243 10   50     72 content => HashRef, { default => {} },
244 10         29 );
245 10         31 my $p = $check->(@_);
246 10         50  
247             my @all_requests = (@{ delete $p->{ranges} }, $self); # add myself to the list.
248              
249             # for each object that has requests to submit, store them so that
250 10         65 # they can process the responses that come back.
251 10         81 my @ranges = map {
252 10 50       37 $_->batch_requests() ? $_ : ();
253             } @all_requests;
254             return if !@ranges;
255 10         48  
256             # pull out the requests hashes to be sent to the rest api.
257             my @batch_requests = map {
258             $_->batch_requests();
259             } @all_requests;
260 7     7 1 12 return if !@batch_requests;
261              
262 7         18 # we're about to do a bunch of requests that could affect what's in the cache.
263             # TODO: selectively invalidate the cache based on what's being submitted.
264             $self->_cache_delete();
265              
266 7         1954 # call the batch request api.
267             $p->{content}->{requests} = \@batch_requests;
268 7         236 $p->{method} = 'post';
  7         29  
269             $p->{uri} = ':batchUpdate';
270             my $api = $self->api(%$p);
271              
272             # grab the json decoded replies from the response.
273 7 100       20 my $responses = delete $api->{replies};
  14         44  
274             # present the responses back to those who are waiting, each will strip off the ones they requested.
275 7 50       54 $_->requests_response_from_api($responses) foreach @ranges;
276             # if there are any left over, it sux. we are out of sync somewhere. all requestors should
277             # process their corresponding response.
278             LOGDIE "Returned batch request responses were not consumed" if @$responses;
279 7         17  
  14         32  
280             return $api;
281 7 50       17 }
282              
283             my $self = shift;
284              
285 7         25 state $check = compile(RangeNamed, { optional => 1 });
286             my ($named_range_name) = $check->(@_);
287              
288 7         22 my $named_ranges = $self->attrs('namedRanges')->{namedRanges};
289 7         16 return $named_ranges if !$named_range_name;
290 7         16  
291 7         25 my ($named_range) = grep { $_->{name} eq $named_range_name; } @$named_ranges;
292             return $named_range;
293             }
294 7         26  
295             # takes a named range and works out the actual range it represents.
296 7         38 my $self = shift;
297              
298             state $check = compile(RangeNamed);
299 7 50       17 my ($named_range_name) = $check->(@_);
300              
301 7         41 my $named_range = $self->named_ranges($named_range_name) or return;
302             $named_range = $named_range->{range};
303             my $range = [
304             [ $named_range->{startColumnIndex} + 1, $named_range->{startRowIndex} + 1 ],
305 1     1 1 3 [ $named_range->{endColumnIndex}, $named_range->{endRowIndex} ],
306             ];
307 1         4  
308 1         735 return ($named_range->{sheetId}, $range);
309             }
310 1         21  
311 1 50       21  
312             # each worksheet has an entry:
313 1         4 # ---
  1         5  
314 1         5 # - protectedRanges:
315             # - editors:
316             # users:
317             # - xxx@gmail.com
318             # - yyy@gmail.com
319 1     1 0 3 # protectedRangeId: 1161285259
320             # range: {}
321 1         4 # requestingUserCanEdit: !!perl/scalar:JSON::PP::Boolean 1
322 1         687 # warningOnly: !!perl/scalar:JSON::PP::Boolean 1
323             # - {}
324 1 50       21 # - {}
325 1         3 # submit_requests needs to be called by the caller after this.
326             my $self = shift;
327             foreach my $worksheet (@{ $self->protected_ranges() }) {
328 1         8 my $ranges = $worksheet->{protectedRanges} or next;
329             $self->delete_protected_range($_->{protectedRangeId}) foreach (@$ranges);
330             }
331 1         6 return $self;
332             }
333              
334 0     0 1 0  
335             1;
336              
337              
338             =head1 NAME
339              
340             Google::RestApi::SheetsApi4::Spreadsheet - Represents a Google Spreadsheet.
341              
342             =head1 DESCRIPTION
343              
344             See the description and synopsis at L<Google::RestApi::SheetsApi4>.
345              
346             =head1 NAVIGATION
347              
348             =over
349              
350             =item * L<Google::RestApi::SheetsApi4>
351 0     0 1 0  
352 0         0 =item * L<Google::RestApi::SheetsApi4::Spreadsheet>
  0         0  
353 0 0       0  
354 0         0 =item * L<Google::RestApi::SheetsApi4::Worksheet>
355              
356 0         0 =item * L<Google::RestApi::SheetsApi4::Range>
357              
358             =item * L<Google::RestApi::SheetsApi4::Range::All>
359 0     0 1 0  
360 238     238 1 1366 =item * L<Google::RestApi::SheetsApi4::Range::Col>
361 8     8 0 422  
362 0     0 0 0 =item * L<Google::RestApi::SheetsApi4::Range::Row>
363 6     6 0 43  
364             =item * L<Google::RestApi::SheetsApi4::Range::Cell>
365              
366             =item * L<Google::RestApi::SheetsApi4::RangeGroup>
367              
368             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Iterator>
369              
370             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Tie>
371              
372             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Tie::Iterator>
373              
374             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet>
375              
376             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet::Worksheet>
377              
378             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet::Worksheet::Range>
379              
380             =back
381              
382             =head1 SUBROUTINES
383              
384             =over
385              
386             =item new(%args);
387              
388             Creates a new instance of a Spreadsheet object.
389              
390             %args consists of:
391              
392             =over
393              
394             =item * C<sheets_api> <SheetsApi4>: The parent SheetsApi4 object.
395              
396             =item * C<id> <string>: The id of the spreadsheet (Google Drive file ID).
397              
398             =item * C<name> <string>: The name of the spreadsheet (as shown in Google Drive).
399              
400             =item * C<title> <string>: An alias for name.
401              
402             =item * C<uri> <string>: The spreadsheet ID extracted from the overall URI.
403              
404             =item * C<cache_seconds> <int>: Cache information for this many seconds (default to 5, 0 disables).
405              
406             =back
407              
408             You would not normally call this directly, you would obtain it from the SheetsApi4::open_spreadsheet routine.
409              
410             Only one of id/name/title/uri should be specified and this API will derive the others as necessary.
411              
412             The cache exists so that repeated calls for the same attributes or worksheet properties doesn't keep hammering the Google API over and over.
413             The default is 5 seconds. See 'cache_seconds' below.
414              
415             =item api(%args);
416              
417             Calls the parent SheetsApi4's 'api' routine adding the sheet file ID, and passing through any args such as content, params, headers, etc.
418              
419             You would not normally call this directly unless you were making a Google API call not currently supported by this API framework.
420              
421             Returns the response hash from Google API.
422              
423             =item spreadsheet_id();
424              
425             Returns the spreadsheet id (the Google Drive file id).
426              
427             =item spreadsheet_uri();
428              
429             Returns the URI of this spreadsheet.
430              
431             =item spreadsheet_name();
432              
433             Returns the name of the spreadsheet.
434              
435             =item spreadsheet_title();
436              
437             An alias for 'spreadsheet_name'.
438              
439             =item attrs(fields<string>);
440              
441             Returns the spreadsheet attributes of the specified fields.
442              
443             =item properties(properties<string>);
444              
445             Returns the spreadsheet property attributes of the specified fields.
446              
447             =item worksheet_properties(what<string>);
448              
449             Returns an array ref of the properties of the worksheets owned by this spreadsheet.
450              
451             =item cache_seconds(seconds<int>)
452              
453             Sets the caching time in seconds. Calling will always delete the existing cache. 0 disables the cache (not recommended).
454              
455             =item delete_all_protected_ranges();
456              
457             Deletes all the protected ranges from all the worksheets owned by this spreadsheet.
458              
459             =item named_ranges(name<string>);
460              
461             Returns the properties of the named range passed, or if false is passed, all the named ranges for this spreadsheet.
462              
463             =item copy_spreadsheet(%args);
464              
465             Creates a copy of this spreadsheet and passes any args to the Google Drive file copy routine.
466              
467             =item delete_spreadsheet();
468              
469             Deletes this spreadsheet from Google Drive.
470              
471             =item range_group(range<array>...);
472              
473             Creates a range group with the contained ranges. See the RangeGroup object.
474              
475             =item tie(%args);
476              
477             %args is a hash of key => Range object pairs.
478              
479             Ties the given 'key => range' pairs into a tied range group. The range group can be used to send batch values (API batchUpdate) and
480             batch requests (API batchRequests) as a single call once all the changes have been made to the overall hash.
481              
482             Turning on the 'fetch_range' property will return the underlying ranges on fetch so that formatting for the ranges can be set. You
483             would normally only turn this on for a short time, and turn it off when the underlying batch requests have been submitted.
484              
485             $tied = $ss->tie(id => $range_cell);
486             tied(%$tied)->fetch_range(1);
487             $tied->{id}->bold()->red()->background_blue();
488             tied(%$tied)->fetch_range(0)->submit_requests();
489              
490             See also Worksheet::tie.
491              
492             =item submit_values(%args);
493              
494             %args consists of:
495              
496             =over
497              
498             =item * C<ranges> <arrayref>: The ranges that have requests to be sent in one batchUpdate Google API call.
499              
500             =item * C<content> <hashref>: Any additional content to be sent to the Google API for this batch submit, for any customized content that needs to be passed.
501              
502             =back
503              
504             Submits the batch values (Google API's batchUpdate) for the specified ranges. Content is passed to the SheetsApi4's 'api' call for any customized
505             content you may need to pass.
506              
507             =item submit_requests(%args);
508              
509             %args consists of:
510              
511             =over
512              
513             =item * C<ranges> <arrayref>: The ranges that have requests to be sent in one batchRequest Google API call.
514              
515             =item * C<content> <hashref>: Any additional content to be sent to the Google API for this batch submit, for any customized content that needs to be passed.
516              
517             =back
518              
519             Submits the batch requests (Google API's batchRequest) for the specified ranges. Content is passed to the SheetsApi4's 'api' call for any customized
520             content you may need to pass.
521              
522             =item protected_ranges();
523              
524             Returns all the protected ranges for this spreadsheet.
525              
526             =item open_worksheet(%args);
527              
528             Creates a new Worksheet object, passing the args to that object's 'new' routine (which see).
529              
530             =item sheets_api();
531              
532             Returns the SheetsApi4 object.
533              
534             =back
535              
536             =head1 AUTHORS
537              
538             =over
539              
540             =item
541              
542             Robin Murray mvsjes@cpan.org
543              
544             =back
545              
546             =head1 COPYRIGHT
547              
548             Copyright (c) 2021, Robin Murray. All rights reserved.
549              
550             This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.