json_schemer 0.2.18 → 2.3.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 +7 -7
  3. data/CHANGELOG.md +102 -0
  4. data/Gemfile.lock +30 -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 -30
  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