sheets_v4 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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