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,382 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft202012
4
+ module Vocab
5
+ module Applicator
6
+ class AllOf < Keyword
7
+ def error(formatted_instance_location:, **)
8
+ "value at #{formatted_instance_location} does not match all `allOf` schemas"
9
+ end
10
+
11
+ def parse
12
+ value.map.with_index do |subschema, index|
13
+ subschema(subschema, index.to_s)
14
+ end
15
+ end
16
+
17
+ def validate(instance, instance_location, keyword_location, context)
18
+ nested = parsed.map.with_index do |subschema, index|
19
+ subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
20
+ end
21
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
22
+ end
23
+ end
24
+
25
+ class AnyOf < Keyword
26
+ def error(formatted_instance_location:, **)
27
+ "value at #{formatted_instance_location} does not match any `anyOf` schemas"
28
+ end
29
+
30
+ def parse
31
+ value.map.with_index do |subschema, index|
32
+ subschema(subschema, index.to_s)
33
+ end
34
+ end
35
+
36
+ def validate(instance, instance_location, keyword_location, context)
37
+ nested = parsed.map.with_index do |subschema, index|
38
+ subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
39
+ end
40
+ result(instance, instance_location, keyword_location, nested.any?(&:valid), nested)
41
+ end
42
+ end
43
+
44
+ class OneOf < Keyword
45
+ def error(formatted_instance_location:, **)
46
+ "value at #{formatted_instance_location} does not match exactly one `oneOf` schema"
47
+ end
48
+
49
+ def parse
50
+ value.map.with_index do |subschema, index|
51
+ subschema(subschema, index.to_s)
52
+ end
53
+ end
54
+
55
+ def validate(instance, instance_location, keyword_location, context)
56
+ nested = parsed.map.with_index do |subschema, index|
57
+ subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
58
+ end
59
+ valid_count = nested.count(&:valid)
60
+ result(instance, instance_location, keyword_location, valid_count == 1, nested, :ignore_nested => valid_count > 1)
61
+ end
62
+ end
63
+
64
+ class Not < Keyword
65
+ def error(formatted_instance_location:, **)
66
+ "value at #{formatted_instance_location} matches `not` schema"
67
+ end
68
+
69
+ def parse
70
+ subschema(value)
71
+ end
72
+
73
+ def validate(instance, instance_location, keyword_location, context)
74
+ subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
75
+ result(instance, instance_location, keyword_location, !subschema_result.valid, subschema_result.nested)
76
+ end
77
+ end
78
+
79
+ class If < Keyword
80
+ def parse
81
+ subschema(value)
82
+ end
83
+
84
+ def validate(instance, instance_location, keyword_location, context)
85
+ subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
86
+ result(instance, instance_location, keyword_location, true, subschema_result.nested, :annotation => subschema_result.valid)
87
+ end
88
+ end
89
+
90
+ class Then < Keyword
91
+ def error(formatted_instance_location:, **)
92
+ "value at #{formatted_instance_location} does not match conditional `then` schema"
93
+ end
94
+
95
+ def parse
96
+ subschema(value)
97
+ end
98
+
99
+ def validate(instance, instance_location, keyword_location, context)
100
+ return unless context.adjacent_results.key?(If) && context.adjacent_results.fetch(If).annotation
101
+ subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
102
+ result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
103
+ end
104
+ end
105
+
106
+ class Else < Keyword
107
+ def error(formatted_instance_location:, **)
108
+ "value at #{formatted_instance_location} does not match conditional `else` schema"
109
+ end
110
+
111
+ def parse
112
+ subschema(value)
113
+ end
114
+
115
+ def validate(instance, instance_location, keyword_location, context)
116
+ return unless context.adjacent_results.key?(If) && !context.adjacent_results.fetch(If).annotation
117
+ subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
118
+ result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
119
+ end
120
+ end
121
+
122
+ class DependentSchemas < Keyword
123
+ def error(formatted_instance_location:, **)
124
+ "value at #{formatted_instance_location} does not match applicable `dependentSchemas` schemas"
125
+ end
126
+
127
+ def parse
128
+ value.each_with_object({}) do |(key, subschema), out|
129
+ out[key] = subschema(subschema, key)
130
+ end
131
+ end
132
+
133
+ def validate(instance, instance_location, keyword_location, context)
134
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
135
+
136
+ nested = parsed.select do |key, _subschema|
137
+ instance.key?(key)
138
+ end.map do |key, subschema|
139
+ subschema.validate_instance(instance, instance_location, join_location(keyword_location, key), context)
140
+ end
141
+
142
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
143
+ end
144
+ end
145
+
146
+ class PrefixItems < Keyword
147
+ def error(formatted_instance_location:, **)
148
+ "array items at #{formatted_instance_location} do not match corresponding `prefixItems` schemas"
149
+ end
150
+
151
+ def parse
152
+ value.map.with_index do |subschema, index|
153
+ subschema(subschema, index.to_s)
154
+ end
155
+ end
156
+
157
+ def validate(instance, instance_location, keyword_location, context)
158
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
159
+
160
+ nested = instance.take(parsed.size).map.with_index do |item, index|
161
+ parsed.fetch(index).validate_instance(item, join_location(instance_location, index.to_s), join_location(keyword_location, index.to_s), context)
162
+ end
163
+
164
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => (nested.size - 1))
165
+ end
166
+ end
167
+
168
+ class Items < Keyword
169
+ def error(formatted_instance_location:, **)
170
+ "array items at #{formatted_instance_location} do not match `items` schema"
171
+ end
172
+
173
+ def parse
174
+ subschema(value)
175
+ end
176
+
177
+ def validate(instance, instance_location, keyword_location, context)
178
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
179
+
180
+ evaluated_index = context.adjacent_results[PrefixItems]&.annotation
181
+ offset = evaluated_index ? (evaluated_index + 1) : 0
182
+
183
+ nested = instance.slice(offset..-1).map.with_index do |item, index|
184
+ parsed.validate_instance(item, join_location(instance_location, (offset + index).to_s), keyword_location, context)
185
+ end
186
+
187
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => nested.any?)
188
+ end
189
+ end
190
+
191
+ class Contains < Keyword
192
+ def error(formatted_instance_location:, **)
193
+ "array at #{formatted_instance_location} does not contain enough items that match `contains` schema"
194
+ end
195
+
196
+ def parse
197
+ subschema(value)
198
+ end
199
+
200
+ def validate(instance, instance_location, keyword_location, context)
201
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
202
+
203
+ nested = instance.map.with_index do |item, index|
204
+ parsed.validate_instance(item, join_location(instance_location, index.to_s), keyword_location, context)
205
+ end
206
+
207
+ annotation = []
208
+ nested.each_with_index do |nested_result, index|
209
+ annotation << index if nested_result.valid
210
+ end
211
+
212
+ min_contains = schema.parsed['minContains']&.parsed || 1
213
+
214
+ result(instance, instance_location, keyword_location, annotation.size >= min_contains, nested, :annotation => annotation, :ignore_nested => true)
215
+ end
216
+ end
217
+
218
+ class Properties < Keyword
219
+ def error(formatted_instance_location:, **)
220
+ "object properties at #{formatted_instance_location} do not match corresponding `properties` schemas"
221
+ end
222
+
223
+ def parse
224
+ value.each_with_object({}) do |(property, subschema), out|
225
+ out[property] = subschema(subschema, property)
226
+ end
227
+ end
228
+
229
+ def validate(instance, instance_location, keyword_location, context)
230
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
231
+
232
+ if root.before_property_validation.any?
233
+ original_instance = context.original_instance(instance_location)
234
+ root.before_property_validation.each do |hook|
235
+ parsed.each do |property, subschema|
236
+ hook.call(original_instance, property, subschema.value, schema.value)
237
+ end
238
+ end
239
+ instance.replace(deep_stringify_keys(original_instance))
240
+ end
241
+
242
+ evaluated_keys = []
243
+ nested = []
244
+
245
+ parsed.each do |property, subschema|
246
+ if instance.key?(property)
247
+ evaluated_keys << property
248
+ nested << subschema.validate_instance(instance.fetch(property), join_location(instance_location, property), join_location(keyword_location, property), context)
249
+ end
250
+ end
251
+
252
+ if root.after_property_validation.any?
253
+ original_instance = context.original_instance(instance_location)
254
+ root.after_property_validation.each do |hook|
255
+ parsed.each do |property, subschema|
256
+ hook.call(original_instance, property, subschema.value, schema.value)
257
+ end
258
+ end
259
+ instance.replace(deep_stringify_keys(original_instance))
260
+ end
261
+
262
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => evaluated_keys)
263
+ end
264
+ end
265
+
266
+ class PatternProperties < Keyword
267
+ def error(formatted_instance_location:, **)
268
+ "object properties at #{formatted_instance_location} do not match corresponding `patternProperties` schemas"
269
+ end
270
+
271
+ def parse
272
+ value.each_with_object({}) do |(pattern, subschema), out|
273
+ out[pattern] = subschema(subschema, pattern)
274
+ end
275
+ end
276
+
277
+ def validate(instance, instance_location, keyword_location, context)
278
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
279
+
280
+ evaluated = Set[]
281
+ nested = []
282
+
283
+ parsed.each do |pattern, subschema|
284
+ regexp = root.resolve_regexp(pattern)
285
+ instance.each do |key, value|
286
+ if regexp.match?(key)
287
+ evaluated << key
288
+ nested << subschema.validate_instance(value, join_location(instance_location, key), join_location(keyword_location, pattern), context)
289
+ end
290
+ end
291
+ end
292
+
293
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => evaluated.to_a)
294
+ end
295
+ end
296
+
297
+ class AdditionalProperties < Keyword
298
+ def error(formatted_instance_location:, **)
299
+ "object properties at #{formatted_instance_location} do not match `additionalProperties` schema"
300
+ end
301
+
302
+ def false_schema_error(formatted_instance_location:, **)
303
+ "object property at #{formatted_instance_location} is a disallowed additional property"
304
+ end
305
+
306
+ def parse
307
+ subschema(value)
308
+ end
309
+
310
+ def validate(instance, instance_location, keyword_location, context)
311
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
312
+
313
+ evaluated_keys = context.adjacent_results[Properties]&.annotation || []
314
+ evaluated_keys += context.adjacent_results[PatternProperties]&.annotation || []
315
+ evaluated_keys = evaluated_keys.to_set
316
+
317
+ evaluated = instance.reject do |key, _value|
318
+ evaluated_keys.include?(key)
319
+ end
320
+
321
+ nested = evaluated.map do |key, value|
322
+ parsed.validate_instance(value, join_location(instance_location, key), keyword_location, context)
323
+ end
324
+
325
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => evaluated.keys)
326
+ end
327
+ end
328
+
329
+ class PropertyNames < Keyword
330
+ def error(formatted_instance_location:, **)
331
+ "object property names at #{formatted_instance_location} do not match `propertyNames` schema"
332
+ end
333
+
334
+ def parse
335
+ subschema(value)
336
+ end
337
+
338
+ def validate(instance, instance_location, keyword_location, context)
339
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
340
+
341
+ nested = instance.map do |key, _value|
342
+ parsed.validate_instance(key, instance_location, keyword_location, context)
343
+ end
344
+
345
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
346
+ end
347
+ end
348
+
349
+ class Dependencies < Keyword
350
+ def error(formatted_instance_location:, **)
351
+ "object at #{formatted_instance_location} either does not match applicable `dependencies` schemas or is missing required `dependencies` properties"
352
+ end
353
+
354
+ def parse
355
+ value.each_with_object({}) do |(key, value), out|
356
+ out[key] = value.is_a?(Array) ? value : subschema(value, key)
357
+ end
358
+ end
359
+
360
+ def validate(instance, instance_location, keyword_location, context)
361
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
362
+
363
+ existing_keys = instance.keys
364
+
365
+ nested = parsed.select do |key, _value|
366
+ instance.key?(key)
367
+ end.map do |key, value|
368
+ if value.is_a?(Array)
369
+ missing_keys = value - existing_keys
370
+ result(instance, instance_location, join_location(keyword_location, key), missing_keys.none?, :details => { 'missing_keys' => missing_keys })
371
+ else
372
+ value.validate_instance(instance, instance_location, join_location(keyword_location, key), context)
373
+ end
374
+ end
375
+
376
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft202012
4
+ module Vocab
5
+ module Content
6
+ class ContentEncoding < Keyword
7
+ def parse
8
+ root.fetch_content_encoding(value) { raise UnknownContentEncoding, value }
9
+ end
10
+
11
+ def validate(instance, instance_location, keyword_location, _context)
12
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
13
+
14
+ _valid, annotation = parsed.call(instance)
15
+
16
+ result(instance, instance_location, keyword_location, true, :annotation => annotation)
17
+ end
18
+ end
19
+
20
+ class ContentMediaType < Keyword
21
+ def parse
22
+ root.fetch_content_media_type(value) { raise UnknownContentMediaType, value }
23
+ end
24
+
25
+ def validate(instance, instance_location, keyword_location, context)
26
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
27
+
28
+ decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance
29
+ _valid, annotation = parsed.call(decoded_instance)
30
+
31
+ result(instance, instance_location, keyword_location, true, :annotation => annotation)
32
+ end
33
+ end
34
+
35
+ class ContentSchema < Keyword
36
+ def parse
37
+ subschema(value)
38
+ end
39
+
40
+ def validate(instance, instance_location, keyword_location, context)
41
+ return result(instance, instance_location, keyword_location, true) unless context.adjacent_results.key?(ContentMediaType)
42
+
43
+ parsed_instance = context.adjacent_results.fetch(ContentMediaType).annotation
44
+ annotation = parsed.validate_instance(parsed_instance, instance_location, keyword_location, context)
45
+
46
+ result(instance, instance_location, keyword_location, true, :annotation => annotation.to_output_unit)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft202012
4
+ module Vocab
5
+ module Core
6
+ class Schema < Keyword
7
+ def parse
8
+ schema.meta_schema = if value == schema.base_uri.to_s
9
+ schema
10
+ else
11
+ META_SCHEMAS_BY_BASE_URI_STR[value] || root.resolve_ref(URI(value))
12
+ end
13
+ value
14
+ end
15
+ end
16
+
17
+ class Vocabulary < Keyword
18
+ def parse
19
+ value.each_with_object({}) do |(vocabulary, required), out|
20
+ if VOCABULARIES.key?(vocabulary)
21
+ out[vocabulary] = VOCABULARIES.fetch(vocabulary)
22
+ elsif required
23
+ raise UnknownVocabulary, vocabulary
24
+ end
25
+ end.tap do |vocabularies|
26
+ schema.keywords = vocabularies.sort_by do |vocabulary, _keywords|
27
+ VOCABULARY_ORDER.fetch(vocabulary, Float::INFINITY)
28
+ end.each_with_object({}) do |(_vocabulary, keywords), out|
29
+ out.merge!(keywords)
30
+ end
31
+ schema.keyword_order = schema.keywords.transform_values.with_index { |_keyword_class, index| index }
32
+ end
33
+ end
34
+ end
35
+
36
+ class Id < Keyword
37
+ def parse
38
+ URI.join(schema.base_uri, value).tap do |uri|
39
+ schema.base_uri = uri
40
+ root.resources[:lexical][uri] = schema
41
+ end
42
+ end
43
+ end
44
+
45
+ class Anchor < Keyword
46
+ def parse
47
+ URI.join(schema.base_uri, "##{value}").tap do |uri|
48
+ root.resources[:lexical][uri] = schema
49
+ end
50
+ end
51
+ end
52
+
53
+ class Ref < Keyword
54
+ def self.exclusive?
55
+ false
56
+ end
57
+
58
+ def ref_uri
59
+ @ref_uri ||= URI.join(schema.base_uri, value)
60
+ end
61
+
62
+ def ref_schema
63
+ @ref_schema ||= root.resolve_ref(ref_uri)
64
+ end
65
+
66
+ def validate(instance, instance_location, keyword_location, context)
67
+ ref_schema.validate_instance(instance, instance_location, keyword_location, context)
68
+ end
69
+ end
70
+
71
+ class DynamicAnchor < Keyword
72
+ def parse
73
+ URI.join(schema.base_uri, "##{value}").tap do |uri|
74
+ root.resources[:lexical][uri] = schema
75
+ root.resources[:dynamic][uri] = schema
76
+ end
77
+ end
78
+ end
79
+
80
+ class DynamicRef < Keyword
81
+ def ref_uri
82
+ @ref_uri ||= URI.join(schema.base_uri, value)
83
+ end
84
+
85
+ def ref_schema
86
+ @ref_schema ||= root.resolve_ref(ref_uri)
87
+ end
88
+
89
+ def dynamic_anchor
90
+ return @dynamic_anchor if defined?(@dynamic_anchor)
91
+ fragment = ref_schema.parsed['$dynamicAnchor']&.parsed&.fragment
92
+ @dynamic_anchor = (fragment == ref_uri.fragment ? fragment : nil)
93
+ end
94
+
95
+ def validate(instance, instance_location, keyword_location, context)
96
+ schema = ref_schema
97
+
98
+ if dynamic_anchor
99
+ context.dynamic_scope.each do |ancestor|
100
+ dynamic_uri = URI.join(ancestor.base_uri, "##{dynamic_anchor}")
101
+ if ancestor.root.resources.fetch(:dynamic).key?(dynamic_uri)
102
+ schema = ancestor.root.resources.fetch(:dynamic).fetch(dynamic_uri)
103
+ break
104
+ end
105
+ end
106
+ end
107
+
108
+ schema.validate_instance(instance, instance_location, keyword_location, context)
109
+ end
110
+ end
111
+
112
+ class Defs < Keyword
113
+ def parse
114
+ value.each_with_object({}) do |(key, subschema), out|
115
+ out[key] = subschema(subschema, key)
116
+ end
117
+ end
118
+ end
119
+
120
+ class Comment < Keyword; end
121
+
122
+ class XError < Keyword
123
+ def message(error_key)
124
+ value.is_a?(Hash) ? (value[error_key] || value[CATCHALL]) : value
125
+ end
126
+ end
127
+
128
+ class UnknownKeyword < Keyword
129
+ def parse
130
+ if value.is_a?(Hash)
131
+ {}
132
+ elsif value.is_a?(Array)
133
+ []
134
+ else
135
+ value
136
+ end
137
+ end
138
+
139
+ def fetch(token)
140
+ if value.is_a?(Hash)
141
+ parsed[token] ||= JSONSchemer::Schema::UNKNOWN_KEYWORD_CLASS.new(value.fetch(token), self, token, schema)
142
+ elsif value.is_a?(Array)
143
+ parsed[token.to_i] ||= JSONSchemer::Schema::UNKNOWN_KEYWORD_CLASS.new(value.fetch(token.to_i), self, token, schema)
144
+ else
145
+ raise KeyError.new(:receiver => parsed, :key => token)
146
+ end
147
+ end
148
+
149
+ def parsed_schema
150
+ @parsed_schema ||= subschema(value)
151
+ end
152
+
153
+ def validate(instance, instance_location, keyword_location, _context)
154
+ result(instance, instance_location, keyword_location, true, :annotation => value)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft202012
4
+ module Vocab
5
+ module FormatAnnotation
6
+ class Format < Keyword
7
+ def error(formatted_instance_location:, **)
8
+ "value at #{formatted_instance_location} does not match format: #{value}"
9
+ end
10
+
11
+ def parse
12
+ root.format && root.fetch_format(value, false)
13
+ end
14
+
15
+ def validate(instance, instance_location, keyword_location, _context)
16
+ valid = parsed == false || parsed.call(instance, value)
17
+ result(instance, instance_location, keyword_location, valid, :annotation => value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft202012
4
+ module Vocab
5
+ module FormatAssertion
6
+ class Format < Keyword
7
+ def error(formatted_instance_location:, **)
8
+ "value at #{formatted_instance_location} does not match format: #{value}"
9
+ end
10
+
11
+ def parse
12
+ root.format && root.fetch_format(value) { raise UnknownFormat, value }
13
+ end
14
+
15
+ def validate(instance, instance_location, keyword_location, _context)
16
+ valid = parsed == false || parsed.call(instance, value)
17
+ result(instance, instance_location, keyword_location, valid, :annotation => value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft202012
4
+ module Vocab
5
+ module MetaData
6
+ class ReadOnly < Keyword
7
+ def error(formatted_instance_location:, **)
8
+ "value at #{formatted_instance_location} is `readOnly`"
9
+ end
10
+
11
+ def validate(instance, instance_location, keyword_location, context)
12
+ valid = parsed != true || !context.access_mode || context.access_mode == 'read'
13
+ result(instance, instance_location, keyword_location, valid, :annotation => value)
14
+ end
15
+ end
16
+
17
+ class WriteOnly < Keyword
18
+ def error(formatted_instance_location:, **)
19
+ "value at #{formatted_instance_location} is `writeOnly`"
20
+ end
21
+
22
+ def validate(instance, instance_location, keyword_location, context)
23
+ valid = parsed != true || !context.access_mode || context.access_mode == 'write'
24
+ result(instance, instance_location, keyword_location, valid, :annotation => value)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end