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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6357385eeb33468356dd974f8fda49f92e09b30b9911a825c562509bf5f70dd
4
- data.tar.gz: a289b95495fd49e8221a89e037168aaae903ca40af2f9167cff60c75c0f28acc
3
+ metadata.gz: ea8bb19ec472d573c796d277e420c088273c087aa9ffe4ebbb63df0a2ffb69ae
4
+ data.tar.gz: 5a2dca82c26a9efdd7584a5c33f4e4d9bfa1f8ac0003cf504d936554e6bb9f7d
5
5
  SHA512:
6
- metadata.gz: 59b81fbfa525ee8f5810d1aeeb3d6168dfffe0ffd1a8c19792d8f27548cdb1b2106748a30f499f8619bd83cd73d773c5647f0b7fd57640c990af2eb7e9d5fd7b
7
- data.tar.gz: '09ffa389430c102e6e4df4370bc42f2e8769399f78cabbfe61441372ed6bf4a87378a124a3c17fe0c7f53a73255fae7f153cd08df67e93589c5bcbb49360166e'
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
- # 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.5.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'
@@ -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
- ValidateApiObject.new(logger).call(schema_name, object)
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.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-09-30 00:00:00.000000000 Z
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/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