json_schemer 1.0.3 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -7
  3. data/CHANGELOG.md +42 -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 +45 -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 +1673 -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 +1559 -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 +424 -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