sheets_v4 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6357385eeb33468356dd974f8fda49f92e09b30b9911a825c562509bf5f70dd
4
- data.tar.gz: a289b95495fd49e8221a89e037168aaae903ca40af2f9167cff60c75c0f28acc
3
+ metadata.gz: 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