json_schemer 1.0.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -6
  3. data/CHANGELOG.md +25 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +137 -14
  6. data/json_schemer.gemspec +1 -1
  7. data/lib/json_schemer/draft201909/meta.rb +335 -0
  8. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  9. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  10. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  11. data/lib/json_schemer/draft202012/meta.rb +361 -0
  12. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  13. data/lib/json_schemer/draft202012/vocab/content.rb +44 -0
  14. data/lib/json_schemer/draft202012/vocab/core.rb +154 -0
  15. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +31 -0
  16. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +29 -0
  17. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  18. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
  19. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  20. data/lib/json_schemer/draft202012/vocab.rb +103 -0
  21. data/lib/json_schemer/draft4/meta.rb +155 -0
  22. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  23. data/lib/json_schemer/draft4/vocab.rb +18 -0
  24. data/lib/json_schemer/draft6/meta.rb +161 -0
  25. data/lib/json_schemer/draft6/vocab.rb +16 -0
  26. data/lib/json_schemer/draft7/meta.rb +178 -0
  27. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  28. data/lib/json_schemer/draft7/vocab.rb +30 -0
  29. data/lib/json_schemer/errors.rb +1 -0
  30. data/lib/json_schemer/format/duration.rb +23 -0
  31. data/lib/json_schemer/format/json_pointer.rb +18 -0
  32. data/lib/json_schemer/format.rb +52 -26
  33. data/lib/json_schemer/keyword.rb +41 -0
  34. data/lib/json_schemer/location.rb +25 -0
  35. data/lib/json_schemer/openapi.rb +40 -0
  36. data/lib/json_schemer/openapi30/document.rb +1673 -0
  37. data/lib/json_schemer/openapi30/meta.rb +26 -0
  38. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  39. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  40. data/lib/json_schemer/openapi31/document.rb +1559 -0
  41. data/lib/json_schemer/openapi31/meta.rb +128 -0
  42. data/lib/json_schemer/openapi31/vocab/base.rb +89 -0
  43. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  44. data/lib/json_schemer/output.rb +55 -0
  45. data/lib/json_schemer/result.rb +168 -0
  46. data/lib/json_schemer/schema.rb +390 -0
  47. data/lib/json_schemer/version.rb +1 -1
  48. data/lib/json_schemer.rb +197 -24
  49. metadata +42 -10
  50. data/lib/json_schemer/schema/base.rb +0 -677
  51. data/lib/json_schemer/schema/draft4.json +0 -149
  52. data/lib/json_schemer/schema/draft4.rb +0 -44
  53. data/lib/json_schemer/schema/draft6.json +0 -155
  54. data/lib/json_schemer/schema/draft6.rb +0 -25
  55. data/lib/json_schemer/schema/draft7.json +0 -172
  56. 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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
- VERSION = '1.0.3'
3
+ VERSION = '2.0.0'
4
4
  end
data/lib/json_schemer.rb CHANGED
@@ -14,35 +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'
19
21
  require 'json_schemer/format/email'
20
22
  require 'json_schemer/format'
21
23
  require 'json_schemer/errors'
22
24
  require 'json_schemer/cached_resolver'
23
25
  require 'json_schemer/ecma_regexp'
24
- require 'json_schemer/schema/base'
25
- require 'json_schemer/schema/draft4'
26
- require 'json_schemer/schema/draft6'
27
- require 'json_schemer/schema/draft7'
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'
28
62
 
29
63
  module JSONSchemer
30
64
  class UnsupportedMetaSchema < StandardError; end
65
+ class UnsupportedOpenAPIVersion < StandardError; end
31
66
  class UnknownRef < StandardError; end
32
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
33
72
  class InvalidRefResolution < StandardError; end
73
+ class InvalidRefPointer < StandardError; end
34
74
  class InvalidRegexpResolution < StandardError; end
35
75
  class InvalidFileURI < StandardError; end
36
- class InvalidSymbolKey < StandardError; end
37
76
  class InvalidEcmaRegexp < StandardError; end
38
77
 
39
- DEFAULT_SCHEMA_CLASS = Schema::Draft7
40
- SCHEMA_CLASS_BY_META_SCHEMA = {
41
- 'http://json-schema.org/schema#' => Schema::Draft4, # Version-less $schema deprecated after Draft 4
42
- 'http://json-schema.org/draft-04/schema#' => Schema::Draft4,
43
- 'http://json-schema.org/draft-06/schema#' => Schema::Draft6,
44
- 'http://json-schema.org/draft-07/schema#' => Schema::Draft7
45
- }.freeze
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 }
46
103
 
47
104
  WINDOWS_URI_PATH_REGEX = /\A\/[a-z]:/i
48
105
 
@@ -55,7 +112,7 @@ module JSONSchemer
55
112
  end
56
113
 
57
114
  class << self
58
- def schema(schema, default_schema_class: DEFAULT_SCHEMA_CLASS, **options)
115
+ def schema(schema, meta_schema: draft202012, **options)
59
116
  case schema
60
117
  when String
61
118
  schema = JSON.parse(schema)
@@ -70,23 +127,139 @@ module JSONSchemer
70
127
  ref_resolver.call(base_uri)
71
128
  end
72
129
  end
73
-
74
- schema_class = if schema.is_a?(Hash) && schema.key?('$schema')
75
- meta_schema = schema.fetch('$schema')
76
- SCHEMA_CLASS_BY_META_SCHEMA[meta_schema] || raise(UnsupportedMetaSchema, meta_schema)
77
- else
78
- 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)
79
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
80
161
 
81
- schema_class.new(schema, **options)
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
+ )
82
169
  end
83
170
 
84
- def valid_schema?(schema, default_schema_class: DEFAULT_SCHEMA_CLASS)
85
- schema(schema, default_schema_class: default_schema_class).valid_schema?
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
+ )
86
178
  end
87
179
 
88
- def validate_schema(schema, default_schema_class: DEFAULT_SCHEMA_CLASS)
89
- schema(schema, default_schema_class: default_schema_class).validate_schema
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
+ )
90
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
91
264
  end
92
265
  end