json_schemer 1.0.3 → 2.0.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.
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