File Coverage

lib/Google/RestApi/SheetsApi4.pm
Criterion Covered Total %
statement 81 82 98.7
branch 7 8 87.5
condition 5 6 83.3
subroutine 19 20 95.0
pod 9 12 75.0
total 121 128 94.5


line stmt bran cond sub pod time code
1              
2             our $VERSION = '1.0.2';
3              
4             use Google::RestApi::Setup;
5 1     1   82476  
  1         2  
  1         7  
6             use Module::Load qw( load );
7 1     1   18740 use Try::Tiny ();
  1         3  
  1         6  
8 1     1   72 use YAML::Any ();
  1         22  
  1         14  
9 1     1   4  
  1         2  
  1         16  
10             use aliased 'Google::RestApi::DriveApi3';
11 1     1   5 use aliased 'Google::RestApi::SheetsApi4::Spreadsheet';
  1         3  
  1         6  
12 1     1   75  
  1         4  
  1         4  
13             # TODO: switch to ReadOnly
14             use constant {
15             Sheets_Endpoint => "https://sheets.googleapis.com/v4/spreadsheets",
16 1         1076 Spreadsheet_Filter => "mimeType='application/vnd.google-apps.spreadsheet'",
17             Spreadsheet_Id => DriveApi3->Drive_File_Id,
18             Spreadsheet_Uri => "https://docs.google.com/spreadsheets/d",
19             Worksheet_Id => "[0-9]+",
20             Worksheet_Uri => "[#&]gid=([0-9]+)",
21             };
22 1     1   121  
  1         3  
23             my $class = shift;
24              
25 150     150 1 19722 state $check = compile_named(
26             api => HasApi, # the G::RestApi object that will be used to send http calls.
27 150         482 drive => HasMethods[qw(filter_files)], { optional => 1 }, # a drive instnace, could be your own, defaults to G::R::DriveApi3.
28             endpoint => Str, { default => Sheets_Endpoint }, # this gets tacked on to the api uri to reach the sheets endpoint.
29             );
30             my $self = $check->(@_);
31              
32 150         2503 return bless $self, $class;
33             }
34 150         6356  
35             # this gets called by lower-level classes like worksheet and range objects. they
36             # will have passed thier own uri with params and possible body, we tack on the
37             # sheets endpoint and pass it up the line to G::RestApi to make the actual call.
38             my $self = shift;
39             state $check = compile_named(
40             uri => Str, { default => '' },
41 220     220 1 9180 _extra_ => slurpy Any, # just pass through any extra params to G::RestApi::api call.
42 220         348 );
43             my $p = named_extra($check->(@_));
44             my $uri = $self->{endpoint}; # tack on the uri endpoint and pass the buck.
45             $uri .= "/$p->{uri}" if $p->{uri};
46 220         2071 return $self->rest_api()->api(%$p, uri => $uri);
47 220         855 }
48 220 100       1116  
49 220         856 my $self = shift;
50              
51             state $check = compile_named(
52             title => Str, { optional => 1 },
53 3     3 1 314 name => Str, { optional => 1 },
54             _extra_ => slurpy Any,
55 3         10 );
56             my $p = named_extra($check->(@_));
57             # we allow name and title to be synonymous for convenience. it's actuall title in the google api.
58             $p->{title} || $p->{name} or LOGDIE "Either 'title' or 'name' should be supplied";
59             $p->{title} ||= $p->{name};
60 3         1455 delete $p->{name};
61              
62 3 100 100     35 my $result = $self->api(
63 2   66     14 method => 'post',
64 2         7 content => { properties => $p },
65             );
66 2         12 for (qw(spreadsheetId spreadsheetUrl properties)) {
67             $result->{$_} or LOGDIE "No '$_' returned from creating spreadsheet";
68             }
69              
70 2         5 return $self->open_spreadsheet(
71 6 50       16 id => $result->{spreadsheetId},
72             uri => $result->{spreadsheetUrl},
73             );
74             }
75              
76             my $self = shift;
77 2         9 my $id = Spreadsheet_Id;
78             state $check = compile_named(
79             spreadsheet_id => StrMatch[qr/$id/],
80             _extra_ => slurpy Any,
81 6     6 1 306 );
82 6         15 my $p = named_extra($check->(@_));
83 6         48 my $file_id = delete $p->{spreadsheet_id};
84             my $file = $self->drive()->file(id => $file_id);
85             my $copy = $file->copy(%$p);
86             return $self->open_spreadsheet(id => $copy->file_id());
87 6         1916 }
88 6         25  
89 6         27 my $self = shift;
90 6         246 my $id = Spreadsheet_Id;
91 6         299 state $check = compile(StrMatch[qr/$id/]);
92             my ($spreadsheet_id) = $check->(@_);
93             return $self->drive()->file(id => $spreadsheet_id)->delete();
94             }
95 5     5 1 253  
96 5         10 # delete all the spreadsheets by the names passed.
97 5         37 my $self = shift;
98 5         1812  
99 5         86 state $check = compile(ArrayRef->plus_coercions(Str, sub { [$_]; }));
100             my ($names) = $check->(@_);
101              
102             my $count = 0;
103             foreach my $name (@$names) {
104 4     4 1 231 my @spreadsheets = grep { $_->{name} eq $name; } $self->spreadsheets();
105             $count += scalar @spreadsheets;
106 4     4   11 DEBUG(sprintf("Deleting %d spreadsheets for name '$name'", scalar @spreadsheets));
  4         56  
107 4         2123 $self->delete_spreadsheet($_->{id}) foreach (@spreadsheets);
108             }
109 4         25 return $count;
110 4         10 }
111 4         10  
  12         24  
112 4         11 # list all spreadsheets.
113 4         30 my $self = shift;
114 4         36 my $drive = $self->drive();
115             my $spreadsheets = $drive->filter_files(Spreadsheet_Filter);
116 4         21 my @spreadsheets = map { { id => $_->{id}, name => $_->{name} }; } @{ $spreadsheets->{files} };
117             return @spreadsheets;
118             }
119              
120             my $self = shift;
121 9     9 1 260 if (!$self->{drive}) {
122 9         29 load DriveApi3;
123 9         34 $self->{drive} = DriveApi3->new(api => $self->rest_api());
124 9         20 }
  27         97  
  9         32  
125 9         34 return $self->{drive};
126             }
127              
128              
129 20     20 1 38 1;
130 20 100       87  
131 10         45  
132 10         759 =head1 NAME
133              
134 20         374 Google::RestApi::SheetsApi4 - API to Google Sheets API V4.
135              
136             =head1 SYNOPSIS
137 8     8 1 70  
138 0     0 0 0 =over
139 6     6 0 16  
140 246     246 0 2593 use aliased Google::RestApi;
141             use aliased Google::RestApi::SheetsApi4;
142              
143             $rest_api = RestApi->new(%config);
144             $sheets_api = SheetsApi4->new(api => $rest_api);
145             $sheet = $sheets_api->create_spreadsheet(title => 'my_name');
146             $ws0 = $sheet->open_worksheet(id => 0);
147             $sw1 = $sheet->add_worksheet(name => 'Fred');
148              
149             # sub Worksheet::cell/col/cols/row/rows immediately get/set
150             # values. this is less efficient but the simplest way to
151             # interface with the api. you don't deal with any intermediate
152             # api objects.
153            
154             # add some data to the worksheet:
155             @values = (
156             [ 1001, "Herb Ellis", "100", "10000" ],
157             [ 1002, "Bela Fleck", "200", "20000" ],
158             [ 1003, "Freddie Mercury", "999", "99999" ],
159             );
160             $ws0->rows([1, 2, 3], \@values);
161             $values = $ws0->rows([1, 2, 3]);
162              
163             # use and manipulate 'range' objects to do more complex work.
164             # ranges can be specified in many ways, use whatever way is most convenient.
165             $range = $ws0->range("A1:B2");
166             $range = $ws0->range([[1,1],[2,2]]);
167             $range = $ws0->range([{col => 1, row => 1}, {col => 2, row => 2}]);
168              
169             $cell = $ws0->range_cell("A1");
170             $cell = $ws0->range_cell([1,1]);
171             $cell = $ws0->range_cell({col => 1, row => 1});
172              
173             $col = $ws0->range_col(1);
174             $col = $ws0->range_col("A3:A");
175             $col = $ws0->range_col([1]);
176             $col = $ws0->range_col([[1, 3], [1]]);
177             $col = $ws0->range_col({col => 1});
178              
179             $row = $ws0->range_row(1);
180             $row = $ws0->range_row("C1:1");
181             $row = $ws0->range_row([<false>, 1]);
182             $row = $ws0->range_row({row => 1});
183             $row = $ws0->range_row([{col => 3, row => 1 }, {row => 1}]);
184              
185             # add a header:
186             $row = $ws0->range_row(1);
187             $row->insert_d()->freeze()->bold()->italic()->center()->middle()->submit_requests();
188             # sends the values to the api directly, not using batch (less efficient):
189             $row->values(values => [qw(Id Name Tax Salary)]);
190              
191             # bold the names:
192             $col = $ws0->range_col("B2:B");
193             $col->bold()->submit_requests();
194              
195             # add some tax info:
196             $tax = $ws0->range_cell([ 3, 5 ]); # or 'C5' or [ 'C', 5 ] or { col => 3, row => 5 }...
197             $salary = $ws0->range_cell({ col => "D", row => 5 }); # same as "D5"
198             # set up batch update with staged values:
199             $tax->batch_values(values => "=SUM(C2:C4)");
200             $salary->batch_values(values => "=SUM(D2:D4)");
201             # now collect the ranges into a group and send the values via batch:
202             $rg = $sheet->range_group($tax, $salary);
203             $rg->submit_values();
204             # bold and italicize both cells, and put a solid border around each one:
205             $rg->bold()->italic()->bd_solid()->submit_requests();
206              
207             # tie ranges to a hash:
208             $row = $ws0->tie_cells({id => 'A2'}, {name => 'B2'});
209             $row->{id} = '1001';
210             $row->{name} = 'Herb Ellis';
211             tied(%$row)->submit_values();
212              
213             # or use a hash slice:
214             $ranges = $ws0->tie_ranges();
215             @$ranges{ 'A2', 'B2', 'C2', 'D4:E5' } =
216             (1001, "Herb Ellis", "123 Some Street", [["Halifax"]]);
217             tied(%$ranges)->submit_values();
218              
219             # use simple header column/row values as a source for tied keys:
220             $cols = $ws0->tie_cols('Id', 'Name');
221             $cols->{Id} = [1001, 1002, 1003];
222             $cols->{Name} = ['Herb Ellis', 'Bela Fleck', 'Freddie Mercury'];
223             tied(%$cols)->submit_values();
224              
225             # format tied values by requesting that the tied hash returns the
226             # underlying range objects on fetch:
227             tied(%$rows)->fetch_range(1);
228             $rows->{Id}->bold()->center();
229             $rows->{Name}->red();
230             # turn off fetch range and submit the formatting:
231             tied(%$rows)->fetch_range(0)->submit_requests();
232              
233             # iterators can be used to step through ranges:
234             # a basic iterator on a column:
235             $col = $ws0->range_col(1);
236             $i = $col->iterator();
237             while(1) {
238             $cell = $i->next();
239             last if !defined $cell->values();
240             }
241              
242             # a basic iterator on an arbitrary range, iterating by col or row:
243             $range = $ws0->range("A1:C3");
244             $i = $range->iterator(dim => 'col');
245             $cell = $i->next(); # A1
246             $cell = $i->next(); # A2
247             $i = $range->iterator(dim => 'row');
248             $cell = $i->next(); # A1
249             $cell = $i->next(); # B1
250              
251             # an iterator on a range group:
252             $col = $ws0->range_col(1);
253             $row = $ws0->range_row(1);
254             $rg = $sheet->range_group($col, $row);
255             $i = $rg->iterator();
256             $rg2 = $i->next(); # another range group of cells A1, A1
257             $rg2 = $i->next(); # another range group of cells A2, B1
258              
259             # an iterator on a tied range group:
260             $cols = $ws0->tie_cols(qw(Id Name));
261             $i = tied(%$cols)->iterator();
262             $row = $i->next();
263             $row->{Id} = '1001';
264             $row->{Name} = 'Herb Ellis';
265             tied(%$row)->submit_values();
266              
267             =back
268              
269             =head1 DESCRIPTION
270              
271             SheetsApi4 is an API to Google Sheets. It is very perl-ish in that there is usually "more than one way to do it". It provides default behaviours
272             that should be fine for most normal needs, but those behaviours can be overridden when necessary.
273              
274             It is assumed that you are familiar with the Google Sheets API: https://developers.google.com/sheets/api
275              
276             C<t/tutorial/sheets/*> also has a step-by-step tutorial of creating and updating a spreadsheet, showing you the API calls and return values for each step.
277              
278             =head1 NAVIGATION
279              
280             =over
281              
282             =item * L<Google::RestApi::SheetsApi4>
283              
284             =item * L<Google::RestApi::SheetsApi4::Spreadsheet>
285              
286             =item * L<Google::RestApi::SheetsApi4::Worksheet>
287              
288             =item * L<Google::RestApi::SheetsApi4::Range>
289              
290             =item * L<Google::RestApi::SheetsApi4::Range::All>
291              
292             =item * L<Google::RestApi::SheetsApi4::Range::Col>
293              
294             =item * L<Google::RestApi::SheetsApi4::Range::Row>
295              
296             =item * L<Google::RestApi::SheetsApi4::Range::Cell>
297              
298             =item * L<Google::RestApi::SheetsApi4::RangeGroup>
299              
300             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Iterator>
301              
302             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Tie>
303              
304             =item * L<Google::RestApi::SheetsApi4::RangeGroup::Tie::Iterator>
305              
306             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet>
307              
308             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet::Worksheet>
309              
310             =item * L<Google::RestApi::SheetsApi4::Request::Spreadsheet::Worksheet::Range>
311              
312             =back
313              
314             =head1 SUBROUTINES
315              
316             =over
317              
318             =item new(%args);
319              
320             Creates a new instance of a SheetsApi object.
321              
322             %args consists of:
323              
324             =over
325              
326             =item C<api> L<<Google::RestApi>>: A reference to a configured L<Google::RestApi> instance.
327              
328             =back
329              
330             =item api(%args);
331              
332             %args consists of:
333              
334             =over
335              
336             =item * C<uri> <path_segments_string>: Adds this path segment to the Sheets endpoint and calls the L<Google::RestApi>'s C<api> subroutine.
337              
338             =item * C<%args>: Passes any extra arguments to the L<Google::RestApi>'s C<api> subroutine (content, params, method etc).
339              
340             =back
341              
342             This is essentially a pass-through method between lower-level Worksheet/Range objects and L<Google::RestApi>, where this method adds in the Sheets endpoint.
343             See <Google::RestApi::SheetsApi4::Worksheet>'s C<api> routine for how this is called. You would not normally call this directly unless you were making a Google API call not currently
344             supported by this API framework.
345              
346             Returns the response hash from Google API.
347              
348             =item create_spreadsheet(%args);
349              
350             Creates a new spreadsheet.
351              
352             %args consists of:
353              
354             =over
355              
356             =item * C<title|name> <string>: The title (or name) of the new spreadsheet.
357              
358             =item * C<%args>: Passes through any extra arguments to Google Drive's create file routine.
359              
360             =back
361              
362             Args C<title> and C<name> are synonymous, you can use either. Note that Sheets allows multiple spreadsheets with the same name.
363              
364             Normally this would be called via the Spreadsheet object, which would fill in the Drive file ID for you.
365              
366             Returns the object instance of the new spreadsheet object.
367              
368             =item copy_spreadsheet(%args);
369              
370             Creates a copy of a spreadsheet.
371              
372             %args consists of:
373              
374             =over
375              
376             =item * C<spreadsheet_id> <string>: The file ID in Google Drive of the spreadsheet you want to make a copy of.
377              
378             =item * C<%args>: Additional arguments passed through to Google Drive file copy subroutine.
379              
380             =back
381              
382             Returns the object instance of the new spreadsheet object.
383              
384             =item delete_spreadsheet(spreadsheet_id<string>);
385              
386             Deletes the spreadsheet from Google Drive.
387              
388             %args consists of:
389              
390             spreadsheet_id is the file ID in Google Drive of the spreadsheet you want to delete.
391              
392             Returns the Google API response.
393              
394             =item delete_all_spreadsheets(spreadsheet_name<string>);
395              
396             Deletes all spreadsheets with the given name from Google Drive.
397              
398             Returns the number of spreadsheets deleted.
399              
400             =item spreadsheets();
401              
402             Returns a list of spreadsheets in Google Drive.
403              
404             =item drive();
405              
406             Returns an instance of Google Drive that shares the same RestApi as this SheetsApi object. You would not normally need to use this directly.
407              
408             =item open_spreadsheet(%args);
409              
410             Opens a new spreadsheet from the given id, uri, or name.
411              
412             %args consists of any args passed to Spreadsheet->new routine (which see).
413              
414             =back
415              
416             =head1 STATUS
417              
418             This api is currently in beta status. It is incomplete. There may be design flaws that need to be addressed in later releases. Later
419             releases may break this release. Not all api calls have been implemented.
420              
421             =head1 AUTHORS
422              
423             =over
424              
425             =item
426              
427             Robin Murray mvsjes@cpan.org
428              
429             =back
430              
431             =head1 COPYRIGHT
432              
433             Copyright (c) 2021, Robin Murray. All rights reserved.
434              
435             This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.