json_schemer 1.0.3 → 2.1.1

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 +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