discovery_v1 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/CHANGELOG.md +13 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/Rakefile +101 -0
- data/examples/list_schemas +17 -0
- data/examples/validate_object +32 -0
- data/lib/discovery_v1/google_extensions/rest_description.rb +68 -0
- data/lib/discovery_v1/google_extensions.rb +3 -0
- data/lib/discovery_v1/validation/load_schemas.rb +225 -0
- data/lib/discovery_v1/validation/resolve_schema_ref.rb +85 -0
- data/lib/discovery_v1/validation/traverse_object_tree.rb +80 -0
- data/lib/discovery_v1/validation/validate_object.rb +99 -0
- data/lib/discovery_v1/validation.rb +21 -0
- data/lib/discovery_v1/version.rb +6 -0
- data/lib/discovery_v1.rb +69 -0
- data/sig/discovery_v1.rbs +4 -0
- metadata +269 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/inflector'
|
|
5
|
+
|
|
6
|
+
module DiscoveryV1
|
|
7
|
+
module Validation
|
|
8
|
+
# Load the Google Discovery V1 API description for the Discovery V1 API
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# logger = Logger.new(STDOUT, :level => Logger::ERROR)
|
|
12
|
+
# schemas = DiscoveryV1::Validation::LoadSchemas.new(logger:).call
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
#
|
|
16
|
+
class LoadSchemas
|
|
17
|
+
# Loads schemas for the Discovery V1 API object from the Google Discovery V1 API
|
|
18
|
+
#
|
|
19
|
+
# By default, a nil logger is used. This means that nothing is logged.
|
|
20
|
+
#
|
|
21
|
+
# The schemas are only loaded once and cached.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# schema_loader = DiscoveryV1::Validation::LoadSchemas.new
|
|
25
|
+
#
|
|
26
|
+
# @param rest_description [Google::Apis::DiscoveryV1::RestDescription]
|
|
27
|
+
# the api description to load schemas from
|
|
28
|
+
# @param logger [Logger] the logger to use
|
|
29
|
+
#
|
|
30
|
+
def initialize(rest_description:, logger: Logger.new(nil))
|
|
31
|
+
@rest_description = rest_description
|
|
32
|
+
@logger = logger
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The Google Discovery V1 API description to load schemas from
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# rest_description = DiscoveryV1.discovery_service.get_rest_description('sheets', 'v4')
|
|
39
|
+
# loader = DiscoveryV1::Validation::LoadSchemas.new(rest_description:)
|
|
40
|
+
# loader.rest_description == rest_description # => true
|
|
41
|
+
#
|
|
42
|
+
# @return [Google::Apis::DiscoveryV1::RestDescription]
|
|
43
|
+
#
|
|
44
|
+
attr_reader :rest_description
|
|
45
|
+
|
|
46
|
+
# The logger to use internally for logging errors
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# logger = Logger.new(STDOUT, :level => Logger::INFO)
|
|
50
|
+
# schema_loader = DiscoveryV1::Validation::LoadSchemas.new(logger)
|
|
51
|
+
# schema_loader.logger == logger # => true
|
|
52
|
+
#
|
|
53
|
+
# @return [Logger]
|
|
54
|
+
#
|
|
55
|
+
attr_reader :logger
|
|
56
|
+
|
|
57
|
+
# A hash of schemas keyed by schema name loaded from the Google Discovery V1 API
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# DiscoveryV1.api_object_schemas #=> { 'PersonSchema' => { 'type' => 'object', ... } ... }
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash<String, Object>] a hash of schemas keyed by schema name
|
|
63
|
+
#
|
|
64
|
+
def call
|
|
65
|
+
self.class.load_schemas_mutex.synchronize do
|
|
66
|
+
self.class.schemas(rest_description:) ||
|
|
67
|
+
self.class.memoize_schemas(rest_description:, schemas: load_api_schemas)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# A mutex used to synchronize access to the schemas so they are only loaded
|
|
74
|
+
# once
|
|
75
|
+
#
|
|
76
|
+
# @return [Thread::Mutex]
|
|
77
|
+
#
|
|
78
|
+
@load_schemas_mutex = Thread::Mutex.new
|
|
79
|
+
|
|
80
|
+
# Memoization cache that stores the schemas for each rest_description
|
|
81
|
+
#
|
|
82
|
+
# The cache is a hash where:
|
|
83
|
+
# * The key is the canonical name of the rest_description
|
|
84
|
+
# * The value is a hash of schemas keyed by schema name
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash<String, Object] a hash of schemas keyed by schema name
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
#
|
|
90
|
+
@schemas_cache = {}
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
# A mutex used to synchronize access to the schemas so they are only loaded once
|
|
94
|
+
#
|
|
95
|
+
# @return [Thread::Mutex]
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
98
|
+
#
|
|
99
|
+
attr_reader :load_schemas_mutex
|
|
100
|
+
|
|
101
|
+
# The memoized schemas for the given rest_description or nil
|
|
102
|
+
#
|
|
103
|
+
# @param rest_description [Google::Apis::DiscoveryV1::RestDescription] the
|
|
104
|
+
# rest_description to get the schemas for
|
|
105
|
+
#
|
|
106
|
+
# @return [Hash<String, Object>, nil] a hash of schemas keyed by schema name
|
|
107
|
+
#
|
|
108
|
+
# @api private
|
|
109
|
+
#
|
|
110
|
+
def schemas(rest_description:)
|
|
111
|
+
@schemas_cache[rest_description.canonical_name]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Memoize the schemas for the given rest_description returning the given schemas
|
|
115
|
+
#
|
|
116
|
+
# @param rest_description [Google::Apis::DiscoveryV1::RestDescription] the
|
|
117
|
+
# rest_description to memoize the schemas for
|
|
118
|
+
# @param schemas [Hash<String, Object>] a hash of schemas keyed by schema name
|
|
119
|
+
#
|
|
120
|
+
# @return [Hash<String, Object>] the given schemas
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
123
|
+
#
|
|
124
|
+
def memoize_schemas(rest_description:, schemas:)
|
|
125
|
+
@schemas_cache[rest_description.canonical_name] = schemas
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Clear the memoization cache (intended for testing)
|
|
129
|
+
#
|
|
130
|
+
# @return [void]
|
|
131
|
+
#
|
|
132
|
+
# @api private
|
|
133
|
+
#
|
|
134
|
+
def clear_schemas_cache
|
|
135
|
+
@schemas_cache = {}
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Load the schemas from the Google Discovery V1 API
|
|
140
|
+
#
|
|
141
|
+
# @return [Hash<String, Object>] a hash of schemas keyed by schema name
|
|
142
|
+
#
|
|
143
|
+
# @api private
|
|
144
|
+
#
|
|
145
|
+
def load_api_schemas
|
|
146
|
+
source = "https://#{rest_description.name}.googleapis.com/$discovery/rest?version=#{rest_description.version}"
|
|
147
|
+
http_response = Net::HTTP.get_response(URI.parse(source))
|
|
148
|
+
raise_error(http_response) if http_response.code != '200'
|
|
149
|
+
|
|
150
|
+
data = http_response.body
|
|
151
|
+
JSON.parse(data)['schemas'].tap { |schemas| post_process_schemas(schemas) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Log an error and raise a RuntimeError based on the HTTP response code
|
|
155
|
+
# @param http_response [Net::HTTPResponse] the HTTP response
|
|
156
|
+
# @return [void]
|
|
157
|
+
# @raise [RuntimeError]
|
|
158
|
+
# @api private
|
|
159
|
+
def raise_error(http_response)
|
|
160
|
+
message = "HTTP Error '#{http_response.code}' loading schemas from '#{http_response.uri}'"
|
|
161
|
+
logger.error(message)
|
|
162
|
+
raise message
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
REF_KEY = '$ref'
|
|
166
|
+
|
|
167
|
+
# A visitor for the schema object tree that fixes up the tree as it goes
|
|
168
|
+
# @return [void]
|
|
169
|
+
# @api private
|
|
170
|
+
def schema_visitor(path:, object:)
|
|
171
|
+
return unless object.is_a? Hash
|
|
172
|
+
|
|
173
|
+
convert_schema_names_to_snake_case(path, object)
|
|
174
|
+
convert_schema_ids_to_snake_case(path, object)
|
|
175
|
+
add_unevaluated_properties(path, object)
|
|
176
|
+
convert_property_names_to_snake_case(path, object)
|
|
177
|
+
convert_ref_values_to_snake_case(path, object)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Convert schema names to snake case
|
|
181
|
+
# @return [void]
|
|
182
|
+
# @api private
|
|
183
|
+
def convert_schema_names_to_snake_case(path, object)
|
|
184
|
+
object.transform_keys!(&:underscore) if path.empty?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Convert schema IDs to snake case
|
|
188
|
+
# @return [void]
|
|
189
|
+
# @api private
|
|
190
|
+
def convert_schema_ids_to_snake_case(path, object)
|
|
191
|
+
object['id'] = object['id'].underscore if object.key?('id') && path.size == 1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Add 'unevaluatedProperties: false' to all schemas
|
|
195
|
+
# @return [void]
|
|
196
|
+
# @api private
|
|
197
|
+
def add_unevaluated_properties(path, object)
|
|
198
|
+
object['unevaluatedProperties'] = false if path.size == 1
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Convert object property names to snake case
|
|
202
|
+
# @return [void]
|
|
203
|
+
# @api private
|
|
204
|
+
def convert_property_names_to_snake_case(path, object)
|
|
205
|
+
object.transform_keys!(&:underscore) if path[-1] == 'properties'
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Convert reference values to snake case
|
|
209
|
+
# @return [void]
|
|
210
|
+
# @api private
|
|
211
|
+
def convert_ref_values_to_snake_case(path, object)
|
|
212
|
+
object[REF_KEY] = object[REF_KEY].underscore if object.key?(REF_KEY) && path[-1] != 'properties'
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Traverse the schema object tree and apply the schema visitor to each node
|
|
216
|
+
# @return [void]
|
|
217
|
+
# @api private
|
|
218
|
+
def post_process_schemas(schemas)
|
|
219
|
+
DiscoveryV1::Validation::TraverseObjectTree.call(
|
|
220
|
+
object: schemas, visitor: ->(path:, object:) { schema_visitor(path:, object:) }
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiscoveryV1
|
|
4
|
+
module Validation
|
|
5
|
+
# Resolve a JSON schema reference to a Google Discovery V1 API schema
|
|
6
|
+
#
|
|
7
|
+
# This class uses the Google Discovery V1 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 V1 API and returning the schema object (as a Hash).
|
|
10
|
+
#
|
|
11
|
+
# This means that `{ "$ref": "cell_data" }` is resolved by returning
|
|
12
|
+
# `DiscoveryV1::Validation::LoadSchemas.new(logger:).call['cell_data']`.
|
|
13
|
+
#
|
|
14
|
+
# An RuntimeError is raised if `DiscoveryV1::Validation::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 = DiscoveryV1::Validation::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 rest_description [Google::Apis::DiscoveryV1::RestDescription] the api description to load schemas from
|
|
37
|
+
# @param logger [Logger] the logger to use
|
|
38
|
+
#
|
|
39
|
+
# @api private
|
|
40
|
+
#
|
|
41
|
+
def initialize(rest_description:, logger: Logger.new(nil))
|
|
42
|
+
@rest_description = rest_description
|
|
43
|
+
@logger = logger
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The Google Discovery V1 API description to load schemas from
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# rest_description = DiscoveryV1.discovery_service.get_rest_description('sheets', 'v4')
|
|
50
|
+
# resolver = DiscoveryV1::Validation::ResolveSchemaRef.new(rest_description:)
|
|
51
|
+
# resolver.rest_description == rest_description # => true
|
|
52
|
+
#
|
|
53
|
+
# @return [Google::Apis::DiscoveryV1::RestDescription]
|
|
54
|
+
#
|
|
55
|
+
attr_reader :rest_description
|
|
56
|
+
|
|
57
|
+
# The logger to use internally
|
|
58
|
+
#
|
|
59
|
+
# Currently, only debug messages are logged.
|
|
60
|
+
#
|
|
61
|
+
# @return [Logger]
|
|
62
|
+
#
|
|
63
|
+
# @api private
|
|
64
|
+
#
|
|
65
|
+
attr_reader :logger
|
|
66
|
+
|
|
67
|
+
# Resolve a JSON schema reference
|
|
68
|
+
#
|
|
69
|
+
# @param ref [URI] the reference to resolve usually in the form "json-schemer://schema/[name]"
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] the schema object as a hash
|
|
72
|
+
#
|
|
73
|
+
# @api private
|
|
74
|
+
#
|
|
75
|
+
def call(ref)
|
|
76
|
+
schema_name = ref.path[1..]
|
|
77
|
+
logger.debug { "Reading schema #{schema_name}" }
|
|
78
|
+
schemas = DiscoveryV1::Validation::LoadSchemas.new(rest_description:, logger:).call
|
|
79
|
+
schemas[schema_name].tap do |schema_object|
|
|
80
|
+
raise "Schema for #{ref} not found" unless schema_object
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiscoveryV1
|
|
4
|
+
module Validation
|
|
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
|
+
# @example
|
|
16
|
+
# # In the examples below assume the elided code is the following:
|
|
17
|
+
# visitor = -> (path:, object:) { puts "path: #{path}, object: #{obj}" }
|
|
18
|
+
# DiscoveryV1::Validation::TraverseObjectTree.call(object:, visitor:)
|
|
19
|
+
#
|
|
20
|
+
# @example Given a simple object (not very exciting)
|
|
21
|
+
# object = 1
|
|
22
|
+
# ...
|
|
23
|
+
# #=> path: [], object: 1
|
|
24
|
+
#
|
|
25
|
+
# @example Given an array
|
|
26
|
+
# object = [1, 2, 3]
|
|
27
|
+
# ...
|
|
28
|
+
# #=> path: [], object: [1, 2, 3]
|
|
29
|
+
# #=> path: [0], object: 1
|
|
30
|
+
# #=> path: [1], object: 2
|
|
31
|
+
# #=> path: [2], object: 3
|
|
32
|
+
#
|
|
33
|
+
# @example Given a hash
|
|
34
|
+
# object = { name: 'James', age: 42 }
|
|
35
|
+
# ...
|
|
36
|
+
# #=> path: [], object: { name: 'James', age: 42 }
|
|
37
|
+
# #=> path: [:name], object: James
|
|
38
|
+
# #=> path: [:age], object: 42
|
|
39
|
+
#
|
|
40
|
+
# @example Given an array of hashes
|
|
41
|
+
# object = [{ name: 'James', age: 42 }, { name: 'Jane', age: 43 }]
|
|
42
|
+
# ...
|
|
43
|
+
# #=> path: [], object: [{ name: 'James', age: 42 }, { name: 'Jane', age: 43 }]
|
|
44
|
+
# #=> path: [0], object: { name: 'James', age: 42 }
|
|
45
|
+
# #=> path: [0, :name], object: James
|
|
46
|
+
# #=> path: [0, :age], object: 42
|
|
47
|
+
# #=> path: [1], object: { name: 'Jane', age: 43 }
|
|
48
|
+
# #=> path: [1, :name], object: Jane
|
|
49
|
+
# #=> path: [1, :age], object: 43
|
|
50
|
+
#
|
|
51
|
+
# @example Given a hash of hashes
|
|
52
|
+
# object = { person1: { name: 'James', age: 42 }, person2: { name: 'Jane', age: 43 } }
|
|
53
|
+
# ...
|
|
54
|
+
# #=> path: [], object: { person1: { name: 'James', age: 42 }, person2: { name: 'Jane', age: 43 } }
|
|
55
|
+
# #=> path: [:person1], object: { name: 'James', age: 42 }
|
|
56
|
+
# #=> path: [:person1, :name], object: James
|
|
57
|
+
# #=> path: [:person1, :age], object: 42
|
|
58
|
+
# #=> path: [:person2], object: { name: 'Jane', age: 43 }
|
|
59
|
+
# #=> path: [:person2, :name], object: Jane
|
|
60
|
+
# #=> path: [:person2, :age], object: 43
|
|
61
|
+
#
|
|
62
|
+
# @param path [Array] the path to the object
|
|
63
|
+
# @param object [Object] the object to visit
|
|
64
|
+
# @param visitor [#call] the visitor to call for each object
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
#
|
|
68
|
+
# @api public
|
|
69
|
+
#
|
|
70
|
+
def self.call(object:, visitor:, path: [])
|
|
71
|
+
visitor&.call(path:, object:)
|
|
72
|
+
if object.is_a? Hash
|
|
73
|
+
object.each { |k, obj| call(path: (path + [k]), object: obj, visitor:) }
|
|
74
|
+
elsif object.is_a? Array
|
|
75
|
+
object.each_with_index { |obj, k| call(path: (path + [k]), object: obj, visitor:) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json_schemer'
|
|
4
|
+
|
|
5
|
+
module DiscoveryV1
|
|
6
|
+
module Validation
|
|
7
|
+
# Validate objects against a Google Discovery V1 API request object schema
|
|
8
|
+
#
|
|
9
|
+
# @api public
|
|
10
|
+
#
|
|
11
|
+
class ValidateObject
|
|
12
|
+
# Create a new api object validator
|
|
13
|
+
#
|
|
14
|
+
# By default, a nil logger is used. This means that no messages are logged.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# validator = DiscoveryV1::Validation::ValidateObject.new
|
|
18
|
+
#
|
|
19
|
+
# @param logger [Logger] the logger to use
|
|
20
|
+
#
|
|
21
|
+
def initialize(rest_description:, logger: Logger.new(nil))
|
|
22
|
+
@rest_description = rest_description
|
|
23
|
+
@logger = logger
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The Google Discovery V1 API description containing schemas to use for validation
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# rest_description = DiscoveryV1.discovery_service.get_rest_description('sheets', 'v4')
|
|
30
|
+
# validator = DiscoveryV1::Validation::ValidateObject.new(rest_description:)
|
|
31
|
+
# validator.rest_description == rest_description # => true
|
|
32
|
+
#
|
|
33
|
+
# @return [Google::Apis::DiscoveryV1::RestDescription]
|
|
34
|
+
#
|
|
35
|
+
attr_reader :rest_description
|
|
36
|
+
|
|
37
|
+
# The logger to use internally
|
|
38
|
+
#
|
|
39
|
+
# Validation errors are logged at the error level. Other messages are logged
|
|
40
|
+
# at the debug level.
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# logger = Logger.new(STDOUT, :level => Logger::INFO)
|
|
44
|
+
# validator = DiscoveryV1::Validation::ValidateObject.new(logger)
|
|
45
|
+
# validator.logger == logger # => true
|
|
46
|
+
# validator.logger.debug { "Debug message" }
|
|
47
|
+
#
|
|
48
|
+
# @return [Logger]
|
|
49
|
+
#
|
|
50
|
+
attr_reader :logger
|
|
51
|
+
|
|
52
|
+
# Validate the object using the JSON schema named schema_name
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# schema_name = 'batch_update_spreadsheet_request'
|
|
56
|
+
# object = { 'requests' => [] }
|
|
57
|
+
# validator = DiscoveryV1::Validation::ValidateObject.new
|
|
58
|
+
# validator.call(schema_name:, object:)
|
|
59
|
+
#
|
|
60
|
+
# @param schema_name [String] the name of the schema to validate against
|
|
61
|
+
# @param object [Object] the object to validate
|
|
62
|
+
#
|
|
63
|
+
# @raise [RuntimeError] if the object does not conform to the schema
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
#
|
|
67
|
+
def call(schema_name:, object:)
|
|
68
|
+
logger.debug { "Validating #{object} against #{schema_name}" }
|
|
69
|
+
|
|
70
|
+
schema = { '$ref' => schema_name }
|
|
71
|
+
schemer = JSONSchemer.schema(schema, ref_resolver:)
|
|
72
|
+
errors = schemer.validate(object)
|
|
73
|
+
raise_error!(schema_name, object, errors) if errors.any?
|
|
74
|
+
|
|
75
|
+
logger.debug { "Object #{object} conforms to #{schema_name}" }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# The resolver to use to resolve JSON schema references
|
|
81
|
+
# @return [ResolveSchemaRef]
|
|
82
|
+
# @api private
|
|
83
|
+
def ref_resolver
|
|
84
|
+
@ref_resolver ||= DiscoveryV1::Validation::ResolveSchemaRef.new(rest_description:, logger:)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Raise an error when the object does not conform to the schema
|
|
88
|
+
# @return [void]
|
|
89
|
+
# @raise [RuntimeError]
|
|
90
|
+
# @api private
|
|
91
|
+
def raise_error!(schema_name, _object, errors)
|
|
92
|
+
error = errors.first['error']
|
|
93
|
+
error_message = "Object does not conform to #{schema_name}: #{error}"
|
|
94
|
+
logger.error(error_message)
|
|
95
|
+
raise error_message
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiscoveryV1
|
|
4
|
+
# Validate API Objects against the Google Discovery V1 API
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# discovery_service = DiscoveryV1.discovery_service
|
|
8
|
+
# rest_description = discovery_service.get_rest_description('sheets', 'v4')
|
|
9
|
+
# schema_name = 'batch_update_spreadsheet_request'
|
|
10
|
+
# object = { 'requests' => [] }
|
|
11
|
+
# DiscoveryV1::Validation::ValidateObject.new(rest_description:).call(schema_name:, object:)
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
#
|
|
15
|
+
module Validation; end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require_relative 'validation/load_schemas'
|
|
19
|
+
require_relative 'validation/resolve_schema_ref'
|
|
20
|
+
require_relative 'validation/traverse_object_tree'
|
|
21
|
+
require_relative 'validation/validate_object'
|
data/lib/discovery_v1.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'google/apis/discovery_v1'
|
|
4
|
+
|
|
5
|
+
require_relative 'discovery_v1/version'
|
|
6
|
+
require_relative 'discovery_v1/validation'
|
|
7
|
+
|
|
8
|
+
# Unofficial helpers for the Google Discovery V1 API
|
|
9
|
+
#
|
|
10
|
+
# @api public
|
|
11
|
+
#
|
|
12
|
+
module DiscoveryV1
|
|
13
|
+
class << self
|
|
14
|
+
# Create a new Google::Apis::DiscoveryV1::DiscoveryService object
|
|
15
|
+
#
|
|
16
|
+
# A credential is not needed to use the DiscoveryService.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# DiscoveryV1.discovery_service
|
|
20
|
+
#
|
|
21
|
+
# @return [Google::Apis::DiscoveryV1::DiscoveryService] a new DiscoveryService instance
|
|
22
|
+
#
|
|
23
|
+
def discovery_service
|
|
24
|
+
Google::Apis::DiscoveryV1::DiscoveryService.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @!group Validation
|
|
28
|
+
|
|
29
|
+
# Validate the object using the named JSON schema
|
|
30
|
+
#
|
|
31
|
+
# The JSON schemas are loaded from the Google Disocvery API. The schemas names are
|
|
32
|
+
# returned by `DiscoveryV1.api_object_schema_names`.
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# schema_name = 'batch_update_spreadsheet_request'
|
|
36
|
+
# object = { 'requests' => [] }
|
|
37
|
+
# DiscoveryV1.validate_api_object(schema_name:, object:)
|
|
38
|
+
#
|
|
39
|
+
# @param rest_description [Google::Apis::DiscoveryV1::RestDescription] the Google Discovery V1 API rest description
|
|
40
|
+
# @param schema_name [String] the name of the schema to validate against
|
|
41
|
+
# @param object [Object] the object to validate
|
|
42
|
+
# @param logger [Logger] the logger to use for logging error, info, and debug message
|
|
43
|
+
#
|
|
44
|
+
# @raise [RuntimeError] if the object does not conform to the schema
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
47
|
+
#
|
|
48
|
+
def validate_object(rest_description:, schema_name:, object:, logger: Logger.new(nil))
|
|
49
|
+
DiscoveryV1::Validation::ValidateObject.new(rest_description:, logger:).call(schema_name:, object:)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# List the names of the schemas available to use in the Google Discovery V1 API
|
|
53
|
+
#
|
|
54
|
+
# @example List the name of the schemas available
|
|
55
|
+
# rest_description = DiscoveryV1.discovery_service.get_rest_api('sheets', 'v4')
|
|
56
|
+
# DiscoveryV1.api_object_schema_names #=> ["add_banding_request", "add_banding_response", ...]
|
|
57
|
+
#
|
|
58
|
+
# @param rest_description [Google::Apis::DiscoveryV1::RestDescription] the Google Discovery V1 API rest description
|
|
59
|
+
# @param logger [Logger] the logger to use for logging error, info, and debug message
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<String>] the names of the schemas available
|
|
62
|
+
#
|
|
63
|
+
def object_schema_names(rest_description:, logger: Logger.new(nil))
|
|
64
|
+
DiscoveryV1::Validation::LoadSchemas.new(rest_description:, logger:).call.keys.sort
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @!endgroup
|
|
68
|
+
end
|
|
69
|
+
end
|