schemurai 1.0.0 → 2.1.0
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.
- checksums.yaml +4 -4
- data/README.md +95 -3
- data/lib/schemurai/backend.rb +32 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluator.rb +30 -23
- data/lib/schemurai/meta_schemas/draft2019_09.rb +103 -2
- data/lib/schemurai/meta_schemas/draft2020_12.rb +103 -2
- data/lib/schemurai/schema_domain.rb +82 -0
- data/lib/schemurai/schema_graph.rb +21 -6
- data/lib/schemurai/version.rb +2 -1
- data/lib/schemurai/vm/compiler.rb +377 -0
- data/lib/schemurai/vm/evaluator.rb +811 -0
- data/lib/schemurai.rb +167 -18
- metadata +21 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1b157d6e772537cab2886ef6d89bd547992c7359b73a13a49b9d9cfcdcc9808
|
|
4
|
+
data.tar.gz: f0e8e28536c58c95cf1848db949dad8007df0c7a6550dd8b0fd76439841617cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6f8206124aab29ae13908951efdd011e276ca347a75c0abf79da9fb3a9988f91a0a59b0ec559b85a88f01d5975b9ea251f9f14bb7aba634b7c8f3625c288167
|
|
7
|
+
data.tar.gz: 3fcd551c456cb81b53842c0736ae9e629676ca8c7c0e43017ee43991b00b995fde7a530f1c0cb38f81d5e7bb640b68a8ad8ad388934af73b06b4ef77c4e46a34
|
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Schemurai
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/schemurai)
|
|
4
|
+
[](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml)
|
|
5
|
+
[](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml)
|
|
6
|
+
|
|
3
7
|
A small, light-weight-dependency JSON Schema validator for Ruby supporting Draft 7,
|
|
4
8
|
Draft 2019-09, and Draft 2020-12. It covers the required cases in the official
|
|
5
9
|
JSON-Schema-Test-Suite, as well as the applicable optional tests for numeric
|
|
@@ -93,6 +97,11 @@ validator. Repeatedly compiling the same schema object with one registry reuses
|
|
|
93
97
|
its compiled schema graph. `Schemurai.validate` and `.valid?` continue
|
|
94
98
|
to accept raw JSON-like schemas and perform compilation internally.
|
|
95
99
|
|
|
100
|
+
Schema inputs are checked recursively before compilation. They must use the
|
|
101
|
+
built-in JSON-shaped Ruby classes described in
|
|
102
|
+
[`docs/compatibility-domain.md`](docs/compatibility-domain.md); rejected values
|
|
103
|
+
are never retained by a registry.
|
|
104
|
+
|
|
96
105
|
To resolve external references, pass a mapping of URIs to schemas using
|
|
97
106
|
`schemas:`.
|
|
98
107
|
|
|
@@ -111,7 +120,39 @@ validation for `contentEncoding` and `contentMediaType` with `content: true`.
|
|
|
111
120
|
Enable optional format assertions with `format: true`; support for each format is
|
|
112
121
|
listed separately below.
|
|
113
122
|
|
|
114
|
-
###
|
|
123
|
+
### Validation errors
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
result = Schemurai.validate(
|
|
127
|
+
{ "$ref" => "https://example.test/positive" },
|
|
128
|
+
-1,
|
|
129
|
+
schemas: { "https://example.test/positive" => { "type" => "integer", "minimum" => 1 } }
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
validation_error = result.errors[0]
|
|
133
|
+
p validation_error.keyword # => "minimum"
|
|
134
|
+
p validation_error.instance_path # => ""
|
|
135
|
+
p validation_error.schema_path # => "/$ref/minimum"
|
|
136
|
+
p validation_error.message # => "number must be greater than or equal to 1"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`Result#errors` contains `Schemurai::ValidationError` objects.
|
|
140
|
+
Check the following attributes to investigate schema errors:
|
|
141
|
+
|
|
142
|
+
- `keyword` identifies the failed JSON Schema keyword. Schemurai uses
|
|
143
|
+
`falseSchema` for a boolean `false` schema, which has no keyword of its own.
|
|
144
|
+
- `instance_path` is a JSON Pointer to the value that failed validation.
|
|
145
|
+
- `schema_path` is a JSON Pointer to the schema location that produced the
|
|
146
|
+
error.
|
|
147
|
+
- `message` is a human-readable `String` describing the failure.
|
|
148
|
+
|
|
149
|
+
An empty path points to the instance or schema root. Error order is stable.
|
|
150
|
+
The presence and type of `message` are part of the public API, but its exact
|
|
151
|
+
wording is not. Use `keyword`, `instance_path`, and `schema_path`, rather than
|
|
152
|
+
matching `message`, when handling errors programmatically. `ValidationError#to_h`
|
|
153
|
+
returns these four attributes as a Hash.
|
|
154
|
+
|
|
155
|
+
### Thread / Ractor support
|
|
115
156
|
|
|
116
157
|
To share a registry between threads or Ractors, finish registering schemas and
|
|
117
158
|
make the registry shareable first. This eagerly compiles every registered
|
|
@@ -122,7 +163,9 @@ registry = Schemurai::SchemaRegistry.new(
|
|
|
122
163
|
schemas: {
|
|
123
164
|
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 },
|
|
124
165
|
"https://example.test/value" => { "$ref" => "https://example.test/positive" }
|
|
125
|
-
}
|
|
166
|
+
},
|
|
167
|
+
# enable virtual machine backend that is faster for iterative validation.
|
|
168
|
+
backend: :vm
|
|
126
169
|
)
|
|
127
170
|
registry.make_shareable
|
|
128
171
|
|
|
@@ -134,7 +177,46 @@ validator = registry.validator_for("https://example.test/value")
|
|
|
134
177
|
`ResolutionError` if a reference cannot be resolved. After it returns,
|
|
135
178
|
`validator_for` is read-only and may be called concurrently, while `compile` is
|
|
136
179
|
no longer available. A `Validator` contains per-validation mutable state and
|
|
137
|
-
must not be shared between threads or Ractors.
|
|
180
|
+
must not be shared between threads or Ractors. With the VM backend, the registry
|
|
181
|
+
compiles and shares its bytecode before this transition, so those validators do
|
|
182
|
+
not compile separate instruction streams in each Ractor.
|
|
183
|
+
|
|
184
|
+
### Meta-schema validation
|
|
185
|
+
|
|
186
|
+
Meta-schema validation is opt-in so ordinary compilation does not pay its
|
|
187
|
+
additional time and memory cost. Set `validate_schema: true` when creating a
|
|
188
|
+
`SchemaRegistry` to validate every schema registered through `schemas:` and
|
|
189
|
+
every schema passed to `compile`. The convenience `compile`, `validate`, and
|
|
190
|
+
`valid?` methods accept the same option and apply it to their internal registry.
|
|
191
|
+
Schemas without `$schema` use the Draft 7 meta-schema. An invalid schema raises
|
|
192
|
+
`Schemurai::InvalidSchemaError`, whose `result` contains the detailed validation
|
|
193
|
+
errors.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
Schemurai.compile(schema, validate_schema: true)
|
|
197
|
+
|
|
198
|
+
registry = Schemurai::SchemaRegistry.new(
|
|
199
|
+
schemas: external_schemas,
|
|
200
|
+
validate_schema: true
|
|
201
|
+
)
|
|
202
|
+
validator = registry.compile(schema)
|
|
203
|
+
|
|
204
|
+
result = Schemurai.validate_schema(schema)
|
|
205
|
+
result.valid? # whether the schema itself is valid
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`SchemaRegistry#validate_schema` and `#valid_schema?` perform explicit checks
|
|
209
|
+
regardless of the registry option and reuse meta-schema graph data within that
|
|
210
|
+
registry. The stateful validator used for a check is temporary; no global
|
|
211
|
+
validator is retained.
|
|
212
|
+
|
|
213
|
+
### Backend selection
|
|
214
|
+
|
|
215
|
+
`Schemurai.backend`, `SchemaRegistry#backend`, and `Validator#backend` expose
|
|
216
|
+
the actual backend. Pass `backend: :ruby` to force the Ruby oracle or
|
|
217
|
+
`backend: :vm` to ahead-of-time compile schemas for the validator virtual machine.
|
|
218
|
+
Schema graph construction remains shared Ruby infrastructure. Environment-based selection
|
|
219
|
+
is documented in [`docs/backend-selection.md`](docs/backend-selection.md).
|
|
138
220
|
|
|
139
221
|
## JSON Schema conformance
|
|
140
222
|
|
|
@@ -195,6 +277,16 @@ Run the test suite with:
|
|
|
195
277
|
bundle exec rspec
|
|
196
278
|
```
|
|
197
279
|
|
|
280
|
+
Verify the reviewed official-suite classifications with:
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
ruby script/oracle-cases --summary
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The serialized oracle tools are `script/oracle-runner` and
|
|
287
|
+
`script/oracle-compare`. Ruby interpreter and YJIT baseline instructions are in
|
|
288
|
+
[`benchmark/baselines/README.md`](benchmark/baselines/README.md).
|
|
289
|
+
|
|
198
290
|
Run the linter with:
|
|
199
291
|
|
|
200
292
|
```sh
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Backend
|
|
5
|
+
VM_FEATURE = "vm/evaluator"
|
|
6
|
+
CHOICES = %i[default ruby vm].freeze
|
|
7
|
+
|
|
8
|
+
module_function def requested
|
|
9
|
+
value = ENV.fetch("SCHEMURAI_BACKEND", "default").to_sym
|
|
10
|
+
return value if CHOICES.include?(value)
|
|
11
|
+
|
|
12
|
+
raise Error, "unknown Schemurai backend #{value.inspect}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module_function def resolve(selection = requested)
|
|
16
|
+
selection = selection.to_sym
|
|
17
|
+
raise Error, "unknown Schemurai backend #{selection.inspect}" unless CHOICES.include?(selection)
|
|
18
|
+
|
|
19
|
+
selection = production_default if selection == :default
|
|
20
|
+
load_vm! if selection == :vm
|
|
21
|
+
selection
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module_function def production_default
|
|
25
|
+
:ruby
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module_function def load_vm!
|
|
29
|
+
require_relative VM_FEATURE
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
# Builds validation messages shared by every evaluator backend.
|
|
6
|
+
module ErrorMessage
|
|
7
|
+
module_function def false_schema = "value is not allowed by this schema"
|
|
8
|
+
|
|
9
|
+
module_function def type(expected, value)
|
|
10
|
+
expected = Array(expected).map { |name| name.inspect }.join(" or ")
|
|
11
|
+
"value has type #{json_type(value).inspect}, but must have type #{expected}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module_function def enum = "value must equal one of the values defined by `enum`"
|
|
15
|
+
module_function def const = "value must equal the value defined by `const`"
|
|
16
|
+
|
|
17
|
+
module_function def numeric_limit(keyword, limit)
|
|
18
|
+
comparison = {
|
|
19
|
+
"maximum" => "less than or equal to",
|
|
20
|
+
"minimum" => "greater than or equal to",
|
|
21
|
+
"exclusiveMaximum" => "less than",
|
|
22
|
+
"exclusiveMinimum" => "greater than"
|
|
23
|
+
}.fetch(keyword)
|
|
24
|
+
"number must be #{comparison} #{display_number(limit)}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module_function def multiple_of(divisor) = "number must be a multiple of #{display_number(divisor)}"
|
|
28
|
+
|
|
29
|
+
module_function def size(keyword, limit, actual)
|
|
30
|
+
subject, unit, comparison = {
|
|
31
|
+
"maxLength" => ["string", "characters", "at most"],
|
|
32
|
+
"minLength" => ["string", "characters", "at least"],
|
|
33
|
+
"maxItems" => ["array", "items", "at most"],
|
|
34
|
+
"minItems" => ["array", "items", "at least"],
|
|
35
|
+
"maxProperties" => ["object", "properties", "at most"],
|
|
36
|
+
"minProperties" => ["object", "properties", "at least"]
|
|
37
|
+
}.fetch(keyword)
|
|
38
|
+
unit = {"characters" => "character", "items" => "item", "properties" => "property"}.fetch(unit) if limit == 1
|
|
39
|
+
"#{subject} must contain #{comparison} #{limit} #{unit} (found #{actual})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module_function def pattern(pattern) = "string must match pattern #{pattern.inspect}"
|
|
43
|
+
module_function def invalid_pattern(pattern) = "schema pattern #{pattern.inspect} is not a valid regular expression"
|
|
44
|
+
module_function def format(name) = "string must match the #{name} format"
|
|
45
|
+
module_function def content_encoding = "string must be valid base64"
|
|
46
|
+
module_function def content_media_type = "string must contain valid JSON"
|
|
47
|
+
module_function def unique_items = "array items must be unique"
|
|
48
|
+
|
|
49
|
+
module_function def contains(actual, minimum, maximum)
|
|
50
|
+
expected = if maximum.infinite?
|
|
51
|
+
"at least #{minimum}"
|
|
52
|
+
elsif minimum == maximum
|
|
53
|
+
"exactly #{minimum}"
|
|
54
|
+
elsif minimum.zero?
|
|
55
|
+
"at most #{maximum}"
|
|
56
|
+
else
|
|
57
|
+
"between #{minimum} and #{maximum}"
|
|
58
|
+
end
|
|
59
|
+
"array must contain #{expected} items matching `contains` (found #{actual})"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
module_function def required(name) = "object is missing required property #{name.inspect}"
|
|
63
|
+
|
|
64
|
+
module_function def dependent_required(name, required_name)
|
|
65
|
+
"property #{required_name.inspect} is required when property #{name.inspect} is present"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module_function def any_of = "value must match at least one subschema"
|
|
69
|
+
module_function def one_of(matches) = "value must match exactly one subschema (matched #{matches})"
|
|
70
|
+
module_function def not = "value must not match the subschema"
|
|
71
|
+
|
|
72
|
+
module_function def json_type(value)
|
|
73
|
+
case value
|
|
74
|
+
when nil then "null"
|
|
75
|
+
when true, false then "boolean"
|
|
76
|
+
when Hash then "object"
|
|
77
|
+
when Array then "array"
|
|
78
|
+
when String then "string"
|
|
79
|
+
when Numeric
|
|
80
|
+
if value.is_a?(Complex)
|
|
81
|
+
value.class.name
|
|
82
|
+
elsif value.finite? && value.to_i == value
|
|
83
|
+
"integer"
|
|
84
|
+
else
|
|
85
|
+
"number"
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
value.class.name
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
private_class_method :json_type
|
|
92
|
+
|
|
93
|
+
module_function def display_number(value)
|
|
94
|
+
rational = value.is_a?(Rational) ? value : Rational(value.to_s)
|
|
95
|
+
return rational.numerator.to_s if rational.denominator == 1
|
|
96
|
+
|
|
97
|
+
rational.to_f.to_s
|
|
98
|
+
end
|
|
99
|
+
private_class_method :display_number
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/schemurai/evaluator.rb
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "base64"
|
|
5
5
|
require_relative "evaluation"
|
|
6
|
+
require_relative "error_message"
|
|
6
7
|
|
|
7
8
|
module Schemurai
|
|
8
9
|
module Internal
|
|
9
10
|
class Evaluator
|
|
10
11
|
MISSING_SEGMENT = Object.new.freeze
|
|
11
12
|
|
|
13
|
+
def backend = :ruby
|
|
14
|
+
|
|
12
15
|
def initialize(graph, root, content: false, format: false)
|
|
13
16
|
@validate_content = content
|
|
14
17
|
@validate_format = format
|
|
@@ -28,6 +31,7 @@ module Schemurai
|
|
|
28
31
|
@error_callback = nil
|
|
29
32
|
@error_count = 0
|
|
30
33
|
@track_dynamic_scope = @graph.dynamic_scope?
|
|
34
|
+
@dynamic_scope = nil
|
|
31
35
|
@instance_path = nil
|
|
32
36
|
@schema_path = nil
|
|
33
37
|
evaluate_valid(@root, instance)
|
|
@@ -37,6 +41,7 @@ module Schemurai
|
|
|
37
41
|
@error_callback = callback
|
|
38
42
|
@error_count = 0
|
|
39
43
|
@track_dynamic_scope = @graph.dynamic_scope?
|
|
44
|
+
@dynamic_scope = nil
|
|
40
45
|
@instance_path = []
|
|
41
46
|
@schema_path = []
|
|
42
47
|
evaluate(@root, instance)
|
|
@@ -329,7 +334,7 @@ module Schemurai
|
|
|
329
334
|
schema = node.schema
|
|
330
335
|
return Evaluation.valid if schema == true
|
|
331
336
|
if schema == false
|
|
332
|
-
add_error("falseSchema",
|
|
337
|
+
add_error("falseSchema", append_keyword: false) { ErrorMessage.false_schema }
|
|
333
338
|
return Evaluation.invalid
|
|
334
339
|
end
|
|
335
340
|
return Evaluation.valid unless schema.is_a?(Hash)
|
|
@@ -411,7 +416,7 @@ module Schemurai
|
|
|
411
416
|
target = @graph.resolve(node, reference)
|
|
412
417
|
return target unless reference.to_s.end_with?("#") && target.schema.is_a?(Hash) && target.schema["$recursiveAnchor"] == true
|
|
413
418
|
|
|
414
|
-
@dynamic_scope.filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
|
|
419
|
+
Array(@dynamic_scope).filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
|
|
415
420
|
end
|
|
416
421
|
|
|
417
422
|
private def dynamic_target(node, reference)
|
|
@@ -420,7 +425,7 @@ module Schemurai
|
|
|
420
425
|
return target if raw_fragment.nil? || raw_fragment.empty? || raw_fragment.start_with?("/")
|
|
421
426
|
return target unless target.schema.is_a?(Hash) && target.schema["$dynamicAnchor"] == raw_fragment
|
|
422
427
|
|
|
423
|
-
@dynamic_scope.each do |resource|
|
|
428
|
+
Array(@dynamic_scope).each do |resource|
|
|
424
429
|
dynamic = @graph.dynamic_anchor(resource, raw_fragment)
|
|
425
430
|
return dynamic if dynamic
|
|
426
431
|
end
|
|
@@ -438,15 +443,15 @@ module Schemurai
|
|
|
438
443
|
types = Array(schema["type"])
|
|
439
444
|
return if types.any? { |type| type?(value, type) }
|
|
440
445
|
|
|
441
|
-
add_error("type"
|
|
446
|
+
add_error("type") { ErrorMessage.type(types, value) }
|
|
442
447
|
end
|
|
443
448
|
|
|
444
449
|
private def check_enum(schema, value)
|
|
445
450
|
if schema.key?("enum") && !schema["enum"].any? { |candidate| json_equal?(candidate, value) }
|
|
446
|
-
add_error("enum"
|
|
451
|
+
add_error("enum") { ErrorMessage.enum }
|
|
447
452
|
end
|
|
448
453
|
if schema.key?("const") && !json_equal?(schema["const"], value)
|
|
449
|
-
add_error("const"
|
|
454
|
+
add_error("const") { ErrorMessage.const }
|
|
450
455
|
end
|
|
451
456
|
end
|
|
452
457
|
|
|
@@ -468,7 +473,7 @@ module Schemurai
|
|
|
468
473
|
matches << result if result.valid?
|
|
469
474
|
end
|
|
470
475
|
if matches.empty?
|
|
471
|
-
add_error("anyOf"
|
|
476
|
+
add_error("anyOf") { ErrorMessage.any_of }
|
|
472
477
|
else
|
|
473
478
|
matches.each { |result| evaluation = evaluation.merge(result) }
|
|
474
479
|
end
|
|
@@ -483,12 +488,12 @@ module Schemurai
|
|
|
483
488
|
if matches.length == 1
|
|
484
489
|
evaluation = evaluation.merge(matches.first)
|
|
485
490
|
else
|
|
486
|
-
add_error("oneOf"
|
|
491
|
+
add_error("oneOf") { ErrorMessage.one_of(matches.length) }
|
|
487
492
|
end
|
|
488
493
|
end
|
|
489
494
|
|
|
490
495
|
if schema.key?("not") && trial_at(node.child("not"), value, MISSING_SEGMENT, "not").valid?
|
|
491
|
-
add_error("not"
|
|
496
|
+
add_error("not") { ErrorMessage.not }
|
|
492
497
|
end
|
|
493
498
|
|
|
494
499
|
if schema.key?("if")
|
|
@@ -512,14 +517,14 @@ module Schemurai
|
|
|
512
517
|
|
|
513
518
|
divisor = decimal(schema["multipleOf"])
|
|
514
519
|
valid = divisor.positive? && decimal(value).remainder(divisor).zero?
|
|
515
|
-
add_error("multipleOf"
|
|
520
|
+
add_error("multipleOf") { ErrorMessage.multiple_of(schema["multipleOf"]) } unless valid
|
|
516
521
|
end
|
|
517
522
|
|
|
518
523
|
private def compare(schema, keyword, value)
|
|
519
524
|
return unless schema.key?(keyword)
|
|
520
525
|
return if yield(decimal(value), decimal(schema[keyword]))
|
|
521
526
|
|
|
522
|
-
add_error(keyword
|
|
527
|
+
add_error(keyword) { ErrorMessage.numeric_limit(keyword, schema[keyword]) }
|
|
523
528
|
end
|
|
524
529
|
|
|
525
530
|
private def check_string(node, value)
|
|
@@ -530,14 +535,14 @@ module Schemurai
|
|
|
530
535
|
|
|
531
536
|
if schema.key?("pattern")
|
|
532
537
|
matched = ecma_regexp(schema["pattern"]).match?(value)
|
|
533
|
-
add_error("pattern"
|
|
538
|
+
add_error("pattern") { ErrorMessage.pattern(schema["pattern"]) } unless matched
|
|
534
539
|
end
|
|
535
540
|
if format_asserted?(node)
|
|
536
|
-
add_error("format"
|
|
541
|
+
add_error("format") { ErrorMessage.format(node.format.name) } unless node.format.call(value)
|
|
537
542
|
end
|
|
538
543
|
check_content(schema, value) if @validate_content
|
|
539
544
|
rescue RegexpError
|
|
540
|
-
add_error("pattern"
|
|
545
|
+
add_error("pattern") { ErrorMessage.invalid_pattern(schema["pattern"]) }
|
|
541
546
|
end
|
|
542
547
|
|
|
543
548
|
private def format_asserted?(node)
|
|
@@ -554,7 +559,7 @@ module Schemurai
|
|
|
554
559
|
duplicate = value.each_with_index.any? do |item, index|
|
|
555
560
|
value[0...index].any? { |previous| json_equal?(previous, item) }
|
|
556
561
|
end
|
|
557
|
-
add_error("uniqueItems"
|
|
562
|
+
add_error("uniqueItems") { ErrorMessage.unique_items } if duplicate
|
|
558
563
|
end
|
|
559
564
|
|
|
560
565
|
prefix_items = schema["prefixItems"]
|
|
@@ -596,7 +601,7 @@ module Schemurai
|
|
|
596
601
|
minimum = schema.fetch("minContains", 1)
|
|
597
602
|
maximum = schema.fetch("maxContains", Float::INFINITY)
|
|
598
603
|
unless matched.length.between?(minimum, maximum)
|
|
599
|
-
add_error("contains"
|
|
604
|
+
add_error("contains") { ErrorMessage.contains(matched.length, minimum, maximum) }
|
|
600
605
|
end
|
|
601
606
|
evaluated.concat(matched)
|
|
602
607
|
end
|
|
@@ -619,7 +624,7 @@ module Schemurai
|
|
|
619
624
|
limit(schema, "minProperties", value.length) { |actual, expected| actual >= expected }
|
|
620
625
|
|
|
621
626
|
Array(schema["required"]).each do |name|
|
|
622
|
-
add_error("required"
|
|
627
|
+
add_error("required") { ErrorMessage.required(name) } unless value.key?(name)
|
|
623
628
|
end
|
|
624
629
|
|
|
625
630
|
properties = schema.fetch("properties", {})
|
|
@@ -653,7 +658,7 @@ module Schemurai
|
|
|
653
658
|
next unless value.key?(name)
|
|
654
659
|
if dependency.is_a?(Array)
|
|
655
660
|
dependency.each do |required_name|
|
|
656
|
-
add_error("dependencies"
|
|
661
|
+
add_error("dependencies") { ErrorMessage.dependent_required(name, required_name) } unless value.key?(required_name)
|
|
657
662
|
end
|
|
658
663
|
else
|
|
659
664
|
result = evaluate_at(node.child("dependencies", name), value, MISSING_SEGMENT, "dependencies", name)
|
|
@@ -665,7 +670,7 @@ module Schemurai
|
|
|
665
670
|
next unless value.key?(name)
|
|
666
671
|
required_names.each do |required_name|
|
|
667
672
|
unless value.key?(required_name)
|
|
668
|
-
add_error("dependentRequired"
|
|
673
|
+
add_error("dependentRequired") { ErrorMessage.dependent_required(name, required_name) }
|
|
669
674
|
end
|
|
670
675
|
end
|
|
671
676
|
end
|
|
@@ -703,7 +708,7 @@ module Schemurai
|
|
|
703
708
|
return unless schema.key?(keyword)
|
|
704
709
|
return if yield(actual, schema[keyword])
|
|
705
710
|
|
|
706
|
-
add_error(keyword
|
|
711
|
+
add_error(keyword) { ErrorMessage.size(keyword, schema[keyword], actual) }
|
|
707
712
|
end
|
|
708
713
|
|
|
709
714
|
private def type?(value, type)
|
|
@@ -799,10 +804,12 @@ module Schemurai
|
|
|
799
804
|
JSON.parse(decoded)
|
|
800
805
|
rescue ArgumentError, JSON::ParserError
|
|
801
806
|
keyword = (schema["contentEncoding"] == "base64") ? "contentEncoding" : "contentMediaType"
|
|
802
|
-
add_error(keyword
|
|
807
|
+
add_error(keyword) do
|
|
808
|
+
(keyword == "contentEncoding") ? ErrorMessage.content_encoding : ErrorMessage.content_media_type
|
|
809
|
+
end
|
|
803
810
|
end
|
|
804
811
|
|
|
805
|
-
private def add_error(keyword, message, append_keyword: true)
|
|
812
|
+
private def add_error(keyword, message = nil, append_keyword: true)
|
|
806
813
|
@error_count += 1
|
|
807
814
|
if @error_callback
|
|
808
815
|
schema_keyword = append_keyword ? keyword : MISSING_SEGMENT
|
|
@@ -811,7 +818,7 @@ module Schemurai
|
|
|
811
818
|
keyword: keyword,
|
|
812
819
|
instance_path: pointer(@instance_path),
|
|
813
820
|
schema_path: pointer(@schema_path, schema_keyword),
|
|
814
|
-
message: message
|
|
821
|
+
message: message || yield
|
|
815
822
|
)
|
|
816
823
|
)
|
|
817
824
|
end
|
|
@@ -13,8 +13,47 @@ module Schemurai
|
|
|
13
13
|
"$recursiveAnchor" => true,
|
|
14
14
|
"type" => ["object", "boolean"],
|
|
15
15
|
"properties" => {
|
|
16
|
+
"$id" => {"type" => "string"},
|
|
17
|
+
"$schema" => {"type" => "string"},
|
|
18
|
+
"$ref" => {"type" => "string"},
|
|
19
|
+
"$anchor" => {"type" => "string", "pattern" => "^[A-Za-z][-A-Za-z0-9.:_]*$"},
|
|
20
|
+
"$recursiveRef" => {"type" => "string"},
|
|
21
|
+
"$recursiveAnchor" => {"type" => "boolean"},
|
|
22
|
+
"$vocabulary" => {"type" => "object", "additionalProperties" => {"type" => "boolean"}},
|
|
23
|
+
"$comment" => {"type" => "string"},
|
|
16
24
|
"$defs" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
|
|
17
25
|
"definitions" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
|
|
26
|
+
"items" => {
|
|
27
|
+
"anyOf" => [
|
|
28
|
+
{"$recursiveRef" => "#"},
|
|
29
|
+
{"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"additionalItems" => {"$recursiveRef" => "#"},
|
|
33
|
+
"contains" => {"$recursiveRef" => "#"},
|
|
34
|
+
"additionalProperties" => {"$recursiveRef" => "#"},
|
|
35
|
+
"unevaluatedItems" => {"$recursiveRef" => "#"},
|
|
36
|
+
"unevaluatedProperties" => {"$recursiveRef" => "#"},
|
|
37
|
+
"properties" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
|
|
38
|
+
"patternProperties" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
|
|
39
|
+
"dependentSchemas" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
|
|
40
|
+
"dependencies" => {
|
|
41
|
+
"type" => "object",
|
|
42
|
+
"additionalProperties" => {
|
|
43
|
+
"anyOf" => [
|
|
44
|
+
{"$recursiveRef" => "#"},
|
|
45
|
+
{"type" => "array", "items" => {"type" => "string"}, "uniqueItems" => true}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"propertyNames" => {"$recursiveRef" => "#"},
|
|
50
|
+
"if" => {"$recursiveRef" => "#"},
|
|
51
|
+
"then" => {"$recursiveRef" => "#"},
|
|
52
|
+
"else" => {"$recursiveRef" => "#"},
|
|
53
|
+
"allOf" => {"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}},
|
|
54
|
+
"anyOf" => {"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}},
|
|
55
|
+
"oneOf" => {"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}},
|
|
56
|
+
"not" => {"$recursiveRef" => "#"},
|
|
18
57
|
"type" => {
|
|
19
58
|
"anyOf" => [
|
|
20
59
|
{"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
|
|
@@ -26,20 +65,82 @@ module Schemurai
|
|
|
26
65
|
}
|
|
27
66
|
]
|
|
28
67
|
},
|
|
68
|
+
"enum" => {"type" => "array"},
|
|
69
|
+
"multipleOf" => {"type" => "number", "exclusiveMinimum" => 0},
|
|
70
|
+
"maximum" => {"type" => "number"},
|
|
71
|
+
"exclusiveMaximum" => {"type" => "number"},
|
|
72
|
+
"minimum" => {"type" => "number"},
|
|
73
|
+
"exclusiveMinimum" => {"type" => "number"},
|
|
29
74
|
"minLength" => {"type" => "integer", "minimum" => 0},
|
|
30
75
|
"maxLength" => {"type" => "integer", "minimum" => 0},
|
|
76
|
+
"pattern" => {"type" => "string"},
|
|
31
77
|
"minItems" => {"type" => "integer", "minimum" => 0},
|
|
32
78
|
"maxItems" => {"type" => "integer", "minimum" => 0},
|
|
79
|
+
"uniqueItems" => {"type" => "boolean"},
|
|
33
80
|
"minContains" => {"type" => "integer", "minimum" => 0},
|
|
34
81
|
"maxContains" => {"type" => "integer", "minimum" => 0},
|
|
35
82
|
"minProperties" => {"type" => "integer", "minimum" => 0},
|
|
36
|
-
"maxProperties" => {"type" => "integer", "minimum" => 0}
|
|
83
|
+
"maxProperties" => {"type" => "integer", "minimum" => 0},
|
|
84
|
+
"required" => {
|
|
85
|
+
"type" => "array",
|
|
86
|
+
"items" => {"type" => "string"},
|
|
87
|
+
"uniqueItems" => true
|
|
88
|
+
},
|
|
89
|
+
"dependentRequired" => {
|
|
90
|
+
"type" => "object",
|
|
91
|
+
"additionalProperties" => {
|
|
92
|
+
"type" => "array",
|
|
93
|
+
"items" => {"type" => "string"},
|
|
94
|
+
"uniqueItems" => true
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"title" => {"type" => "string"},
|
|
98
|
+
"description" => {"type" => "string"},
|
|
99
|
+
"deprecated" => {"type" => "boolean"},
|
|
100
|
+
"readOnly" => {"type" => "boolean"},
|
|
101
|
+
"writeOnly" => {"type" => "boolean"},
|
|
102
|
+
"examples" => {"type" => "array"},
|
|
103
|
+
"format" => {"type" => "string"},
|
|
104
|
+
"contentEncoding" => {"type" => "string"},
|
|
105
|
+
"contentMediaType" => {"type" => "string"},
|
|
106
|
+
"contentSchema" => {"$recursiveRef" => "#"}
|
|
37
107
|
}
|
|
38
108
|
}.freeze
|
|
39
109
|
|
|
40
110
|
MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
|
|
41
111
|
|
|
42
|
-
|
|
112
|
+
COMPONENT_KEYWORDS = {
|
|
113
|
+
"core" => %w[
|
|
114
|
+
$id $schema $ref $anchor $recursiveRef $recursiveAnchor $vocabulary $comment $defs
|
|
115
|
+
],
|
|
116
|
+
"applicator" => %w[
|
|
117
|
+
additionalItems unevaluatedItems items contains additionalProperties
|
|
118
|
+
unevaluatedProperties properties patternProperties dependentSchemas propertyNames
|
|
119
|
+
if then else allOf anyOf oneOf not
|
|
120
|
+
],
|
|
121
|
+
"validation" => %w[
|
|
122
|
+
type const enum multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
|
|
123
|
+
maxLength minLength pattern maxItems minItems uniqueItems maxContains minContains
|
|
124
|
+
maxProperties minProperties required dependentRequired
|
|
125
|
+
],
|
|
126
|
+
"meta-data" => %w[title description default deprecated readOnly writeOnly examples],
|
|
127
|
+
"format" => %w[format],
|
|
128
|
+
"content" => %w[contentEncoding contentMediaType contentSchema]
|
|
129
|
+
}.freeze
|
|
130
|
+
|
|
131
|
+
COMPONENT_KEYWORDS.each do |name, keywords|
|
|
132
|
+
uri = "https://json-schema.org/draft/2019-09/meta/#{name}"
|
|
133
|
+
component = {
|
|
134
|
+
"$schema" => META_SCHEMA_URI,
|
|
135
|
+
"$id" => uri,
|
|
136
|
+
"$recursiveAnchor" => true,
|
|
137
|
+
"type" => ["object", "boolean"],
|
|
138
|
+
"properties" => SCHEMA.fetch("properties").slice(*keywords)
|
|
139
|
+
}.freeze
|
|
140
|
+
MetaSchemas.register(uri, component)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private_constant :META_SCHEMA_URI, :SCHEMA, :COMPONENT_KEYWORDS
|
|
43
144
|
end
|
|
44
145
|
end
|
|
45
146
|
end
|