json_schemer 1.0.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -6
- data/CHANGELOG.md +25 -0
- data/Gemfile.lock +2 -2
- data/README.md +137 -14
- data/json_schemer.gemspec +1 -1
- data/lib/json_schemer/draft201909/meta.rb +335 -0
- data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
- data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
- data/lib/json_schemer/draft201909/vocab.rb +31 -0
- data/lib/json_schemer/draft202012/meta.rb +361 -0
- data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
- data/lib/json_schemer/draft202012/vocab/content.rb +44 -0
- data/lib/json_schemer/draft202012/vocab/core.rb +154 -0
- data/lib/json_schemer/draft202012/vocab/format_annotation.rb +31 -0
- data/lib/json_schemer/draft202012/vocab/format_assertion.rb +29 -0
- data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
- data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
- data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
- data/lib/json_schemer/draft202012/vocab.rb +103 -0
- data/lib/json_schemer/draft4/meta.rb +155 -0
- data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
- data/lib/json_schemer/draft4/vocab.rb +18 -0
- data/lib/json_schemer/draft6/meta.rb +161 -0
- data/lib/json_schemer/draft6/vocab.rb +16 -0
- data/lib/json_schemer/draft7/meta.rb +178 -0
- data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
- data/lib/json_schemer/draft7/vocab.rb +30 -0
- data/lib/json_schemer/errors.rb +1 -0
- data/lib/json_schemer/format/duration.rb +23 -0
- data/lib/json_schemer/format/email.rb +56 -0
- data/lib/json_schemer/format/json_pointer.rb +18 -0
- data/lib/json_schemer/format.rb +53 -34
- data/lib/json_schemer/keyword.rb +41 -0
- data/lib/json_schemer/location.rb +25 -0
- data/lib/json_schemer/openapi.rb +40 -0
- data/lib/json_schemer/openapi30/document.rb +1673 -0
- data/lib/json_schemer/openapi30/meta.rb +26 -0
- data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
- data/lib/json_schemer/openapi30/vocab.rb +12 -0
- data/lib/json_schemer/openapi31/document.rb +1559 -0
- data/lib/json_schemer/openapi31/meta.rb +128 -0
- data/lib/json_schemer/openapi31/vocab/base.rb +89 -0
- data/lib/json_schemer/openapi31/vocab.rb +18 -0
- data/lib/json_schemer/output.rb +55 -0
- data/lib/json_schemer/result.rb +168 -0
- data/lib/json_schemer/schema.rb +390 -0
- data/lib/json_schemer/version.rb +1 -1
- data/lib/json_schemer.rb +198 -24
- metadata +43 -10
- data/lib/json_schemer/schema/base.rb +0 -677
- data/lib/json_schemer/schema/draft4.json +0 -149
- data/lib/json_schemer/schema/draft4.rb +0 -44
- data/lib/json_schemer/schema/draft6.json +0 -155
- data/lib/json_schemer/schema/draft6.rb +0 -25
- data/lib/json_schemer/schema/draft7.json +0 -172
- data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -0,0 +1,390 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module JSONSchemer
|
3
|
+
class Schema
|
4
|
+
Context = Struct.new(:instance, :dynamic_scope, :adjacent_results, :short_circuit, :access_mode) do
|
5
|
+
def original_instance(instance_location)
|
6
|
+
Hana::Pointer.parse(Location.resolve(instance_location)).reduce(instance) do |obj, token|
|
7
|
+
obj.fetch(obj.is_a?(Array) ? token.to_i : token)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
include Output
|
13
|
+
include Format::JSONPointer
|
14
|
+
|
15
|
+
DEFAULT_SCHEMA = Draft202012::BASE_URI.to_s.freeze
|
16
|
+
SCHEMA_KEYWORD_CLASS = Draft202012::Vocab::Core::Schema
|
17
|
+
VOCABULARY_KEYWORD_CLASS = Draft202012::Vocab::Core::Vocabulary
|
18
|
+
ID_KEYWORD_CLASS = Draft202012::Vocab::Core::Id
|
19
|
+
UNKNOWN_KEYWORD_CLASS = Draft202012::Vocab::Core::UnknownKeyword
|
20
|
+
NOT_KEYWORD_CLASS = Draft202012::Vocab::Applicator::Not
|
21
|
+
PROPERTIES_KEYWORD_CLASS = Draft202012::Vocab::Applicator::Properties
|
22
|
+
DEFAULT_BASE_URI = URI('json-schemer://schema').freeze
|
23
|
+
DEFAULT_FORMATS = {}.freeze
|
24
|
+
DEFAULT_KEYWORDS = {}.freeze
|
25
|
+
DEFAULT_BEFORE_PROPERTY_VALIDATION = [].freeze
|
26
|
+
DEFAULT_AFTER_PROPERTY_VALIDATION = [].freeze
|
27
|
+
DEFAULT_REF_RESOLVER = proc { |uri| raise UnknownRef, uri.to_s }
|
28
|
+
NET_HTTP_REF_RESOLVER = proc { |uri| JSON.parse(Net::HTTP.get(uri)) }
|
29
|
+
RUBY_REGEXP_RESOLVER = proc { |pattern| Regexp.new(pattern) }
|
30
|
+
ECMA_REGEXP_RESOLVER = proc { |pattern| Regexp.new(EcmaRegexp.ruby_equivalent(pattern)) }
|
31
|
+
|
32
|
+
DEFAULT_PROPERTY_DEFAULT_RESOLVER = proc do |instance, property, results_with_tree_validity|
|
33
|
+
results_with_tree_validity = results_with_tree_validity.select(&:last) unless results_with_tree_validity.size == 1
|
34
|
+
annotations = results_with_tree_validity.to_set { |result, _tree_valid| result.annotation }
|
35
|
+
if annotations.size == 1
|
36
|
+
instance[property] = annotations.first.clone
|
37
|
+
true
|
38
|
+
else
|
39
|
+
false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_accessor :base_uri, :meta_schema, :keywords, :keyword_order
|
44
|
+
attr_reader :value, :parent, :root, :parsed
|
45
|
+
attr_reader :vocabulary, :format, :formats, :custom_keywords, :before_property_validation, :after_property_validation, :insert_property_defaults, :property_default_resolver
|
46
|
+
|
47
|
+
def initialize(
|
48
|
+
value,
|
49
|
+
parent = nil,
|
50
|
+
root = self,
|
51
|
+
keyword = nil,
|
52
|
+
base_uri: DEFAULT_BASE_URI,
|
53
|
+
meta_schema: nil,
|
54
|
+
vocabulary: nil,
|
55
|
+
format: true,
|
56
|
+
formats: DEFAULT_FORMATS,
|
57
|
+
keywords: DEFAULT_KEYWORDS,
|
58
|
+
before_property_validation: DEFAULT_BEFORE_PROPERTY_VALIDATION,
|
59
|
+
after_property_validation: DEFAULT_AFTER_PROPERTY_VALIDATION,
|
60
|
+
insert_property_defaults: false,
|
61
|
+
property_default_resolver: DEFAULT_PROPERTY_DEFAULT_RESOLVER,
|
62
|
+
ref_resolver: DEFAULT_REF_RESOLVER,
|
63
|
+
regexp_resolver: 'ruby',
|
64
|
+
output_format: 'classic',
|
65
|
+
resolve_enumerators: false,
|
66
|
+
access_mode: nil
|
67
|
+
)
|
68
|
+
@value = deep_stringify_keys(value)
|
69
|
+
@parent = parent
|
70
|
+
@root = root
|
71
|
+
@keyword = keyword
|
72
|
+
@schema = self
|
73
|
+
@base_uri = base_uri
|
74
|
+
@meta_schema = meta_schema
|
75
|
+
@vocabulary = vocabulary
|
76
|
+
@format = format
|
77
|
+
@formats = formats
|
78
|
+
@custom_keywords = keywords
|
79
|
+
@before_property_validation = Array(before_property_validation)
|
80
|
+
@after_property_validation = Array(after_property_validation)
|
81
|
+
@insert_property_defaults = insert_property_defaults
|
82
|
+
@property_default_resolver = property_default_resolver
|
83
|
+
@original_ref_resolver = ref_resolver
|
84
|
+
@original_regexp_resolver = regexp_resolver
|
85
|
+
@output_format = output_format
|
86
|
+
@resolve_enumerators = resolve_enumerators
|
87
|
+
@access_mode = access_mode
|
88
|
+
@parsed = parse
|
89
|
+
end
|
90
|
+
|
91
|
+
def valid?(instance, **options)
|
92
|
+
validate(instance, :output_format => 'flag', **options).fetch('valid')
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate(instance, output_format: @output_format, resolve_enumerators: @resolve_enumerators, access_mode: @access_mode)
|
96
|
+
instance_location = Location.root
|
97
|
+
context = Context.new(instance, [], nil, (!insert_property_defaults && output_format == 'flag'), access_mode)
|
98
|
+
result = validate_instance(deep_stringify_keys(instance), instance_location, root_keyword_location, context)
|
99
|
+
if insert_property_defaults && result.insert_property_defaults(context, &property_default_resolver)
|
100
|
+
result = validate_instance(deep_stringify_keys(instance), instance_location, root_keyword_location, context)
|
101
|
+
end
|
102
|
+
output = result.output(output_format)
|
103
|
+
resolve_enumerators!(output) if resolve_enumerators
|
104
|
+
output
|
105
|
+
end
|
106
|
+
|
107
|
+
def valid_schema?
|
108
|
+
meta_schema.valid?(value)
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_schema
|
112
|
+
meta_schema.validate(value)
|
113
|
+
end
|
114
|
+
|
115
|
+
def ref(value)
|
116
|
+
resolve_ref(URI.join(base_uri, value))
|
117
|
+
end
|
118
|
+
|
119
|
+
def validate_instance(instance, instance_location, keyword_location, context)
|
120
|
+
context.dynamic_scope.push(self)
|
121
|
+
original_adjacent_results = context.adjacent_results
|
122
|
+
adjacent_results = context.adjacent_results = {}
|
123
|
+
short_circuit = context.short_circuit
|
124
|
+
|
125
|
+
begin
|
126
|
+
return result(instance, instance_location, keyword_location, false) if value == false
|
127
|
+
return result(instance, instance_location, keyword_location, true) if value == true || value.empty?
|
128
|
+
|
129
|
+
valid = true
|
130
|
+
nested = []
|
131
|
+
|
132
|
+
parsed.each do |keyword, keyword_instance|
|
133
|
+
next unless keyword_result = keyword_instance.validate(instance, instance_location, join_location(keyword_location, keyword), context)
|
134
|
+
valid &&= keyword_result.valid
|
135
|
+
return result(instance, instance_location, keyword_location, false) if short_circuit && !valid
|
136
|
+
nested << keyword_result
|
137
|
+
adjacent_results[keyword_instance.class] = keyword_result
|
138
|
+
end
|
139
|
+
|
140
|
+
if custom_keywords.any?
|
141
|
+
custom_keywords.each do |custom_keyword, callable|
|
142
|
+
if value.key?(custom_keyword)
|
143
|
+
[*callable.call(instance, value, instance_location)].each do |custom_keyword_result|
|
144
|
+
custom_keyword_valid = custom_keyword_result == true
|
145
|
+
valid &&= custom_keyword_valid
|
146
|
+
type = custom_keyword_result.is_a?(String) ? custom_keyword_result : custom_keyword
|
147
|
+
details = { 'keyword' => custom_keyword, 'result' => custom_keyword_result }
|
148
|
+
nested << result(instance, instance_location, keyword_location, custom_keyword_valid, :type => type, :details => details)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
result(instance, instance_location, keyword_location, valid, nested)
|
155
|
+
ensure
|
156
|
+
context.dynamic_scope.pop
|
157
|
+
context.adjacent_results = original_adjacent_results
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def resolve_ref(uri)
|
162
|
+
pointer = ''
|
163
|
+
if valid_json_pointer?(uri.fragment)
|
164
|
+
pointer = URI.decode_www_form_component(uri.fragment)
|
165
|
+
uri.fragment = nil
|
166
|
+
end
|
167
|
+
|
168
|
+
lexical_resources = resources.fetch(:lexical)
|
169
|
+
schema = lexical_resources[uri]
|
170
|
+
|
171
|
+
if !schema && uri.fragment.nil?
|
172
|
+
empty_fragment_uri = uri.dup
|
173
|
+
empty_fragment_uri.fragment = ''
|
174
|
+
schema = lexical_resources[empty_fragment_uri]
|
175
|
+
end
|
176
|
+
|
177
|
+
unless schema
|
178
|
+
location_independent_identifier = uri.fragment
|
179
|
+
uri.fragment = nil
|
180
|
+
remote_schema = JSONSchemer.schema(
|
181
|
+
ref_resolver.call(uri) || raise(InvalidRefResolution, uri.to_s),
|
182
|
+
:base_uri => uri,
|
183
|
+
:meta_schema => meta_schema,
|
184
|
+
:format => format,
|
185
|
+
:formats => formats,
|
186
|
+
:keywords => custom_keywords,
|
187
|
+
:before_property_validation => before_property_validation,
|
188
|
+
:after_property_validation => after_property_validation,
|
189
|
+
:property_default_resolver => property_default_resolver,
|
190
|
+
:ref_resolver => ref_resolver,
|
191
|
+
:regexp_resolver => regexp_resolver
|
192
|
+
)
|
193
|
+
remote_uri = remote_schema.base_uri.dup
|
194
|
+
remote_uri.fragment = location_independent_identifier if location_independent_identifier
|
195
|
+
schema = remote_schema.resources.fetch(:lexical).fetch(remote_uri)
|
196
|
+
end
|
197
|
+
|
198
|
+
schema = Hana::Pointer.parse(pointer).reduce(schema) do |obj, token|
|
199
|
+
if obj.is_a?(UNKNOWN_KEYWORD_CLASS)
|
200
|
+
obj.fetch_unknown!(token)
|
201
|
+
elsif obj.parsed.is_a?(Array)
|
202
|
+
obj.parsed.fetch(token.to_i)
|
203
|
+
else
|
204
|
+
obj.parsed.fetch(token)
|
205
|
+
end
|
206
|
+
rescue IndexError
|
207
|
+
raise InvalidRefPointer, pointer
|
208
|
+
end
|
209
|
+
|
210
|
+
schema = schema.unknown_schema! unless schema.is_a?(Schema)
|
211
|
+
|
212
|
+
schema
|
213
|
+
end
|
214
|
+
|
215
|
+
def resolve_regexp(pattern)
|
216
|
+
regexp_resolver.call(pattern) || raise(InvalidRegexpResolution, pattern)
|
217
|
+
end
|
218
|
+
|
219
|
+
def bundle
|
220
|
+
return value unless value.is_a?(Hash)
|
221
|
+
|
222
|
+
id_keyword = meta_schema.id_keyword
|
223
|
+
defs_keyword = meta_schema.defs_keyword
|
224
|
+
|
225
|
+
compound_document = value.dup
|
226
|
+
compound_document[id_keyword] = base_uri.to_s
|
227
|
+
compound_document['$schema'] = meta_schema.base_uri.to_s
|
228
|
+
embedded_resources = compound_document[defs_keyword] = (compound_document[defs_keyword]&.dup || {})
|
229
|
+
|
230
|
+
if compound_document.key?('$ref') && meta_schema.keywords.fetch('$ref').exclusive?
|
231
|
+
compound_document['allOf'] = (compound_document['allOf']&.dup || [])
|
232
|
+
compound_document['allOf'] << { '$ref' => compound_document.delete('$ref') }
|
233
|
+
end
|
234
|
+
|
235
|
+
values = [self]
|
236
|
+
while value = values.shift
|
237
|
+
case value
|
238
|
+
when Schema
|
239
|
+
values << value.parsed
|
240
|
+
when Keyword
|
241
|
+
if value.respond_to?(:ref_uri) && value.respond_to?(:ref_schema)
|
242
|
+
ref_uri = value.ref_uri.dup
|
243
|
+
ref_uri.fragment = nil
|
244
|
+
ref_id = ref_uri.to_s
|
245
|
+
ref_schema = value.ref_schema.root
|
246
|
+
|
247
|
+
next if ref_schema == root || embedded_resources.key?(ref_id)
|
248
|
+
|
249
|
+
embedded_resource = ref_schema.value.dup
|
250
|
+
embedded_resource[id_keyword] = ref_id
|
251
|
+
embedded_resource['$schema'] = ref_schema.meta_schema.base_uri.to_s
|
252
|
+
embedded_resources[ref_id] = embedded_resource
|
253
|
+
|
254
|
+
values << ref_schema
|
255
|
+
else
|
256
|
+
values << value.parsed
|
257
|
+
end
|
258
|
+
when Hash
|
259
|
+
values.concat(value.values)
|
260
|
+
when Array
|
261
|
+
values.concat(value)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
compound_document
|
266
|
+
end
|
267
|
+
|
268
|
+
def absolute_keyword_location
|
269
|
+
# using `equal?` because `URI::Generic#==` is slow
|
270
|
+
@absolute_keyword_location ||= if !parent || (!parent.schema.base_uri.equal?(base_uri) && (base_uri.fragment.nil? || base_uri.fragment.empty?))
|
271
|
+
absolute_keyword_location_uri = base_uri.dup
|
272
|
+
absolute_keyword_location_uri.fragment = ''
|
273
|
+
absolute_keyword_location_uri.to_s
|
274
|
+
elsif keyword
|
275
|
+
"#{parent.absolute_keyword_location}/#{fragment_encode(escaped_keyword)}"
|
276
|
+
else
|
277
|
+
parent.absolute_keyword_location
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def schema_pointer
|
282
|
+
@schema_pointer ||= if !parent
|
283
|
+
''
|
284
|
+
elsif keyword
|
285
|
+
"#{parent.schema_pointer}/#{escaped_keyword}"
|
286
|
+
else
|
287
|
+
parent.schema_pointer
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def id_keyword
|
292
|
+
@id_keyword ||= (keywords.key?('$id') ? '$id' : 'id')
|
293
|
+
end
|
294
|
+
|
295
|
+
def defs_keyword
|
296
|
+
@defs_keyword ||= (keywords.key?('$defs') ? '$defs' : 'definitions')
|
297
|
+
end
|
298
|
+
|
299
|
+
def resources
|
300
|
+
@resources ||= { :lexical => {}, :dynamic => {} }
|
301
|
+
end
|
302
|
+
|
303
|
+
def error(formatted_instance_location:, **options)
|
304
|
+
if value == false && parent&.respond_to?(:false_schema_error)
|
305
|
+
parent.false_schema_error(:formatted_instance_location => formatted_instance_location, **options)
|
306
|
+
else
|
307
|
+
"value at #{formatted_instance_location} does not match schema"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def inspect
|
312
|
+
"#<#{self.class.name} @value=#{@value.inspect} @parent=#{@parent.inspect} @keyword=#{@keyword.inspect}>"
|
313
|
+
end
|
314
|
+
|
315
|
+
private
|
316
|
+
|
317
|
+
def parse
|
318
|
+
@parsed = {}
|
319
|
+
|
320
|
+
if value.is_a?(Hash) && value.key?('$schema')
|
321
|
+
@parsed['$schema'] = SCHEMA_KEYWORD_CLASS.new(value.fetch('$schema'), self, '$schema')
|
322
|
+
elsif root == self && !meta_schema
|
323
|
+
SCHEMA_KEYWORD_CLASS.new(DEFAULT_SCHEMA, self, '$schema')
|
324
|
+
end
|
325
|
+
|
326
|
+
if value.is_a?(Hash) && value.key?('$vocabulary')
|
327
|
+
@parsed['$vocabulary'] = VOCABULARY_KEYWORD_CLASS.new(value.fetch('$vocabulary'), self, '$vocabulary')
|
328
|
+
elsif vocabulary
|
329
|
+
VOCABULARY_KEYWORD_CLASS.new(vocabulary, self, '$vocabulary')
|
330
|
+
end
|
331
|
+
|
332
|
+
if root == self && (!value.is_a?(Hash) || !value.key?(meta_schema.id_keyword))
|
333
|
+
ID_KEYWORD_CLASS.new(base_uri, self, meta_schema.id_keyword)
|
334
|
+
end
|
335
|
+
|
336
|
+
if value.is_a?(Hash)
|
337
|
+
keywords = meta_schema.keywords
|
338
|
+
|
339
|
+
if value.key?('$ref') && keywords.fetch('$ref').exclusive?
|
340
|
+
@parsed['$ref'] = keywords.fetch('$ref').new(value.fetch('$ref'), self, '$ref')
|
341
|
+
defs_keyword = meta_schema.defs_keyword
|
342
|
+
if value.key?(defs_keyword) && keywords.key?(defs_keyword)
|
343
|
+
@parsed[defs_keyword] = keywords.fetch(defs_keyword).new(value.fetch(defs_keyword), self, defs_keyword)
|
344
|
+
end
|
345
|
+
else
|
346
|
+
keyword_order = meta_schema.keyword_order
|
347
|
+
last = keywords.size
|
348
|
+
|
349
|
+
value.sort do |(keyword_a, _value_a), (keyword_b, _value_b)|
|
350
|
+
keyword_order.fetch(keyword_a, last) <=> keyword_order.fetch(keyword_b, last)
|
351
|
+
end.each do |keyword, value|
|
352
|
+
@parsed[keyword] ||= keywords.fetch(keyword, UNKNOWN_KEYWORD_CLASS).new(value, self, keyword)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
@parsed
|
358
|
+
end
|
359
|
+
|
360
|
+
def root_keyword_location
|
361
|
+
@root_keyword_location ||= Location.root
|
362
|
+
end
|
363
|
+
|
364
|
+
def ref_resolver
|
365
|
+
@ref_resolver ||= @original_ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : @original_ref_resolver
|
366
|
+
end
|
367
|
+
|
368
|
+
def regexp_resolver
|
369
|
+
@regexp_resolver ||= case @original_regexp_resolver
|
370
|
+
when 'ecma'
|
371
|
+
CachedResolver.new(&ECMA_REGEXP_RESOLVER)
|
372
|
+
when 'ruby'
|
373
|
+
CachedResolver.new(&RUBY_REGEXP_RESOLVER)
|
374
|
+
else
|
375
|
+
@original_regexp_resolver
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def resolve_enumerators!(output)
|
380
|
+
case output
|
381
|
+
when Hash
|
382
|
+
output.transform_values! { |value| resolve_enumerators!(value) }
|
383
|
+
when Enumerator
|
384
|
+
output.map { |value| resolve_enumerators!(value) }
|
385
|
+
else
|
386
|
+
output
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
data/lib/json_schemer/version.rb
CHANGED
data/lib/json_schemer.rb
CHANGED
@@ -14,34 +14,92 @@ require 'regexp_parser'
|
|
14
14
|
require 'simpleidn'
|
15
15
|
|
16
16
|
require 'json_schemer/version'
|
17
|
+
require 'json_schemer/format/duration'
|
17
18
|
require 'json_schemer/format/hostname'
|
19
|
+
require 'json_schemer/format/json_pointer'
|
18
20
|
require 'json_schemer/format/uri_template'
|
21
|
+
require 'json_schemer/format/email'
|
19
22
|
require 'json_schemer/format'
|
20
23
|
require 'json_schemer/errors'
|
21
24
|
require 'json_schemer/cached_resolver'
|
22
25
|
require 'json_schemer/ecma_regexp'
|
23
|
-
require 'json_schemer/
|
24
|
-
require 'json_schemer/
|
25
|
-
require 'json_schemer/
|
26
|
-
require 'json_schemer/
|
26
|
+
require 'json_schemer/location'
|
27
|
+
require 'json_schemer/result'
|
28
|
+
require 'json_schemer/output'
|
29
|
+
require 'json_schemer/keyword'
|
30
|
+
require 'json_schemer/draft202012/meta'
|
31
|
+
require 'json_schemer/draft202012/vocab/core'
|
32
|
+
require 'json_schemer/draft202012/vocab/applicator'
|
33
|
+
require 'json_schemer/draft202012/vocab/unevaluated'
|
34
|
+
require 'json_schemer/draft202012/vocab/validation'
|
35
|
+
require 'json_schemer/draft202012/vocab/format_annotation'
|
36
|
+
require 'json_schemer/draft202012/vocab/format_assertion'
|
37
|
+
require 'json_schemer/draft202012/vocab/content'
|
38
|
+
require 'json_schemer/draft202012/vocab/meta_data'
|
39
|
+
require 'json_schemer/draft202012/vocab'
|
40
|
+
require 'json_schemer/draft201909/meta'
|
41
|
+
require 'json_schemer/draft201909/vocab/core'
|
42
|
+
require 'json_schemer/draft201909/vocab/applicator'
|
43
|
+
require 'json_schemer/draft201909/vocab'
|
44
|
+
require 'json_schemer/draft7/meta'
|
45
|
+
require 'json_schemer/draft7/vocab/validation'
|
46
|
+
require 'json_schemer/draft7/vocab'
|
47
|
+
require 'json_schemer/draft6/meta'
|
48
|
+
require 'json_schemer/draft6/vocab'
|
49
|
+
require 'json_schemer/draft4/meta'
|
50
|
+
require 'json_schemer/draft4/vocab/validation'
|
51
|
+
require 'json_schemer/draft4/vocab'
|
52
|
+
require 'json_schemer/openapi31/meta'
|
53
|
+
require 'json_schemer/openapi31/vocab/base'
|
54
|
+
require 'json_schemer/openapi31/vocab'
|
55
|
+
require 'json_schemer/openapi31/document'
|
56
|
+
require 'json_schemer/openapi30/document'
|
57
|
+
require 'json_schemer/openapi30/meta'
|
58
|
+
require 'json_schemer/openapi30/vocab/base'
|
59
|
+
require 'json_schemer/openapi30/vocab'
|
60
|
+
require 'json_schemer/openapi'
|
61
|
+
require 'json_schemer/schema'
|
27
62
|
|
28
63
|
module JSONSchemer
|
29
64
|
class UnsupportedMetaSchema < StandardError; end
|
65
|
+
class UnsupportedOpenAPIVersion < StandardError; end
|
30
66
|
class UnknownRef < StandardError; end
|
31
67
|
class UnknownFormat < StandardError; end
|
68
|
+
class UnknownVocabulary < StandardError; end
|
69
|
+
class UnknownContentEncoding < StandardError; end
|
70
|
+
class UnknownContentMediaType < StandardError; end
|
71
|
+
class UnknownOutputFormat < StandardError; end
|
32
72
|
class InvalidRefResolution < StandardError; end
|
73
|
+
class InvalidRefPointer < StandardError; end
|
33
74
|
class InvalidRegexpResolution < StandardError; end
|
34
75
|
class InvalidFileURI < StandardError; end
|
35
|
-
class InvalidSymbolKey < StandardError; end
|
36
76
|
class InvalidEcmaRegexp < StandardError; end
|
37
77
|
|
38
|
-
|
39
|
-
|
40
|
-
'
|
41
|
-
'
|
42
|
-
'
|
43
|
-
'
|
44
|
-
|
78
|
+
VOCABULARIES = {
|
79
|
+
'https://json-schema.org/draft/2020-12/vocab/core' => Draft202012::Vocab::CORE,
|
80
|
+
'https://json-schema.org/draft/2020-12/vocab/applicator' => Draft202012::Vocab::APPLICATOR,
|
81
|
+
'https://json-schema.org/draft/2020-12/vocab/unevaluated' => Draft202012::Vocab::UNEVALUATED,
|
82
|
+
'https://json-schema.org/draft/2020-12/vocab/validation' => Draft202012::Vocab::VALIDATION,
|
83
|
+
'https://json-schema.org/draft/2020-12/vocab/format-annotation' => Draft202012::Vocab::FORMAT_ANNOTATION,
|
84
|
+
'https://json-schema.org/draft/2020-12/vocab/format-assertion' => Draft202012::Vocab::FORMAT_ASSERTION,
|
85
|
+
'https://json-schema.org/draft/2020-12/vocab/content' => Draft202012::Vocab::CONTENT,
|
86
|
+
'https://json-schema.org/draft/2020-12/vocab/meta-data' => Draft202012::Vocab::META_DATA,
|
87
|
+
|
88
|
+
'https://json-schema.org/draft/2019-09/vocab/core' => Draft201909::Vocab::CORE,
|
89
|
+
'https://json-schema.org/draft/2019-09/vocab/applicator' => Draft201909::Vocab::APPLICATOR,
|
90
|
+
'https://json-schema.org/draft/2019-09/vocab/validation' => Draft201909::Vocab::VALIDATION,
|
91
|
+
'https://json-schema.org/draft/2019-09/vocab/format' => Draft201909::Vocab::FORMAT,
|
92
|
+
'https://json-schema.org/draft/2019-09/vocab/content' => Draft201909::Vocab::CONTENT,
|
93
|
+
'https://json-schema.org/draft/2019-09/vocab/meta-data' => Draft201909::Vocab::META_DATA,
|
94
|
+
|
95
|
+
'json-schemer://draft7' => Draft7::Vocab::ALL,
|
96
|
+
'json-schemer://draft6' => Draft6::Vocab::ALL,
|
97
|
+
'json-schemer://draft4' => Draft4::Vocab::ALL,
|
98
|
+
|
99
|
+
'https://spec.openapis.org/oas/3.1/vocab/base' => OpenAPI31::Vocab::BASE,
|
100
|
+
'json-schemer://openapi30' => OpenAPI30::Vocab::BASE
|
101
|
+
}
|
102
|
+
VOCABULARY_ORDER = VOCABULARIES.transform_values.with_index { |_vocabulary, index| index }
|
45
103
|
|
46
104
|
WINDOWS_URI_PATH_REGEX = /\A\/[a-z]:/i
|
47
105
|
|
@@ -54,7 +112,7 @@ module JSONSchemer
|
|
54
112
|
end
|
55
113
|
|
56
114
|
class << self
|
57
|
-
def schema(schema,
|
115
|
+
def schema(schema, meta_schema: draft202012, **options)
|
58
116
|
case schema
|
59
117
|
when String
|
60
118
|
schema = JSON.parse(schema)
|
@@ -69,23 +127,139 @@ module JSONSchemer
|
|
69
127
|
ref_resolver.call(base_uri)
|
70
128
|
end
|
71
129
|
end
|
72
|
-
|
73
|
-
|
74
|
-
meta_schema = schema.fetch('$schema')
|
75
|
-
SCHEMA_CLASS_BY_META_SCHEMA[meta_schema] || raise(UnsupportedMetaSchema, meta_schema)
|
76
|
-
else
|
77
|
-
default_schema_class
|
130
|
+
unless meta_schema.is_a?(Schema)
|
131
|
+
meta_schema = META_SCHEMAS_BY_BASE_URI_STR[meta_schema] || raise(UnsupportedMetaSchema, meta_schema)
|
78
132
|
end
|
133
|
+
Schema.new(schema, :meta_schema => meta_schema, **options)
|
134
|
+
end
|
135
|
+
|
136
|
+
def valid_schema?(schema, **options)
|
137
|
+
schema(schema, **options).valid_schema?
|
138
|
+
end
|
139
|
+
|
140
|
+
def validate_schema(schema, **options)
|
141
|
+
schema(schema, **options).validate_schema
|
142
|
+
end
|
143
|
+
|
144
|
+
def draft202012
|
145
|
+
@draft202012 ||= Schema.new(
|
146
|
+
Draft202012::SCHEMA,
|
147
|
+
:base_uri => Draft202012::BASE_URI,
|
148
|
+
:ref_resolver => Draft202012::Meta::SCHEMAS.to_proc,
|
149
|
+
:regexp_resolver => 'ecma'
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
def draft201909
|
154
|
+
@draft201909 ||= Schema.new(
|
155
|
+
Draft201909::SCHEMA,
|
156
|
+
:base_uri => Draft201909::BASE_URI,
|
157
|
+
:ref_resolver => Draft201909::Meta::SCHEMAS.to_proc,
|
158
|
+
:regexp_resolver => 'ecma'
|
159
|
+
)
|
160
|
+
end
|
79
161
|
|
80
|
-
|
162
|
+
def draft7
|
163
|
+
@draft7 ||= Schema.new(
|
164
|
+
Draft7::SCHEMA,
|
165
|
+
:vocabulary => { 'json-schemer://draft7' => true },
|
166
|
+
:base_uri => Draft7::BASE_URI,
|
167
|
+
:regexp_resolver => 'ecma'
|
168
|
+
)
|
81
169
|
end
|
82
170
|
|
83
|
-
def
|
84
|
-
|
171
|
+
def draft6
|
172
|
+
@draft6 ||= Schema.new(
|
173
|
+
Draft6::SCHEMA,
|
174
|
+
:vocabulary => { 'json-schemer://draft6' => true },
|
175
|
+
:base_uri => Draft6::BASE_URI,
|
176
|
+
:regexp_resolver => 'ecma'
|
177
|
+
)
|
85
178
|
end
|
86
179
|
|
87
|
-
def
|
88
|
-
|
180
|
+
def draft4
|
181
|
+
@draft4 ||= Schema.new(
|
182
|
+
Draft4::SCHEMA,
|
183
|
+
:vocabulary => { 'json-schemer://draft4' => true },
|
184
|
+
:base_uri => Draft4::BASE_URI,
|
185
|
+
:regexp_resolver => 'ecma'
|
186
|
+
)
|
89
187
|
end
|
188
|
+
|
189
|
+
def openapi31
|
190
|
+
@openapi31 ||= Schema.new(
|
191
|
+
OpenAPI31::SCHEMA,
|
192
|
+
:base_uri => OpenAPI31::BASE_URI,
|
193
|
+
:ref_resolver => OpenAPI31::Meta::SCHEMAS.to_proc,
|
194
|
+
:regexp_resolver => 'ecma',
|
195
|
+
# https://spec.openapis.org/oas/latest.html#data-types
|
196
|
+
:formats => {
|
197
|
+
'int32' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 32 },
|
198
|
+
'int64' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 64 },
|
199
|
+
'float' => proc { |instance, _value| instance.is_a?(Float) },
|
200
|
+
'double' => proc { |instance, _value| instance.is_a?(Float) },
|
201
|
+
'password' => proc { |_instance, _value| true }
|
202
|
+
}
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
def openapi30
|
207
|
+
@openapi30 ||= Schema.new(
|
208
|
+
OpenAPI30::SCHEMA,
|
209
|
+
:vocabulary => {
|
210
|
+
'json-schemer://draft4' => true,
|
211
|
+
'json-schemer://openapi30' => true
|
212
|
+
},
|
213
|
+
:base_uri => OpenAPI30::BASE_URI,
|
214
|
+
:ref_resolver => OpenAPI30::Meta::SCHEMAS.to_proc,
|
215
|
+
:regexp_resolver => 'ecma',
|
216
|
+
:formats => {
|
217
|
+
'int32' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 32 },
|
218
|
+
'int64' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 64 },
|
219
|
+
'float' => proc { |instance, _value| instance.is_a?(Float) },
|
220
|
+
'double' => proc { |instance, _value| instance.is_a?(Float) },
|
221
|
+
'byte' => proc { |instance, _value| Format.decode_content_encoding(instance, 'base64').first },
|
222
|
+
'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT },
|
223
|
+
'password' => proc { |_instance, _value| true }
|
224
|
+
}
|
225
|
+
)
|
226
|
+
end
|
227
|
+
|
228
|
+
def openapi31_document
|
229
|
+
@openapi31_document ||= Schema.new(
|
230
|
+
OpenAPI31::Document::SCHEMA_BASE,
|
231
|
+
:ref_resolver => OpenAPI31::Document::SCHEMAS.to_proc,
|
232
|
+
:regexp_resolver => 'ecma'
|
233
|
+
)
|
234
|
+
end
|
235
|
+
|
236
|
+
def openapi30_document
|
237
|
+
@openapi30_document ||= Schema.new(
|
238
|
+
OpenAPI30::Document::SCHEMA,
|
239
|
+
:ref_resolver => OpenAPI30::Document::SCHEMAS.to_proc,
|
240
|
+
:regexp_resolver => 'ecma'
|
241
|
+
)
|
242
|
+
end
|
243
|
+
|
244
|
+
def openapi(document, **options)
|
245
|
+
OpenAPI.new(document, **options)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
META_SCHEMA_CALLABLES_BY_BASE_URI_STR = {
|
250
|
+
Draft202012::BASE_URI.to_s => method(:draft202012),
|
251
|
+
Draft201909::BASE_URI.to_s => method(:draft201909),
|
252
|
+
Draft7::BASE_URI.to_s => method(:draft7),
|
253
|
+
Draft6::BASE_URI.to_s => method(:draft6),
|
254
|
+
Draft4::BASE_URI.to_s => method(:draft4),
|
255
|
+
# version-less $schema deprecated after Draft 4
|
256
|
+
'http://json-schema.org/schema#' => method(:draft4),
|
257
|
+
OpenAPI31::BASE_URI.to_s => method(:openapi31),
|
258
|
+
OpenAPI30::BASE_URI.to_s => method(:openapi30)
|
259
|
+
}.freeze
|
260
|
+
|
261
|
+
META_SCHEMAS_BY_BASE_URI_STR = Hash.new do |hash, base_uri_str|
|
262
|
+
next unless META_SCHEMA_CALLABLES_BY_BASE_URI_STR.key?(base_uri_str)
|
263
|
+
hash[base_uri_str] = META_SCHEMA_CALLABLES_BY_BASE_URI_STR.fetch(base_uri_str).call
|
90
264
|
end
|
91
265
|
end
|