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,320 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft201909
4
+ BASE_URI = URI('https://json-schema.org/draft/2019-09/schema')
5
+ FORMATS = Draft202012::FORMATS
6
+ CONTENT_ENCODINGS = Draft202012::CONTENT_ENCODINGS
7
+ CONTENT_MEDIA_TYPES = Draft202012::CONTENT_MEDIA_TYPES
8
+ SCHEMA = {
9
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
10
+ '$id' => 'https://json-schema.org/draft/2019-09/schema',
11
+ '$vocabulary' => {
12
+ 'https://json-schema.org/draft/2019-09/vocab/core' => true,
13
+ 'https://json-schema.org/draft/2019-09/vocab/applicator' => true,
14
+ 'https://json-schema.org/draft/2019-09/vocab/validation' => true,
15
+ 'https://json-schema.org/draft/2019-09/vocab/meta-data' => true,
16
+ 'https://json-schema.org/draft/2019-09/vocab/format' => false,
17
+ 'https://json-schema.org/draft/2019-09/vocab/content' => true
18
+ },
19
+ '$recursiveAnchor' => true,
20
+ 'title' => 'Core and Validation specifications meta-schema',
21
+ 'allOf' => [
22
+ {'$ref' => 'meta/core'},
23
+ {'$ref' => 'meta/applicator'},
24
+ {'$ref' => 'meta/validation'},
25
+ {'$ref' => 'meta/meta-data'},
26
+ {'$ref' => 'meta/format'},
27
+ {'$ref' => 'meta/content'}
28
+ ],
29
+ 'type' => ['object', 'boolean'],
30
+ 'properties' => {
31
+ 'definitions' => {
32
+ '$comment' => 'While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.',
33
+ 'type' => 'object',
34
+ 'additionalProperties' => { '$recursiveRef' => '#' },
35
+ 'default' => {}
36
+ },
37
+ 'dependencies' => {
38
+ '$comment' => '"dependencies" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to "dependentSchemas" and "dependentRequired"',
39
+ 'type' => 'object',
40
+ 'additionalProperties' => {
41
+ 'anyOf' => [
42
+ { '$recursiveRef' => '#' },
43
+ { '$ref' => 'meta/validation#/$defs/stringArray' }
44
+ ]
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ module Meta
51
+ CORE = {
52
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
53
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/core',
54
+ '$recursiveAnchor' => true,
55
+ 'title' => 'Core vocabulary meta-schema',
56
+ 'type' => ['object', 'boolean'],
57
+ 'properties' => {
58
+ '$id' => {
59
+ 'type' => 'string',
60
+ 'format' => 'uri-reference',
61
+ '$comment' => 'Non-empty fragments not allowed.',
62
+ 'pattern' => '^[^#]*#?$'
63
+ },
64
+ '$schema' => {
65
+ 'type' => 'string',
66
+ 'format' => 'uri'
67
+ },
68
+ '$anchor' => {
69
+ 'type' => 'string',
70
+ 'pattern' => '^[A-Za-z][-A-Za-z0-9.:_]*$'
71
+ },
72
+ '$ref' => {
73
+ 'type' => 'string',
74
+ 'format' => 'uri-reference'
75
+ },
76
+ '$recursiveRef' => {
77
+ 'type' => 'string',
78
+ 'format' => 'uri-reference'
79
+ },
80
+ '$recursiveAnchor' => {
81
+ 'type' => 'boolean',
82
+ 'default' => false
83
+ },
84
+ '$vocabulary' => {
85
+ 'type' => 'object',
86
+ 'propertyNames' => {
87
+ 'type' => 'string',
88
+ 'format' => 'uri'
89
+ },
90
+ 'additionalProperties' => {
91
+ 'type' => 'boolean'
92
+ }
93
+ },
94
+ '$comment' => {
95
+ 'type' => 'string'
96
+ },
97
+ '$defs' => {
98
+ 'type' => 'object',
99
+ 'additionalProperties' => { '$recursiveRef' => '#' },
100
+ 'default' => {}
101
+ }
102
+ }
103
+ }
104
+
105
+ APPLICATOR = {
106
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
107
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/applicator',
108
+ '$recursiveAnchor' => true,
109
+ 'title' => 'Applicator vocabulary meta-schema',
110
+ 'type' => ['object', 'boolean'],
111
+ 'properties' => {
112
+ 'additionalItems' => { '$recursiveRef' => '#' },
113
+ 'unevaluatedItems' => { '$recursiveRef' => '#' },
114
+ 'items' => {
115
+ 'anyOf' => [
116
+ { '$recursiveRef' => '#' },
117
+ { '$ref' => '#/$defs/schemaArray' }
118
+ ]
119
+ },
120
+ 'contains' => { '$recursiveRef' => '#' },
121
+ 'additionalProperties' => { '$recursiveRef' => '#' },
122
+ 'unevaluatedProperties' => { '$recursiveRef' => '#' },
123
+ 'properties' => {
124
+ 'type' => 'object',
125
+ 'additionalProperties' => { '$recursiveRef' => '#' },
126
+ 'default' => {}
127
+ },
128
+ 'patternProperties' => {
129
+ 'type' => 'object',
130
+ 'additionalProperties' => { '$recursiveRef' => '#' },
131
+ 'propertyNames' => { 'format' => 'regex' },
132
+ 'default' => {}
133
+ },
134
+ 'dependentSchemas' => {
135
+ 'type' => 'object',
136
+ 'additionalProperties' => {
137
+ '$recursiveRef' => '#'
138
+ }
139
+ },
140
+ 'propertyNames' => { '$recursiveRef' => '#' },
141
+ 'if' => { '$recursiveRef' => '#' },
142
+ 'then' => { '$recursiveRef' => '#' },
143
+ 'else' => { '$recursiveRef' => '#' },
144
+ 'allOf' => { '$ref' => '#/$defs/schemaArray' },
145
+ 'anyOf' => { '$ref' => '#/$defs/schemaArray' },
146
+ 'oneOf' => { '$ref' => '#/$defs/schemaArray' },
147
+ 'not' => { '$recursiveRef' => '#' }
148
+ },
149
+ '$defs' => {
150
+ 'schemaArray' => {
151
+ 'type' => 'array',
152
+ 'minItems' => 1,
153
+ 'items' => { '$recursiveRef' => '#' }
154
+ }
155
+ }
156
+ }
157
+
158
+ VALIDATION = {
159
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
160
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/validation',
161
+ '$recursiveAnchor' => true,
162
+ 'title' => 'Validation vocabulary meta-schema',
163
+ 'type' => ['object', 'boolean'],
164
+ 'properties' => {
165
+ 'multipleOf' => {
166
+ 'type' => 'number',
167
+ 'exclusiveMinimum' => 0
168
+ },
169
+ 'maximum' => {
170
+ 'type' => 'number'
171
+ },
172
+ 'exclusiveMaximum' => {
173
+ 'type' => 'number'
174
+ },
175
+ 'minimum' => {
176
+ 'type' => 'number'
177
+ },
178
+ 'exclusiveMinimum' => {
179
+ 'type' => 'number'
180
+ },
181
+ 'maxLength' => { '$ref' => '#/$defs/nonNegativeInteger' },
182
+ 'minLength' => { '$ref' => '#/$defs/nonNegativeIntegerDefault0' },
183
+ 'pattern' => {
184
+ 'type' => 'string',
185
+ 'format' => 'regex'
186
+ },
187
+ 'maxItems' => { '$ref' => '#/$defs/nonNegativeInteger' },
188
+ 'minItems' => { '$ref' => '#/$defs/nonNegativeIntegerDefault0' },
189
+ 'uniqueItems' => {
190
+ 'type' => 'boolean',
191
+ 'default' => false
192
+ },
193
+ 'maxContains' => { '$ref' => '#/$defs/nonNegativeInteger' },
194
+ 'minContains' => {
195
+ '$ref' => '#/$defs/nonNegativeInteger',
196
+ 'default' => 1
197
+ },
198
+ 'maxProperties' => { '$ref' => '#/$defs/nonNegativeInteger' },
199
+ 'minProperties' => { '$ref' => '#/$defs/nonNegativeIntegerDefault0' },
200
+ 'required' => { '$ref' => '#/$defs/stringArray' },
201
+ 'dependentRequired' => {
202
+ 'type' => 'object',
203
+ 'additionalProperties' => {
204
+ '$ref' => '#/$defs/stringArray'
205
+ }
206
+ },
207
+ 'const' => true,
208
+ 'enum' => {
209
+ 'type' => 'array',
210
+ 'items' => true
211
+ },
212
+ 'type' => {
213
+ 'anyOf' => [
214
+ { '$ref' => '#/$defs/simpleTypes' },
215
+ {
216
+ 'type' => 'array',
217
+ 'items' => { '$ref' => '#/$defs/simpleTypes' },
218
+ 'minItems' => 1,
219
+ 'uniqueItems' => true
220
+ }
221
+ ]
222
+ }
223
+ },
224
+ '$defs' => {
225
+ 'nonNegativeInteger' => {
226
+ 'type' => 'integer',
227
+ 'minimum' => 0
228
+ },
229
+ 'nonNegativeIntegerDefault0' => {
230
+ '$ref' => '#/$defs/nonNegativeInteger',
231
+ 'default' => 0
232
+ },
233
+ 'simpleTypes' => {
234
+ 'enum' => [
235
+ 'array',
236
+ 'boolean',
237
+ 'integer',
238
+ 'null',
239
+ 'number',
240
+ 'object',
241
+ 'string'
242
+ ]
243
+ },
244
+ 'stringArray' => {
245
+ 'type' => 'array',
246
+ 'items' => { 'type' => 'string' },
247
+ 'uniqueItems' => true,
248
+ 'default' => []
249
+ }
250
+ }
251
+ }
252
+
253
+ META_DATA = {
254
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
255
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/meta-data',
256
+ '$recursiveAnchor' => true,
257
+ 'title' => 'Meta-data vocabulary meta-schema',
258
+ 'type' => ['object', 'boolean'],
259
+ 'properties' => {
260
+ 'title' => {
261
+ 'type' => 'string'
262
+ },
263
+ 'description' => {
264
+ 'type' => 'string'
265
+ },
266
+ 'default' => true,
267
+ 'deprecated' => {
268
+ 'type' => 'boolean',
269
+ 'default' => false
270
+ },
271
+ 'readOnly' => {
272
+ 'type' => 'boolean',
273
+ 'default' => false
274
+ },
275
+ 'writeOnly' => {
276
+ 'type' => 'boolean',
277
+ 'default' => false
278
+ },
279
+ 'examples' => {
280
+ 'type' => 'array',
281
+ 'items' => true
282
+ }
283
+ }
284
+ }
285
+
286
+ FORMAT = {
287
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
288
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/format',
289
+ '$recursiveAnchor' => true,
290
+ 'title' => 'Format vocabulary meta-schema',
291
+ 'type' => ['object', 'boolean'],
292
+ 'properties' => {
293
+ 'format' => { 'type' => 'string' }
294
+ }
295
+ }
296
+
297
+ CONTENT = {
298
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
299
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/content',
300
+ '$recursiveAnchor' => true,
301
+ 'title' => 'Content vocabulary meta-schema',
302
+ 'type' => ['object', 'boolean'],
303
+ 'properties' => {
304
+ 'contentMediaType' => { 'type' => 'string' },
305
+ 'contentEncoding' => { 'type' => 'string' },
306
+ 'contentSchema' => { '$recursiveRef' => '#' }
307
+ }
308
+ }
309
+
310
+ SCHEMAS = {
311
+ URI('https://json-schema.org/draft/2019-09/meta/core') => CORE,
312
+ URI('https://json-schema.org/draft/2019-09/meta/applicator') => APPLICATOR,
313
+ URI('https://json-schema.org/draft/2019-09/meta/validation') => VALIDATION,
314
+ URI('https://json-schema.org/draft/2019-09/meta/meta-data') => META_DATA,
315
+ URI('https://json-schema.org/draft/2019-09/meta/format') => FORMAT,
316
+ URI('https://json-schema.org/draft/2019-09/meta/content') => CONTENT
317
+ }
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft201909
4
+ module Vocab
5
+ module Applicator
6
+ class Items < Keyword
7
+ def error(formatted_instance_location:, **)
8
+ "array items at #{formatted_instance_location} do not match `items` schema(s)"
9
+ end
10
+
11
+ def parse
12
+ if value.is_a?(Array)
13
+ value.map.with_index do |subschema, index|
14
+ subschema(subschema, index.to_s)
15
+ end
16
+ else
17
+ subschema(value)
18
+ end
19
+ end
20
+
21
+ def validate(instance, instance_location, keyword_location, context)
22
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
23
+
24
+ nested = if parsed.is_a?(Array)
25
+ instance.take(parsed.size).map.with_index do |item, index|
26
+ parsed.fetch(index).validate_instance(item, join_location(instance_location, index.to_s), join_location(keyword_location, index.to_s), context)
27
+ end
28
+ else
29
+ instance.map.with_index do |item, index|
30
+ parsed.validate_instance(item, join_location(instance_location, index.to_s), keyword_location, context)
31
+ end
32
+ end
33
+
34
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => (nested.size - 1))
35
+ end
36
+ end
37
+
38
+ class AdditionalItems < Keyword
39
+ def error(formatted_instance_location:, **)
40
+ "array items at #{formatted_instance_location} do not match `additionalItems` schema"
41
+ end
42
+
43
+ def parse
44
+ subschema(value)
45
+ end
46
+
47
+ def validate(instance, instance_location, keyword_location, context)
48
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
49
+
50
+ evaluated_index = context.adjacent_results[Items]&.annotation
51
+ offset = evaluated_index ? (evaluated_index + 1) : instance.size
52
+
53
+ nested = instance.slice(offset..-1).map.with_index do |item, index|
54
+ parsed.validate_instance(item, join_location(instance_location, (offset + index).to_s), keyword_location, context)
55
+ end
56
+
57
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => nested.any?)
58
+ end
59
+ end
60
+
61
+ class UnevaluatedItems < Keyword
62
+ def error(formatted_instance_location:, **)
63
+ "array items at #{formatted_instance_location} do not match `unevaluatedItems` schema"
64
+ end
65
+
66
+ def parse
67
+ subschema(value)
68
+ end
69
+
70
+ def validate(instance, instance_location, keyword_location, context)
71
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
72
+
73
+ unevaluated_items = instance.size.times.to_set
74
+
75
+ context.adjacent_results.each_value do |adjacent_result|
76
+ collect_unevaluated_items(adjacent_result, instance_location, unevaluated_items)
77
+ end
78
+
79
+ nested = unevaluated_items.map do |index|
80
+ parsed.validate_instance(instance.fetch(index), join_location(instance_location, index.to_s), keyword_location, context)
81
+ end
82
+
83
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => nested.any?)
84
+ end
85
+
86
+ private
87
+
88
+ def collect_unevaluated_items(result, instance_location, unevaluated_items)
89
+ return unless result.valid && result.instance_location == instance_location
90
+ case result.source
91
+ when Items
92
+ unevaluated_items.subtract(0..result.annotation)
93
+ when AdditionalItems, UnevaluatedItems
94
+ unevaluated_items.clear if result.annotation
95
+ end
96
+ result.nested&.each do |nested_result|
97
+ collect_unevaluated_items(nested_result, instance_location, unevaluated_items)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft201909
4
+ module Vocab
5
+ module Core
6
+ class RecursiveAnchor < Keyword
7
+ def parse
8
+ root.resources[:dynamic][schema.base_uri] = schema if value == true
9
+ value
10
+ end
11
+ end
12
+
13
+ class RecursiveRef < Keyword
14
+ def ref_uri
15
+ @ref_uri ||= URI.join(schema.base_uri, value)
16
+ end
17
+
18
+ def ref_schema
19
+ @ref_schema ||= root.resolve_ref(ref_uri)
20
+ end
21
+
22
+ def recursive_anchor
23
+ return @recursive_anchor if defined?(@recursive_anchor)
24
+ @recursive_anchor = (ref_schema.parsed['$recursiveAnchor']&.parsed == true)
25
+ end
26
+
27
+ def validate(instance, instance_location, keyword_location, context)
28
+ schema = ref_schema
29
+
30
+ if recursive_anchor
31
+ context.dynamic_scope.each do |ancestor|
32
+ if ancestor.root.resources.fetch(:dynamic).key?(ancestor.base_uri)
33
+ schema = ancestor.root.resources.fetch(:dynamic).fetch(ancestor.base_uri)
34
+ break
35
+ end
36
+ end
37
+ end
38
+
39
+ schema.validate_instance(instance, instance_location, keyword_location, context)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft201909
4
+ module Vocab
5
+ CORE = Draft202012::Vocab::CORE.dup
6
+ CORE.delete('$dynamicAnchor')
7
+ CORE.delete('$dynamicRef')
8
+ CORE.merge!(
9
+ # https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-02#section-8.2.4.2
10
+ '$recursiveAnchor' => Core::RecursiveAnchor,
11
+ '$recursiveRef' => Core::RecursiveRef
12
+ )
13
+
14
+ APPLICATOR = Draft202012::Vocab::APPLICATOR.dup
15
+ APPLICATOR.delete('prefixItems')
16
+ APPLICATOR.merge!(
17
+ # https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-02#section-9.3.1
18
+ 'items' => Applicator::Items,
19
+ 'additionalItems' => Applicator::AdditionalItems,
20
+ 'unevaluatedItems' => Applicator::UnevaluatedItems,
21
+ # https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-02#section-9.3.2.4
22
+ 'unevaluatedProperties' => Draft202012::Vocab::Unevaluated::UnevaluatedProperties
23
+ )
24
+
25
+ VALIDATION = Draft202012::Vocab::VALIDATION
26
+ FORMAT = Draft202012::Vocab::FORMAT_ANNOTATION
27
+ CONTENT = Draft202012::Vocab::CONTENT
28
+ META_DATA = Draft202012::Vocab::META_DATA
29
+ end
30
+ end
31
+ end