json_schemer 0.2.18 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -7
  3. data/CHANGELOG.md +89 -0
  4. data/Gemfile.lock +35 -10
  5. data/README.md +395 -5
  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 +32 -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 +241 -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.0'
4
4
  end