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