json_schemer 1.0.3 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -7
  3. data/CHANGELOG.md +51 -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 +53 -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 +1672 -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 +1557 -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 +423 -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,423 @@
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
+ obj.fetch(token)
207
+ rescue IndexError
208
+ raise InvalidRefPointer, pointer
209
+ end
210
+
211
+ schema = schema.parsed_schema if schema.is_a?(Keyword)
212
+ raise InvalidRefPointer, pointer unless schema.is_a?(Schema)
213
+
214
+ schema
215
+ end
216
+
217
+ def resolve_regexp(pattern)
218
+ regexp_resolver.call(pattern) || raise(InvalidRegexpResolution, pattern)
219
+ end
220
+
221
+ def bundle
222
+ return value unless value.is_a?(Hash)
223
+
224
+ id_keyword = meta_schema.id_keyword
225
+ defs_keyword = meta_schema.defs_keyword
226
+
227
+ compound_document = value.dup
228
+ compound_document[id_keyword] = base_uri.to_s
229
+ compound_document['$schema'] = meta_schema.base_uri.to_s
230
+ embedded_resources = compound_document[defs_keyword] = (compound_document[defs_keyword]&.dup || {})
231
+
232
+ if compound_document.key?('$ref') && meta_schema.keywords.fetch('$ref').exclusive?
233
+ compound_document['allOf'] = (compound_document['allOf']&.dup || [])
234
+ compound_document['allOf'] << { '$ref' => compound_document.delete('$ref') }
235
+ end
236
+
237
+ values = [self]
238
+ while value = values.shift
239
+ case value
240
+ when Schema
241
+ values << value.parsed
242
+ when Keyword
243
+ if value.respond_to?(:ref_uri) && value.respond_to?(:ref_schema)
244
+ ref_uri = value.ref_uri.dup
245
+ ref_uri.fragment = nil
246
+ ref_id = ref_uri.to_s
247
+ ref_schema = value.ref_schema.root
248
+
249
+ next if ref_schema == root || embedded_resources.key?(ref_id)
250
+
251
+ embedded_resource = ref_schema.value.dup
252
+ embedded_resource[id_keyword] = ref_id
253
+ embedded_resource['$schema'] = ref_schema.meta_schema.base_uri.to_s
254
+ embedded_resources[ref_id] = embedded_resource
255
+
256
+ values << ref_schema
257
+ else
258
+ values << value.parsed
259
+ end
260
+ when Hash
261
+ values.concat(value.values)
262
+ when Array
263
+ values.concat(value)
264
+ end
265
+ end
266
+
267
+ compound_document
268
+ end
269
+
270
+ def absolute_keyword_location
271
+ # using `equal?` because `URI::Generic#==` is slow
272
+ @absolute_keyword_location ||= if !parent || (!parent.schema.base_uri.equal?(base_uri) && (base_uri.fragment.nil? || base_uri.fragment.empty?))
273
+ absolute_keyword_location_uri = base_uri.dup
274
+ absolute_keyword_location_uri.fragment = ''
275
+ absolute_keyword_location_uri.to_s
276
+ elsif keyword
277
+ "#{parent.absolute_keyword_location}/#{fragment_encode(escaped_keyword)}"
278
+ else
279
+ parent.absolute_keyword_location
280
+ end
281
+ end
282
+
283
+ def schema_pointer
284
+ @schema_pointer ||= if !parent
285
+ ''
286
+ elsif keyword
287
+ "#{parent.schema_pointer}/#{escaped_keyword}"
288
+ else
289
+ parent.schema_pointer
290
+ end
291
+ end
292
+
293
+ def error_key
294
+ '^'
295
+ end
296
+
297
+ def fetch(key)
298
+ parsed.fetch(key)
299
+ end
300
+
301
+ def fetch_format(format, *args, &block)
302
+ if meta_schema == self
303
+ formats.fetch(format, *args, &block)
304
+ else
305
+ formats.fetch(format) { meta_schema.fetch_format(format, *args, &block) }
306
+ end
307
+ end
308
+
309
+ def fetch_content_encoding(content_encoding, *args, &block)
310
+ if meta_schema == self
311
+ content_encodings.fetch(content_encoding, *args, &block)
312
+ else
313
+ content_encodings.fetch(content_encoding) { meta_schema.fetch_content_encoding(content_encoding, *args, &block) }
314
+ end
315
+ end
316
+
317
+ def fetch_content_media_type(content_media_type, *args, &block)
318
+ if meta_schema == self
319
+ content_media_types.fetch(content_media_type, *args, &block)
320
+ else
321
+ content_media_types.fetch(content_media_type) { meta_schema.fetch_content_media_type(content_media_type, *args, &block) }
322
+ end
323
+ end
324
+
325
+ def id_keyword
326
+ @id_keyword ||= (keywords.key?('$id') ? '$id' : 'id')
327
+ end
328
+
329
+ def defs_keyword
330
+ @defs_keyword ||= (keywords.key?('$defs') ? '$defs' : 'definitions')
331
+ end
332
+
333
+ def resources
334
+ @resources ||= { :lexical => {}, :dynamic => {} }
335
+ end
336
+
337
+ def error(formatted_instance_location:, **options)
338
+ if value == false && parent&.respond_to?(:false_schema_error)
339
+ parent.false_schema_error(:formatted_instance_location => formatted_instance_location, **options)
340
+ else
341
+ "value at #{formatted_instance_location} does not match schema"
342
+ end
343
+ end
344
+
345
+ def inspect
346
+ "#<#{self.class.name} @value=#{@value.inspect} @parent=#{@parent.inspect} @keyword=#{@keyword.inspect}>"
347
+ end
348
+
349
+ private
350
+
351
+ def parse
352
+ @parsed = {}
353
+
354
+ if value.is_a?(Hash) && value.key?('$schema')
355
+ @parsed['$schema'] = SCHEMA_KEYWORD_CLASS.new(value.fetch('$schema'), self, '$schema')
356
+ elsif root == self && !meta_schema
357
+ SCHEMA_KEYWORD_CLASS.new(DEFAULT_SCHEMA, self, '$schema')
358
+ end
359
+
360
+ if value.is_a?(Hash) && value.key?('$vocabulary')
361
+ @parsed['$vocabulary'] = VOCABULARY_KEYWORD_CLASS.new(value.fetch('$vocabulary'), self, '$vocabulary')
362
+ elsif vocabulary
363
+ VOCABULARY_KEYWORD_CLASS.new(vocabulary, self, '$vocabulary')
364
+ end
365
+
366
+ keywords = meta_schema.keywords
367
+ exclusive_ref = value.is_a?(Hash) && value.key?('$ref') && keywords.fetch('$ref').exclusive?
368
+
369
+ if root == self && (!value.is_a?(Hash) || !value.key?(meta_schema.id_keyword) || exclusive_ref)
370
+ ID_KEYWORD_CLASS.new(base_uri, self, meta_schema.id_keyword)
371
+ end
372
+
373
+ if exclusive_ref
374
+ @parsed['$ref'] = keywords.fetch('$ref').new(value.fetch('$ref'), self, '$ref')
375
+ defs_keyword = meta_schema.defs_keyword
376
+ if value.key?(defs_keyword) && keywords.key?(defs_keyword)
377
+ @parsed[defs_keyword] = keywords.fetch(defs_keyword).new(value.fetch(defs_keyword), self, defs_keyword)
378
+ end
379
+ elsif value.is_a?(Hash)
380
+ keyword_order = meta_schema.keyword_order
381
+ last = keywords.size
382
+
383
+ value.sort do |(keyword_a, _value_a), (keyword_b, _value_b)|
384
+ keyword_order.fetch(keyword_a, last) <=> keyword_order.fetch(keyword_b, last)
385
+ end.each do |keyword, value|
386
+ @parsed[keyword] ||= keywords.fetch(keyword, UNKNOWN_KEYWORD_CLASS).new(value, self, keyword)
387
+ end
388
+ end
389
+
390
+ @parsed
391
+ end
392
+
393
+ def root_keyword_location
394
+ @root_keyword_location ||= Location.root
395
+ end
396
+
397
+ def ref_resolver
398
+ @ref_resolver ||= @original_ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : @original_ref_resolver
399
+ end
400
+
401
+ def regexp_resolver
402
+ @regexp_resolver ||= case @original_regexp_resolver
403
+ when 'ecma'
404
+ CachedResolver.new(&ECMA_REGEXP_RESOLVER)
405
+ when 'ruby'
406
+ CachedResolver.new(&RUBY_REGEXP_RESOLVER)
407
+ else
408
+ @original_regexp_resolver
409
+ end
410
+ end
411
+
412
+ def resolve_enumerators!(output)
413
+ case output
414
+ when Hash
415
+ output.transform_values! { |value| resolve_enumerators!(value) }
416
+ when Enumerator
417
+ output.map { |value| resolve_enumerators!(value) }
418
+ else
419
+ output
420
+ end
421
+ end
422
+ end
423
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
- VERSION = '1.0.3'
3
+ VERSION = '2.1.1'
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