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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +195 -24
- data/examples/set_background_color +6 -4
- data/examples/set_background_color2 +67 -0
- data/lib/sheets_v4/validate_api_objects/load_schemas.rb +155 -0
- data/lib/sheets_v4/validate_api_objects/resolve_schema_ref.rb +72 -0
- data/lib/sheets_v4/validate_api_objects/traverse_object_tree.rb +82 -0
- data/lib/sheets_v4/validate_api_objects/validate.rb +86 -0
- data/lib/sheets_v4/validate_api_objects.rb +20 -0
- data/lib/sheets_v4/version.rb +1 -1
- data/lib/sheets_v4.rb +22 -7
- metadata +8 -3
- data/lib/sheets_v4/validate_api_object.rb +0 -174
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60f10846ff0c3b62cb29c487499eb717897a2b28916606de7d3199fe0424435f
|
4
|
+
data.tar.gz: a6bb345b4379f94dbaa82b266943c144321e3bcf3404db34c1788e766813a766
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
##
|
33
|
+
## Installation
|
26
34
|
|
27
|
-
|
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
|
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
|
-
##
|
65
|
+
## Getting Started
|
44
66
|
|
45
|
-
|
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
|
-
|
48
|
-
bundle add sheets_v4
|
49
|
-
```
|
70
|
+
### Creating a Google Cloud project
|
50
71
|
|
51
|
-
|
72
|
+
Create a Google Cloud project using [these directions](https://developers.google.com/workspace/guides/create-project).
|
52
73
|
|
53
|
-
|
54
|
-
|
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(
|
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(
|
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
|
118
|
-
`rake
|
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
|
-
#
|
47
|
-
#
|
48
|
-
#
|
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'
|
data/lib/sheets_v4/version.rb
CHANGED
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/
|
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
|
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.
|
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.
|
57
|
+
# returned by `SheetsV4.api_object_schema_names`.
|
54
58
|
#
|
55
59
|
# @example
|
56
|
-
# schema_name = '
|
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
|
-
|
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
|
+
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-
|
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/
|
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
|