sheets_v4 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6357385eeb33468356dd974f8fda49f92e09b30b9911a825c562509bf5f70dd
4
- data.tar.gz: a289b95495fd49e8221a89e037168aaae903ca40af2f9167cff60c75c0f28acc
3
+ metadata.gz: 60f10846ff0c3b62cb29c487499eb717897a2b28916606de7d3199fe0424435f
4
+ data.tar.gz: a6bb345b4379f94dbaa82b266943c144321e3bcf3404db34c1788e766813a766
5
5
  SHA512:
6
- metadata.gz: 59b81fbfa525ee8f5810d1aeeb3d6168dfffe0ffd1a8c19792d8f27548cdb1b2106748a30f499f8619bd83cd73d773c5647f0b7fd57640c990af2eb7e9d5fd7b
7
- data.tar.gz: '09ffa389430c102e6e4df4370bc42f2e8769399f78cabbfe61441372ed6bf4a87378a124a3c17fe0c7f53a73255fae7f153cd08df67e93589c5bcbb49360166e'
6
+ metadata.gz: 6914986ccca02b55b260b2a39e6777ce5e2cbca03f6fd00053d8ed96a960456831ee008bd175448da343eeaf0bcfc8c67309e489035b9792ec4ce51aba29d917
7
+ data.tar.gz: f76c250d9b9bec531ca6185ef3b2b351df6e0a12eccb1d6f862cf6a63d4b81305798630d992d3ec22fb44a94cbd488f3736e85da6ff72302eb7b7cf87017e176
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ Changes for each release are listed in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/) for its releases.
6
6
 
7
+ ## v0.6.0 (2023-10-03)
8
+
9
+ [Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.5.0..v0.6.0)
10
+
11
+ Changes since v0.5.0:
12
+
13
+ * 2eab61d Update documentation explaining how to construct and validate a request (#17)
14
+
15
+ ## v0.5.0 (2023-10-01)
16
+
17
+ [Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.4.0..v0.5.0)
18
+
19
+ Changes since v0.4.0:
20
+
21
+ * b403430 Refactor SheetsV4.validate_api_object (#15)
22
+
7
23
  ## v0.4.0 (2023-09-29)
8
24
 
9
25
  [Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.3.0..v0.4.0)
data/README.md CHANGED
@@ -9,29 +9,51 @@
9
9
 
10
10
  Unofficial helpers for the Google Sheets V4 API
11
11
 
12
- * [Important Links for Programming Google Sheets](#important-links-for-programming-google-sheets)
13
- * [General API Documentation](#general-api-documentation)
14
- * [Ruby Implementation of the Sheets API](#ruby-implementation-of-the-sheets-api)
15
- * [Other Links](#other-links)
16
12
  * [Installation](#installation)
13
+ * [Important links for programming Google Sheets](#important-links-for-programming-google-sheets)
14
+ * [General API documentation](#general-api-documentation)
15
+ * [Ruby implementation of the Sheets API](#ruby-implementation-of-the-sheets-api)
16
+ * [Other Links](#other-links)
17
+ * [Getting Started](#getting-started)
18
+ * [Creating a Google Cloud project](#creating-a-google-cloud-project)
19
+ * [Enable the APIs you want to use](#enable-the-apis-you-want-to-use)
20
+ * [Create a Google API credentials](#create-a-google-api-credentials)
17
21
  * [Usage](#usage)
18
22
  * [Obtaining an authenticated SheetsService](#obtaining-an-authenticated-sheetsservice)
23
+ * [Building a request](#building-a-request)
24
+ * [Method 1: constructing requests using `Google::Apis::SheetsV4::*` objects](#method-1-constructing-requests-using-googleapissheetsv4-objects)
25
+ * [Method 2: constructing requests using hashes](#method-2-constructing-requests-using-hashes)
26
+ * [Which method should be used?](#which-method-should-be-used)
27
+ * [Validating requests](#validating-requests)
19
28
  * [Colors](#colors)
20
29
  * [Development](#development)
21
- * [Creating a Google API Service Account](#creating-a-google-api-service-account)
22
30
  * [Contributing](#contributing)
23
31
  * [License](#license)
24
32
 
25
- ## Important Links for Programming Google Sheets
33
+ ## Installation
26
34
 
27
- ### General API Documentation
35
+ Install the gem and add to the application's Gemfile by executing:
36
+
37
+ ```shell
38
+ bundle add sheets_v4
39
+ ```
40
+
41
+ If bundler is not being used to manage dependencies, install the gem by executing:
42
+
43
+ ```shell
44
+ gem install sheets_v4
45
+ ```
46
+
47
+ ## Important links for programming Google Sheets
48
+
49
+ ### General API documentation
28
50
 
29
51
  * [Google Sheets API Overview](https://developers.google.com/sheets/api)
30
52
  * [Google Sheets API Reference](https://developers.google.com/sheets/api/reference/rest)
31
53
  * [Batch Update Requests](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request)
32
54
  * [Discovery Document for the Sheets API](https://sheets.googleapis.com/$discovery/rest?version=v4)
33
55
 
34
- ### Ruby Implementation of the Sheets API
56
+ ### Ruby implementation of the Sheets API
35
57
 
36
58
  * [SheetsService Class](https://github.com/googleapis/google-api-ruby-client/blob/main/generated/google-apis-sheets_v4/lib/google/apis/sheets_v4/service.rb)
37
59
  * [All Other Sheets Classes](https://github.com/googleapis/google-api-ruby-client/blob/main/generated/google-apis-sheets_v4/lib/google/apis/sheets_v4/classes.rb)
@@ -40,19 +62,25 @@ Unofficial helpers for the Google Sheets V4 API
40
62
 
41
63
  * [Apps Script for Sheets](https://developers.google.com/apps-script/guides/sheets)
42
64
 
43
- ## Installation
65
+ ## Getting Started
44
66
 
45
- Install the gem and add to the application's Gemfile by executing:
67
+ In order to use this gem, you will need to obtain a Google API sheets service
68
+ credential following the instructions below.
46
69
 
47
- ```shell
48
- bundle add sheets_v4
49
- ```
70
+ ### Creating a Google Cloud project
50
71
 
51
- If bundler is not being used to manage dependencies, install the gem by executing:
72
+ Create a Google Cloud project using [these directions](https://developers.google.com/workspace/guides/create-project).
52
73
 
53
- ```shell
54
- gem install sheets_v4
55
- ```
74
+ ### Enable the APIs you want to use
75
+
76
+ Enable the Sheets API for this project using [these directions](https://developers.google.com/workspace/guides/enable-apis).
77
+
78
+ ### Create a Google API credentials
79
+
80
+ Create a service account and download credentials using [these directions](https://developers.google.com/workspace/guides/create-credentials#service-account).
81
+
82
+ You can store the download credential files anywhere on your system. The recommended
83
+ location is `~/.google-api-credential.json`.
56
84
 
57
85
  ## Usage
58
86
 
@@ -86,19 +114,165 @@ If the credential is stored elsewhere, pass the credential_source to `SheetsV4.s
86
114
  manually. `credential_source` can be a String:
87
115
 
88
116
  ```Ruby
89
- sheets_service = SheetsV4.sheets_service(credential_sourvce: File.read('credential.json'))
117
+ sheets_service = SheetsV4.sheets_service(credential_source: File.read('credential.json'))
90
118
  ```
91
119
 
92
120
  an IO object:
93
121
 
94
122
  ```Ruby
95
123
  sheets_service = File.open('credential.json') do |credential_source|
96
- SheetsV4.sheets_service(credential_sourvce:)
124
+ SheetsV4.sheets_service(credential_source:)
97
125
  end
98
126
  ```
99
127
 
100
128
  or an already constructed `Google::Auth::*`` object.
101
129
 
130
+ ### Building a request
131
+
132
+ To use the Sheets API, you need to construct JSON formatted requests.
133
+
134
+ These requests can be constructed using two different methods:
135
+ 1. constructing requests using `Google::Apis::SheetsV4::*` objects or
136
+ 2. constructing requests using hashes
137
+
138
+ The following two sections show how each method can be used to construct
139
+ a request to update a row of values in a sheet.
140
+
141
+ For these two examples, values in the `values` array will be written to the
142
+ sheet whose ID is 0. The values will be written one per row starting at cell A1.
143
+
144
+ ```Ruby
145
+ values = %w[one two three four] # 'one' goes in A1, 'two' goes in A2, etc.
146
+ ```
147
+
148
+ The method `SheetsService#batch_update_spreadsheet` will be used to write the values. This
149
+ method takes a `batch_update_spreadsheet_request` object with a `update_cells` request
150
+ that defines the update to perform.
151
+
152
+ #### Method 1: constructing requests using `Google::Apis::SheetsV4::*` objects
153
+
154
+ When using this method, keep the Ruby source file containing the SheetsService class
155
+ ([google/apis/sheets_v4/service.rb](https://github.com/googleapis/google-api-ruby-client/blob/main/generated/google-apis-sheets_v4/lib/google/apis/sheets_v4/service.rb))
156
+ and the Ruby source file containing the defitions of the request & data classes
157
+ ([lib/google/apis/sheets_v4/classes.rb](https://github.com/googleapis/google-api-ruby-client/blob/main/generated/google-apis-sheets_v4/lib/google/apis/sheets_v4/classes.rb))
158
+ open for easy searching. These files will give you all the information you need
159
+ to construct valid requests.
160
+
161
+ Here is the example constructing requests using `Google::Apis::SheetsV4::*` objects
162
+
163
+ ```Ruby
164
+ def values = %w[one two three four]
165
+
166
+ def row_data(value)
167
+ Google::Apis::SheetsV4::RowData.new(
168
+ values: [
169
+ Google::Apis::SheetsV4::CellData.new(
170
+ user_entered_value:
171
+ Google::Apis::SheetsV4::ExtendedValue.new(string_value: value.to_s)
172
+ )
173
+ ]
174
+ )
175
+ end
176
+
177
+ def rows
178
+ values.map { |value| row_data(value) }
179
+ end
180
+
181
+ def write_values_request
182
+ fields = 'user_entered_value'
183
+ start = Google::Apis::SheetsV4::GridCoordinate.new(
184
+ sheet_id: 0, row_index: 0, column_index: 0
185
+ )
186
+ Google::Apis::SheetsV4::Request.new(
187
+ update_cells: Google::Apis::SheetsV4::UpdateCellsRequest.new(rows:, fields:, start:)
188
+ )
189
+ end
190
+
191
+ requests = Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new(requests: [write_values_request])
192
+
193
+ spreadsheet_id = '18FAcgotK7nDfLTOTQuGCIjKwxkJMAguhn1OVzpFFgWY'
194
+ SheetsV4.sheets_service.batch_update_spreadsheet(spreadsheet_id, requests)
195
+ ```
196
+
197
+ #### Method 2: constructing requests using hashes
198
+
199
+ When constructing requests using this method, keep the [Google Sheets Rest API Reference](https://developers.google.com/sheets/api/reference/rest)
200
+ documentation open. In particular, [the Batch Update Requests page](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#cutpasterequest)
201
+ is particularly useful for building spreadsheet batch update requests.
202
+
203
+ One caveat to keep in mind is that the Rest API documents object properties using
204
+ Camel case BUT the Ruby API requires snake case.
205
+
206
+ For instance, the Rest API documents the properties for a grid coordinate to be
207
+ "sheetId", "rowIndex", and "columnIndex". However, in the Ruby API, you should
208
+ construct this object using snake case:
209
+
210
+ ```Ruby
211
+ grid_coordinate = { sheet_id: 0, row_index: 0, column_index: 0 }
212
+ ```
213
+
214
+ Here is the example constructing requests using hashes:
215
+
216
+ ```Ruby
217
+ def values = %w[one two three four]
218
+
219
+ def rows
220
+ values.map do |value|
221
+ { values: [{ user_entered_value: { string_value: value } }] }
222
+ end
223
+ end
224
+
225
+ def write_values_request
226
+ fields = 'user_entered_value'
227
+ start = { sheet_id: 0, row_index: 0, column_index: 0 }
228
+ { update_cells: { rows:, fields:, start: } }
229
+ end
230
+
231
+ requests = { requests: [write_values_request] }
232
+
233
+ spreadsheet_id = '18FAcgotK7nDfLTOTQuGCIjKwxkJMAguhn1OVzpFFgWY'
234
+ response = SheetsV4.sheets_service.batch_update_spreadsheet(spreadsheet_id, requests)
235
+ ```
236
+
237
+ #### Which method should be used?
238
+
239
+ Either method will do the same job. I prefer "Method 2: constructing requests using
240
+ hashes" because my code is more concise and easy to read.
241
+
242
+ While either method can produce a malformed request, "Method 2" is more likely to
243
+ result in malformed requests. Unfortunately, when given a malformed request, the
244
+ Google Sheets API will do one of following depending on the nature of the problem:
245
+
246
+ 1. Raise a `Google::Apis::ClientError` with some additional information
247
+ 2. Raise a `Google::Apis::ClientError` with no additional information (this the most
248
+ common result)
249
+ 3. Not return an error with some of the batch requests not having the expected outcome
250
+
251
+ Luckily, this library provides a way to validate that requests are valid and
252
+ identifies precisely where the request objects do not conform to the API description.
253
+ That is the subject of the next section [Validating requests](#validating-requests).
254
+
255
+ ### Validating requests
256
+
257
+ The [`SheetsV4.validate_api_object`](https://rubydoc.info/gems/sheets_v4/SheetsV4#validate_api_object-class_method)
258
+ method can be used to validate request objects prior to using them in the Google
259
+ Sheets API.
260
+
261
+ This method takes a `schema_name` and an `object` to validate. Schema names can be
262
+ listed using [`SheetsV4.api_object_schema_names`](https://rubydoc.info/gems/sheets_v4/SheetsV4#api_object_schema_names-class_method).
263
+
264
+ This method will either return `true` if `object` conforms to the schema OR it
265
+ will raise a RuntimeError noting where the object structure did not conform to
266
+ the schema.
267
+
268
+ In the previous examples (see [Building a request](#building-a-request)), the
269
+ following line can be inserted after the `requests = ...` line to validate the
270
+ request:
271
+
272
+ ```Ruby
273
+ SheetsV4.validate_api_object(schema: 'batch_update_spreadsheet_request', object: requests)
274
+ ```
275
+
102
276
  ### Colors
103
277
 
104
278
  Color objects (with appropriate :red, :green, :blue values) can be retrieved by name
@@ -114,8 +288,8 @@ SheetsV4::Color.black #=> {:red=>0.0, :green=>0.0, :blue=>0.0}
114
288
 
115
289
  ## Development
116
290
 
117
- After checking out the repo, run `bin/setup` to install dependencies. Then, run
118
- `rake spec` to run the tests. You can also run `bin/console` for an interactive
291
+ After checking out the repo, run `bin/setup` to install dependencies and then, run
292
+ `rake` to run the tests, static analysis, etc. You can also run `bin/console` for an interactive
119
293
  prompt that will allow you to experiment.
120
294
 
121
295
  To install this gem onto your local machine, run `bundle exec rake install`. To
@@ -124,9 +298,6 @@ release a new version, update the version number in `version.rb`, and then run
124
298
  commits and the created tag, and push the `.gem` file to
125
299
  [rubygems.org](https://rubygems.org).
126
300
 
127
- ## Creating a Google API Service Account
128
-
129
-
130
301
  ## Contributing
131
302
 
132
303
  Bug reports and pull requests are welcome on [the main-branch/sheets_v4 GitHub project](https://github.com/main-branch/sheets_v4).
@@ -43,10 +43,12 @@ end
43
43
 
44
44
  def requests = { requests: [write_names, set_background_colors] }
45
45
 
46
- # SheetsV4.validate_api_object(
47
- # schema_name: 'BatchUpdateSpreadsheetRequest', object: request,
48
- # logger: Logger.new(STDOUT, level: Logger::ERROR)
49
- # )
46
+ # OPTIONAL: validate the requests against the schema before sending them to the API.
47
+ #
48
+ # While not necessary, it can be helpful to identify errors since the API will return
49
+ # an error if the request is invalid but does not tell you where the problem is.
50
+ #
51
+ SheetsV4.validate_api_object(schema_name: 'batch_update_spreadsheet_request', object: requests)
50
52
 
51
53
  spreadsheet_id = '18FAcgotK7nDfLTOTQuGCIjKwxkJMAguhn1OVzpFFgWY'
52
54
  SheetsV4.sheets_service.batch_update_spreadsheet(spreadsheet_id, requests)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'sheets_v4'
5
+ require 'googleauth'
6
+
7
+ # Example to show using the SheetsV4 module to set the background color of a cell
8
+ #
9
+ # GIVEN the credential file is at ~/.google-api-credential.json
10
+ # AND the spreadsheet id is 18FAcgotK7nDfLTOTQuGCIjKwxkJMAguhn1OVzpFFgWY
11
+ # AND the spreadsheet has a sheet whose id is 0
12
+ # WHEN the script is run
13
+ # THEN the sheet whose id is 0 will have the background color names starting at A2
14
+ # AND the background color of the cells in column B will be set to the color matching the color name in column A
15
+
16
+ # Build the requests
17
+
18
+ def name_rows
19
+ SheetsV4.color_names.map do |color_name|
20
+ Google::Apis::SheetsV4::RowData.new(
21
+ values: [
22
+ Google::Apis::SheetsV4::CellData.new(
23
+ user_entered_value: Google::Apis::SheetsV4::ExtendedValue.new(string_value: color_name.to_s)
24
+ )
25
+ ]
26
+ )
27
+ # { values: [{ user_entered_value: { string_value: color_name.to_s } }] }
28
+ end
29
+ end
30
+
31
+ def write_names
32
+ rows = name_rows
33
+ fields = 'user_entered_value'
34
+ start = Google::Apis::SheetsV4::GridCoordinate.new(sheet_id: 0, row_index: 1, column_index: 0)
35
+ Google::Apis::SheetsV4::Request.new(
36
+ update_cells: Google::Apis::SheetsV4::UpdateCellsRequest.new(rows:, fields:, start:)
37
+ )
38
+ end
39
+
40
+ def background_color_rows
41
+ SheetsV4.color_names.map { |color_name| SheetsV4.color(color_name) }.map do |color|
42
+ background_color = Google::Apis::SheetsV4::Color.new(**color)
43
+ user_entered_format = Google::Apis::SheetsV4::CellFormat.new(background_color:)
44
+ cell_data = Google::Apis::SheetsV4::CellData.new(user_entered_format:)
45
+ Google::Apis::SheetsV4::RowData.new(values: [cell_data])
46
+ end
47
+ end
48
+
49
+ def set_background_colors
50
+ rows = background_color_rows
51
+ fields = 'user_entered_format'
52
+ start = Google::Apis::SheetsV4::GridCoordinate.new(sheet_id: 0, row_index: 1, column_index: 1)
53
+ update_cells = Google::Apis::SheetsV4::UpdateCellsRequest.new(rows:, fields:, start:)
54
+ Google::Apis::SheetsV4::Request.new(update_cells:)
55
+ end
56
+
57
+ def requests = Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new(requests: [write_names, set_background_colors])
58
+
59
+ # OPTIONAL: validate the requests against the schema before sending them to the API.
60
+ #
61
+ # While not necessary, it can be helpful to identify errors since the API will return
62
+ # an error if the request is invalid but does not tell you where the problem is.
63
+ #
64
+ SheetsV4.validate_api_object(schema_name: 'batch_update_spreadsheet_request', object: requests.to_h)
65
+
66
+ spreadsheet_id = '18FAcgotK7nDfLTOTQuGCIjKwxkJMAguhn1OVzpFFgWY'
67
+ SheetsV4.sheets_service.batch_update_spreadsheet(spreadsheet_id, requests)
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SheetsV4
4
+ module ValidateApiObjects
5
+ # Load the Google Discovery API description for the Sheets V4 API
6
+ #
7
+ # @example
8
+ # logger = Logger.new(STDOUT, :level => Logger::ERROR)
9
+ # schemas = SheetsV4::ValidateApiObjects::LoadSchemas.new(logger:).call
10
+ #
11
+ # @api private
12
+ #
13
+ class LoadSchemas
14
+ # Loads schemas for the Sheets V4 API object from the Google Discovery API
15
+ #
16
+ # By default, a nil logger is used. This means that nothing is logged.
17
+ #
18
+ # The schemas are only loaded once and cached.
19
+ #
20
+ # @example
21
+ # schema_loader = SheetsV4::ValidateApiObjects::LoadSchemas.new
22
+ #
23
+ # @param logger [Logger] the logger to use
24
+ #
25
+ def initialize(logger: Logger.new(nil))
26
+ @logger = logger
27
+ end
28
+
29
+ # The logger to use internally for logging errors
30
+ #
31
+ # @example
32
+ # logger = Logger.new(STDOUT, :level => Logger::INFO)
33
+ # validator = SheetsV4::ValidateApiObjects::LoadSchemas.new(logger)
34
+ # validator.logger == logger # => true
35
+ #
36
+ # @return [Logger]
37
+ #
38
+ attr_reader :logger
39
+
40
+ # A hash of schemas keyed by the schema name loaded from the Google Discovery API
41
+ #
42
+ # @example
43
+ # SheetsV4.api_object_schemas #=> { 'PersonSchema' => { 'type' => 'object', ... } ... }
44
+ #
45
+ # @return [Hash<String, Object>] a hash of schemas keyed by schema name
46
+ #
47
+ def call
48
+ self.class.schema_load_semaphore.synchronize { @call ||= load_api_object_schemas }
49
+ end
50
+
51
+ private
52
+
53
+ # Validate
54
+ # A mutex used to synchronize access to the schemas so they are only loaded
55
+ # once.
56
+ #
57
+ @schema_load_semaphore = Thread::Mutex.new
58
+
59
+ class << self
60
+ # A mutex used to synchronize access to the schemas so they are only loaded once
61
+ #
62
+ # @return [Thread::Mutex]
63
+ #
64
+ # @api private
65
+ #
66
+ attr_reader :schema_load_semaphore
67
+ end
68
+
69
+ # Load the schemas from the Google Discovery API
70
+ #
71
+ # @return [Hash<String, Object>] a hash of schemas keyed by schema name
72
+ #
73
+ # @api private
74
+ #
75
+ def load_api_object_schemas
76
+ source = 'https://sheets.googleapis.com/$discovery/rest?version=v4'
77
+ http_response = Net::HTTP.get_response(URI.parse(source))
78
+ raise_error(http_response) if http_response.code != '200'
79
+
80
+ data = http_response.body
81
+ JSON.parse(data)['schemas'].tap { |schemas| post_process_schemas(schemas) }
82
+ end
83
+
84
+ # Log an error and raise a RuntimeError based on the HTTP response code
85
+ # @param http_response [Net::HTTPResponse] the HTTP response
86
+ # @return [void]
87
+ # @raises [RuntimeError]
88
+ # @api private
89
+ def raise_error(http_response)
90
+ message = "HTTP Error '#{http_response.code}' loading schemas from '#{http_response.uri}'"
91
+ logger.error(message)
92
+ raise message
93
+ end
94
+
95
+ REF_KEY = '$ref'
96
+
97
+ # A visitor for the schema object tree that fixes up the tree as it goes
98
+ # @return [void]
99
+ # @api private
100
+ def schema_visitor(path:, object:)
101
+ return unless object.is_a? Hash
102
+
103
+ convert_schema_names_to_snake_case(path, object)
104
+ convert_schema_ids_to_snake_case(path, object)
105
+ add_unevaluated_properties(path, object)
106
+ convert_property_names_to_snake_case(path, object)
107
+ convert_ref_values_to_snake_case(path, object)
108
+ end
109
+
110
+ # Convert schema names to snake case
111
+ # @return [void]
112
+ # @api private
113
+ def convert_schema_names_to_snake_case(path, object)
114
+ object.transform_keys!(&:underscore) if path.empty?
115
+ end
116
+
117
+ # Convert schema IDs to snake case
118
+ # @return [void]
119
+ # @api private
120
+ def convert_schema_ids_to_snake_case(path, object)
121
+ object['id'] = object['id'].underscore if object.key?('id') && path.size == 1
122
+ end
123
+
124
+ # Add 'unevaluatedProperties: false' to all schemas
125
+ # @return [void]
126
+ # @api private
127
+ def add_unevaluated_properties(path, object)
128
+ object['unevaluatedProperties'] = false if path.size == 1
129
+ end
130
+
131
+ # Convert object property names to snake case
132
+ # @return [void]
133
+ # @api private
134
+ def convert_property_names_to_snake_case(path, object)
135
+ object.transform_keys!(&:underscore) if path[-1] == 'properties'
136
+ end
137
+
138
+ # Convert reference values to snake case
139
+ # @return [void]
140
+ # @api private
141
+ def convert_ref_values_to_snake_case(_path, object)
142
+ object[REF_KEY] = object[REF_KEY].underscore if object.key?(REF_KEY)
143
+ end
144
+
145
+ # Traverse the schema object tree and apply the schema visitor to each node
146
+ # @return [void]
147
+ # @api private
148
+ def post_process_schemas(schemas)
149
+ SheetsV4::ValidateApiObjects::TraverseObjectTree.call(
150
+ object: schemas, visitor: ->(path:, object:) { schema_visitor(path:, object:) }
151
+ )
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SheetsV4
4
+ module ValidateApiObjects
5
+ # Resolve a JSON schema reference to a Google Sheets API schema
6
+ #
7
+ # This class uses the Google Discovery API to get the schemas. Any schema reference
8
+ # in the form `{ "$ref": "schema_name" }` will be resolved by looking up the schema
9
+ # name in the Google Discovery API and returning the schema object (as a Hash).
10
+ #
11
+ # This means that `{ "$ref": "cell_data" }` is resolved by returning
12
+ # `SheetsV4::ValidateApiObjects::LoadSchemas.new(logger:).call['cell_data']`.
13
+ #
14
+ # An RuntimeError is raised if `SheetsV4::ValidateApiObjects::LoadSchemas.new.call`
15
+ # does not have a key matching the schema name.
16
+ #
17
+ # @example
18
+ # logger = Logger.new(STDOUT, level: Logger::INFO)
19
+ # ref_resolver = SheetsV4::ValidateApiObjects::ResolveSchemaRef.new(logger:)
20
+ # people_schema = { 'type' => 'array', 'items' => { '$ref' => 'person' } }
21
+ # json_validator = JSONSchemer.schema(people_schema, ref_resolver:)
22
+ # people_json = [{ 'name' => { 'first' => 'John', 'last' => 'Doe' } }]
23
+ #
24
+ # # Trying to validate people_json using json_validator as follows:
25
+ #
26
+ # json_validator.validate(people_json)
27
+ #
28
+ # # will try to load the referenced schema for 'person'. json_validator will
29
+ # # do this by calling `ref_resolver.call(URI.parse('json-schemer://schema/person'))`
30
+ #
31
+ # @api private
32
+ #
33
+ class ResolveSchemaRef
34
+ # Create a new schema resolver
35
+ #
36
+ # @param logger [Logger] the logger to use
37
+ #
38
+ # @api private
39
+ #
40
+ def initialize(logger: Logger.new(nil))
41
+ @logger = logger
42
+ end
43
+
44
+ # The logger to use internally
45
+ #
46
+ # Currently, only debug messages are logged.
47
+ #
48
+ # @return [Logger]
49
+ #
50
+ # @api private
51
+ #
52
+ attr_reader :logger
53
+
54
+ # Resolve a JSON schema reference
55
+ #
56
+ # @param ref [URI] the reference to resolve usually in the form "json-schemer://schema/{name}"
57
+ #
58
+ # @return [Hash] the schema object as a hash
59
+ #
60
+ # @api private
61
+ #
62
+ def call(ref)
63
+ schema_name = ref.path[1..]
64
+ logger.debug { "Reading schema #{schema_name}" }
65
+ schemas = SheetsV4::ValidateApiObjects::LoadSchemas.new(logger:).call
66
+ schemas[schema_name].tap do |schema_object|
67
+ raise "Schema for #{ref} not found" unless schema_object
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SheetsV4
4
+ module ValidateApiObjects
5
+ # Visit all objects in arbitrarily nested object tree of hashes and/or arrays
6
+ #
7
+ # @api public
8
+ #
9
+ class TraverseObjectTree
10
+ # Visit all objects in arbitrarily nested object tree of hashes and/or arrays
11
+ #
12
+ # For each object, the visitor is called with the path to the object and the object
13
+ # itself.
14
+ #
15
+ # In the examples below assume the elided code is the following:
16
+ #
17
+ # ```Ruby
18
+ # visitor = -> (path:, object:) { puts "path: #{path}, object: #{obj}" }
19
+ # SheetsV4::ValidateApiObjects::TraverseObjectTree.call(object:, visitor:)
20
+ # ```
21
+ #
22
+ # @example Given a simple object (not very exciting)
23
+ # object = 1
24
+ # ...
25
+ # #=> path: [], object: 1
26
+ #
27
+ # @example Given an array
28
+ # object = [1, 2, 3]
29
+ # ...
30
+ # #=> path: [], object: [1, 2, 3]
31
+ # #=> path: [0], object: 1
32
+ # #=> path: [1], object: 2
33
+ # #=> path: [2], object: 3
34
+ #
35
+ # @example Given a hash
36
+ # object = { name: 'James', age: 42 }
37
+ # ...
38
+ # #=> path: [], object: { name: 'James', age: 42 }
39
+ # #=> path: [:name], object: James
40
+ # #=> path: [:age], object: 42
41
+ #
42
+ # @example Given an array of hashes
43
+ # object = [{ name: 'James', age: 42 }, { name: 'Jane', age: 43 }]
44
+ # ...
45
+ # #=> path: [], object: [{ name: 'James', age: 42 }, { name: 'Jane', age: 43 }]
46
+ # #=> path: [0], object: { name: 'James', age: 42 }
47
+ # #=> path: [0, :name], object: James
48
+ # #=> path: [0, :age], object: 42
49
+ # #=> path: [1], object: { name: 'Jane', age: 43 }
50
+ # #=> path: [1, :name], object: Jane
51
+ # #=> path: [1, :age], object: 43
52
+ #
53
+ # @example Given a hash of hashes
54
+ # object = { person1: { name: 'James', age: 42 }, person2: { name: 'Jane', age: 43 } }
55
+ # ...
56
+ # #=> path: [], object: { person1: { name: 'James', age: 42 }, person2: { name: 'Jane', age: 43 } }
57
+ # #=> path: [:person1], object: { name: 'James', age: 42 }
58
+ # #=> path: [:person1, :name], object: James
59
+ # #=> path: [:person1, :age], object: 42
60
+ # #=> path: [:person2], object: { name: 'Jane', age: 43 }
61
+ # #=> path: [:person2, :name], object: Jane
62
+ # #=> path: [:person2, :age], object: 43
63
+ #
64
+ # @param path [Array] the path to the object
65
+ # @param object [Object] the object to visit
66
+ # @param visitor [#call] the visitor to call for each object
67
+ #
68
+ # @return [void]
69
+ #
70
+ # @api public
71
+ #
72
+ def self.call(object:, visitor:, path: [])
73
+ visitor&.call(path:, object:)
74
+ if object.is_a? Hash
75
+ object.each { |k, obj| call(path: (path + [k]), object: obj, visitor:) }
76
+ elsif object.is_a? Array
77
+ object.each_with_index { |obj, k| call(path: (path + [k]), object: obj, visitor:) }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+ require 'json_schemer'
5
+
6
+ module SheetsV4
7
+ module ValidateApiObjects
8
+ # Validate objects against a Google Sheets API request object schema
9
+ #
10
+ # @api public
11
+ #
12
+ class Validate
13
+ # Create a new validator
14
+ #
15
+ # By default, a nil logger is used. This means that no messages are logged.
16
+ #
17
+ # @example
18
+ # validator = SheetsV4::ValidateApiObjects::Validator.new
19
+ #
20
+ # @param logger [Logger] the logger to use
21
+ #
22
+ def initialize(logger: Logger.new(nil))
23
+ @logger = logger
24
+ end
25
+
26
+ # The logger to use internally
27
+ #
28
+ # Validation errors are logged at the error level. Other messages are logged
29
+ # at the debug level.
30
+ #
31
+ # @example
32
+ # logger = Logger.new(STDOUT, :level => Logger::INFO)
33
+ # validator = SheetsV4::ValidateApiObjects::Validator.new(logger)
34
+ # validator.logger == logger # => true
35
+ # validator.logger.debug { "Debug message" }
36
+ #
37
+ # @return [Logger]
38
+ #
39
+ attr_reader :logger
40
+
41
+ # Validate the object using the JSON schema named schema_name
42
+ #
43
+ # @example
44
+ # schema_name = 'batch_update_spreadsheet_request'
45
+ # object = { 'requests' => [] }
46
+ # validator = SheetsV4::ValidateApiObjects::Validator.new
47
+ # validator.call(schema_name:, object:)
48
+ #
49
+ # @param schema_name [String] the name of the schema to validate against
50
+ # @param object [Object] the object to validate
51
+ #
52
+ # @raise [RuntimeError] if the object does not conform to the schema
53
+ #
54
+ # @return [void]
55
+ #
56
+ def call(schema_name:, object:)
57
+ logger.debug { "Validating #{object} against #{schema_name}" }
58
+
59
+ schema = { '$ref' => schema_name }
60
+ schemer = JSONSchemer.schema(schema, ref_resolver:)
61
+ errors = schemer.validate(object)
62
+ raise_error!(schema_name, object, errors) if errors.any?
63
+
64
+ logger.debug { "Object #{object} conforms to #{schema_name}" }
65
+ end
66
+
67
+ private
68
+
69
+ # The resolver to use to resolve JSON schema references
70
+ # @return [ResolveSchemaRef]
71
+ # @api private
72
+ def ref_resolver = @ref_resolver ||= SheetsV4::ValidateApiObjects::ResolveSchemaRef.new(logger:)
73
+
74
+ # Raise an error when the object does not conform to the schema
75
+ # @return [void]
76
+ # @raise [RuntimeError]
77
+ # @api private
78
+ def raise_error!(schema_name, object, errors)
79
+ error = errors.first['error']
80
+ error_message = "Object #{object} does not conform to #{schema_name}: #{error}"
81
+ logger.error(error_message)
82
+ raise error_message
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SheetsV4
4
+ # Validate API Objects against the Google Discovery API
5
+ #
6
+ # @example
7
+ # logger = Logger.new(STDOUT, :level => Logger::ERROR)
8
+ # schema_name = 'batch_update_spreadsheet_request'
9
+ # object = { 'requests' => [] }
10
+ # SheetsV4::ValidateApiObjects::Validator.new(logger:).call(schema_name:, object:)
11
+ #
12
+ # @api public
13
+ #
14
+ module ValidateApiObjects; end
15
+ end
16
+
17
+ require_relative 'validate_api_objects/load_schemas'
18
+ require_relative 'validate_api_objects/resolve_schema_ref'
19
+ require_relative 'validate_api_objects/traverse_object_tree'
20
+ require_relative 'validate_api_objects/validate'
@@ -2,5 +2,5 @@
2
2
 
3
3
  module SheetsV4
4
4
  # The version of this gem
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
data/lib/sheets_v4.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require_relative 'sheets_v4/version'
4
4
  require_relative 'sheets_v4/color'
5
5
  require_relative 'sheets_v4/credential_creator'
6
- require_relative 'sheets_v4/validate_api_object'
6
+ require_relative 'sheets_v4/validate_api_objects'
7
7
 
8
8
  require 'google/apis/sheets_v4'
9
9
  require 'json'
@@ -19,12 +19,16 @@ module SheetsV4
19
19
  #
20
20
  # Simplifies creating and configuring a the credential.
21
21
  #
22
- # @example using the crednetial in `~/.google-api-credential`
22
+ # @example using the credential in `~/.google-api-credential`
23
23
  # SheetsV4.sheets_service
24
24
  #
25
25
  # @example using a credential passed in as a string
26
- # credential_source = File.read(File.join(Dir.home, '.credential'))
27
- # SheetsV4.sheets_service(credential_source:
26
+ # credential_source = File.read(File.expand_path('~/.google-api-credential.json'))
27
+ # SheetsV4.sheets_service(credential_source:)
28
+ #
29
+ # @example using a credential passed in as an IO
30
+ # credential_source = File.open(File.expand_path('~/.google-api-credential.json'))
31
+ # SheetsV4.sheets_service(credential_source:)
28
32
  #
29
33
  # @param credential_source [nil, String, IO, Google::Auth::*] may
30
34
  # be either an already constructed credential, the credential read into a String or
@@ -50,10 +54,10 @@ module SheetsV4
50
54
  # Validate the object using the named JSON schema
51
55
  #
52
56
  # The JSON schemas are loaded from the Google Disocvery API. The schemas names are
53
- # returned by `SheetsV4.api_object_schemas.keys`.
57
+ # returned by `SheetsV4.api_object_schema_names`.
54
58
  #
55
59
  # @example
56
- # schema_name = 'BatchUpdateSpreadsheetRequest'
60
+ # schema_name = 'batch_update_spreadsheet_request'
57
61
  # object = { 'requests' => [] }
58
62
  # SheetsV4.validate_api_object(schema_name:, object:)
59
63
  #
@@ -66,7 +70,18 @@ module SheetsV4
66
70
  # @return [void]
67
71
  #
68
72
  def self.validate_api_object(schema_name:, object:, logger: Logger.new(nil))
69
- ValidateApiObject.new(logger).call(schema_name, object)
73
+ SheetsV4::ValidateApiObjects::Validate.new(logger:).call(schema_name:, object:)
74
+ end
75
+
76
+ # List the names of the schemas available to use in the Google Sheets API
77
+ #
78
+ # @example List the name of the schemas available
79
+ # SheetsV4.api_object_schema_names #=> ["add_banding_request", "add_banding_response", ...]
80
+ #
81
+ # @return [Array<String>] the names of the schemas available
82
+ #
83
+ def self.api_object_schema_names(logger: Logger.new(nil))
84
+ SheetsV4::ValidateApiObjects::LoadSchemas.new(logger:).call.keys.sort
70
85
  end
71
86
 
72
87
  # Given the name of the color, return a Google Sheets API color object
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sheets_v4
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-30 00:00:00.000000000 Z
11
+ date: 2023-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -258,10 +258,15 @@ files:
258
258
  - Rakefile
259
259
  - examples/README.md
260
260
  - examples/set_background_color
261
+ - examples/set_background_color2
261
262
  - lib/sheets_v4.rb
262
263
  - lib/sheets_v4/color.rb
263
264
  - lib/sheets_v4/credential_creator.rb
264
- - lib/sheets_v4/validate_api_object.rb
265
+ - lib/sheets_v4/validate_api_objects.rb
266
+ - lib/sheets_v4/validate_api_objects/load_schemas.rb
267
+ - lib/sheets_v4/validate_api_objects/resolve_schema_ref.rb
268
+ - lib/sheets_v4/validate_api_objects/traverse_object_tree.rb
269
+ - lib/sheets_v4/validate_api_objects/validate.rb
265
270
  - lib/sheets_v4/version.rb
266
271
  homepage: https://github.com/main-branch/sheets_v4
267
272
  licenses:
@@ -1,174 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json_schemer'
4
-
5
- module SheetsV4
6
- # Validate objects against a Google Sheets API request object schema
7
- #
8
- # @api public
9
- #
10
- class ValidateApiObject
11
- # Create a new validator
12
- #
13
- # By default, a nil logger is used. This means that no messages are logged.
14
- #
15
- # @example
16
- # logger = Logger.new(STDOUT, :level => Logger::INFO)
17
- # validator = SheetsV4::ValidateApiObject.new(logger)
18
- #
19
- # @param logger [Logger] the logger to use
20
- #
21
- def initialize(logger = Logger.new(nil))
22
- @logger = logger
23
- end
24
-
25
- # The logger to use internally
26
- #
27
- # Validation errors are logged at the error level. Other messages are logged
28
- # at the debug level.
29
- #
30
- # @example
31
- # logger = Logger.new(STDOUT, :level => Logger::INFO)
32
- # validator = SheetsV4::ValidateApiObject.new(logger)
33
- # validator.logger == logger # => true
34
- # validator.logger.debug { "Debug message" }
35
- #
36
- # @return [Logger]
37
- #
38
- attr_reader :logger
39
-
40
- # Validate the object using the JSON schema named schema_name
41
- #
42
- # @example
43
- # schema_name = 'BatchUpdateSpreadsheetRequest'
44
- # object = { 'requests' => [] }
45
- # validator = SheetsV4::ValidateApiObject.new
46
- # validator.call(schema_name, object)
47
- #
48
- # @param schema_name [String] the name of the schema to validate against
49
- # @param object [Object] the object to validate
50
- #
51
- # @raise [RuntimeError] if the object does not conform to the schema
52
- #
53
- # @return [void]
54
- #
55
- def call(schema_name, object)
56
- logger.debug { "Validating #{object} against #{schema_name}" }
57
-
58
- schema = { '$ref' => schema_name }
59
- schemer = JSONSchemer.schema(schema, ref_resolver:)
60
- errors = schemer.validate(object)
61
- raise_error!(schema_name, object, errors) if errors.any?
62
-
63
- logger.debug { "Object #{object} conforms to #{schema_name}" }
64
- end
65
-
66
- private
67
-
68
- # The resolver to use to resolve JSON schema references
69
- # @return [SchemaRefResolver]
70
- # @api private
71
- def ref_resolver = @ref_resolver ||= SchemaRefResolver.new(logger)
72
-
73
- # Raise an error when the object does not conform to the schema
74
- # @return [void]
75
- # @raise [RuntimeError]
76
- # @api private
77
- def raise_error!(schema_name, object, errors)
78
- error = errors.first['error']
79
- error_message = "Object #{object} does not conform to #{schema_name}: #{error}"
80
- logger.error(error_message)
81
- raise error_message
82
- end
83
- end
84
-
85
- # Resolve JSON schema references to Google Sheets API schemas
86
- #
87
- # Uses the Google Discovery API to get the schemas. This is an implementation
88
- # detail used to interact with JSONSchemer.
89
- #
90
- # @api private
91
- #
92
- class SchemaRefResolver
93
- # Create a new schema resolver
94
- #
95
- # @param logger [Logger] the logger to use
96
- #
97
- # @api private
98
- #
99
- def initialize(logger)
100
- @logger = logger
101
- end
102
-
103
- # The logger to use internally
104
- #
105
- # Currently, only info messages are logged.
106
- #
107
- # @return [Logger]
108
- #
109
- # @api private
110
- #
111
- attr_reader :logger
112
-
113
- # Resolve a JSON schema reference
114
- #
115
- # @param ref [String] the reference to resolve usually in the form "#/definitions/schema_name"
116
- #
117
- # @return [Hash] the schema object as a hash
118
- #
119
- # @api private
120
- #
121
- def call(ref)
122
- schema_name = ref.path[1..]
123
- logger.debug { "Reading schema #{schema_name}" }
124
- schema_object = self.class.api_object_schemas[schema_name]
125
- raise "Schema for #{ref} not found" unless schema_object
126
-
127
- schema_object.to_h.tap do |schema|
128
- schema['unevaluatedProperties'] = false
129
- end
130
- end
131
-
132
- # A hash of schemas keyed by the schema name loaded from the Google Discovery API
133
- #
134
- # @example
135
- # SheetsV4.api_object_schemas #=> { 'PersonSchema' => { 'type' => 'object', ... } ... }
136
- #
137
- # @return [Hash<String, Object>] a hash of schemas keyed by schema name
138
- #
139
- def self.api_object_schemas
140
- schema_load_semaphore.synchronize { @api_object_schemas ||= load_api_object_schemas }
141
- end
142
-
143
- # Validate
144
- # A mutex used to synchronize access to the schemas so they are only loaded
145
- # once.
146
- #
147
- @schema_load_semaphore = Thread::Mutex.new
148
-
149
- class << self
150
- # A mutex used to synchronize access to the schemas so they are only loaded once
151
- #
152
- # @return [Thread::Mutex]
153
- #
154
- # @api private
155
- #
156
- attr_reader :schema_load_semaphore
157
- end
158
-
159
- # Load the schemas from the Google Discovery API
160
- #
161
- # @return [Hash<String, Object>] a hash of schemas keyed by schema name
162
- #
163
- # @api private
164
- #
165
- def self.load_api_object_schemas
166
- source = 'https://sheets.googleapis.com/$discovery/rest?version=v4'
167
- resp = Net::HTTP.get_response(URI.parse(source))
168
- data = resp.body
169
- JSON.parse(data)['schemas'].tap do |schemas|
170
- schemas.each { |_name, schema| schema['unevaluatedProperties'] = false }
171
- end
172
- end
173
- end
174
- end