sheets_v4 0.4.0 → 0.5.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 +8 -0
- 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 +2 -2
- 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: ea8bb19ec472d573c796d277e420c088273c087aa9ffe4ebbb63df0a2ffb69ae
|
4
|
+
data.tar.gz: 5a2dca82c26a9efdd7584a5c33f4e4d9bfa1f8ac0003cf504d936554e6bb9f7d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0676d4dcff6bd008104638a1a593cfcf638ad990d15a6f1a2f71cba44e0a387e5c3e527978cb3fc9e0517ecf9ef35dd8fac2348ac993f58f28147769261d3412
|
7
|
+
data.tar.gz: ab5ae3a72384e45ee10411d1f1a89231ef9092393e686e81b0cb9e7734b216c039589697f2ef5e7d3313b677f5ea022ac8b2135adaa57e7e01c6032a12de0bb0
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,14 @@ 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.5.0 (2023-10-01)
|
8
|
+
|
9
|
+
[Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.4.0..v0.5.0)
|
10
|
+
|
11
|
+
Changes since v0.4.0:
|
12
|
+
|
13
|
+
* b403430 Refactor SheetsV4.validate_api_object (#15)
|
14
|
+
|
7
15
|
## v0.4.0 (2023-09-29)
|
8
16
|
|
9
17
|
[Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.3.0..v0.4.0)
|
@@ -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'
|
@@ -66,7 +66,7 @@ module SheetsV4
|
|
66
66
|
# @return [void]
|
67
67
|
#
|
68
68
|
def self.validate_api_object(schema_name:, object:, logger: Logger.new(nil))
|
69
|
-
|
69
|
+
SheetsV4::ValidateApiObjects::Validate.new(logger:).call(schema_name:, object:)
|
70
70
|
end
|
71
71
|
|
72
72
|
# 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.5.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-02 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
|