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