schemurai 2.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 +36 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluator.rb +24 -21
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/evaluator.rb +51 -38
- data/lib/schemurai.rb +6 -3
- metadata +3 -2
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
|
|
@@ -116,6 +120,38 @@ validation for `contentEncoding` and `contentMediaType` with `content: true`.
|
|
|
116
120
|
Enable optional format assertions with `format: true`; support for each format is
|
|
117
121
|
listed separately below.
|
|
118
122
|
|
|
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
|
+
|
|
119
155
|
### Thread / Ractor support
|
|
120
156
|
|
|
121
157
|
To share a registry between threads or Ractors, finish registering schemas and
|
|
@@ -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,6 +3,7 @@
|
|
|
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
|
|
@@ -333,7 +334,7 @@ module Schemurai
|
|
|
333
334
|
schema = node.schema
|
|
334
335
|
return Evaluation.valid if schema == true
|
|
335
336
|
if schema == false
|
|
336
|
-
add_error("falseSchema",
|
|
337
|
+
add_error("falseSchema", append_keyword: false) { ErrorMessage.false_schema }
|
|
337
338
|
return Evaluation.invalid
|
|
338
339
|
end
|
|
339
340
|
return Evaluation.valid unless schema.is_a?(Hash)
|
|
@@ -442,15 +443,15 @@ module Schemurai
|
|
|
442
443
|
types = Array(schema["type"])
|
|
443
444
|
return if types.any? { |type| type?(value, type) }
|
|
444
445
|
|
|
445
|
-
add_error("type"
|
|
446
|
+
add_error("type") { ErrorMessage.type(types, value) }
|
|
446
447
|
end
|
|
447
448
|
|
|
448
449
|
private def check_enum(schema, value)
|
|
449
450
|
if schema.key?("enum") && !schema["enum"].any? { |candidate| json_equal?(candidate, value) }
|
|
450
|
-
add_error("enum"
|
|
451
|
+
add_error("enum") { ErrorMessage.enum }
|
|
451
452
|
end
|
|
452
453
|
if schema.key?("const") && !json_equal?(schema["const"], value)
|
|
453
|
-
add_error("const"
|
|
454
|
+
add_error("const") { ErrorMessage.const }
|
|
454
455
|
end
|
|
455
456
|
end
|
|
456
457
|
|
|
@@ -472,7 +473,7 @@ module Schemurai
|
|
|
472
473
|
matches << result if result.valid?
|
|
473
474
|
end
|
|
474
475
|
if matches.empty?
|
|
475
|
-
add_error("anyOf"
|
|
476
|
+
add_error("anyOf") { ErrorMessage.any_of }
|
|
476
477
|
else
|
|
477
478
|
matches.each { |result| evaluation = evaluation.merge(result) }
|
|
478
479
|
end
|
|
@@ -487,12 +488,12 @@ module Schemurai
|
|
|
487
488
|
if matches.length == 1
|
|
488
489
|
evaluation = evaluation.merge(matches.first)
|
|
489
490
|
else
|
|
490
|
-
add_error("oneOf"
|
|
491
|
+
add_error("oneOf") { ErrorMessage.one_of(matches.length) }
|
|
491
492
|
end
|
|
492
493
|
end
|
|
493
494
|
|
|
494
495
|
if schema.key?("not") && trial_at(node.child("not"), value, MISSING_SEGMENT, "not").valid?
|
|
495
|
-
add_error("not"
|
|
496
|
+
add_error("not") { ErrorMessage.not }
|
|
496
497
|
end
|
|
497
498
|
|
|
498
499
|
if schema.key?("if")
|
|
@@ -516,14 +517,14 @@ module Schemurai
|
|
|
516
517
|
|
|
517
518
|
divisor = decimal(schema["multipleOf"])
|
|
518
519
|
valid = divisor.positive? && decimal(value).remainder(divisor).zero?
|
|
519
|
-
add_error("multipleOf"
|
|
520
|
+
add_error("multipleOf") { ErrorMessage.multiple_of(schema["multipleOf"]) } unless valid
|
|
520
521
|
end
|
|
521
522
|
|
|
522
523
|
private def compare(schema, keyword, value)
|
|
523
524
|
return unless schema.key?(keyword)
|
|
524
525
|
return if yield(decimal(value), decimal(schema[keyword]))
|
|
525
526
|
|
|
526
|
-
add_error(keyword
|
|
527
|
+
add_error(keyword) { ErrorMessage.numeric_limit(keyword, schema[keyword]) }
|
|
527
528
|
end
|
|
528
529
|
|
|
529
530
|
private def check_string(node, value)
|
|
@@ -534,14 +535,14 @@ module Schemurai
|
|
|
534
535
|
|
|
535
536
|
if schema.key?("pattern")
|
|
536
537
|
matched = ecma_regexp(schema["pattern"]).match?(value)
|
|
537
|
-
add_error("pattern"
|
|
538
|
+
add_error("pattern") { ErrorMessage.pattern(schema["pattern"]) } unless matched
|
|
538
539
|
end
|
|
539
540
|
if format_asserted?(node)
|
|
540
|
-
add_error("format"
|
|
541
|
+
add_error("format") { ErrorMessage.format(node.format.name) } unless node.format.call(value)
|
|
541
542
|
end
|
|
542
543
|
check_content(schema, value) if @validate_content
|
|
543
544
|
rescue RegexpError
|
|
544
|
-
add_error("pattern"
|
|
545
|
+
add_error("pattern") { ErrorMessage.invalid_pattern(schema["pattern"]) }
|
|
545
546
|
end
|
|
546
547
|
|
|
547
548
|
private def format_asserted?(node)
|
|
@@ -558,7 +559,7 @@ module Schemurai
|
|
|
558
559
|
duplicate = value.each_with_index.any? do |item, index|
|
|
559
560
|
value[0...index].any? { |previous| json_equal?(previous, item) }
|
|
560
561
|
end
|
|
561
|
-
add_error("uniqueItems"
|
|
562
|
+
add_error("uniqueItems") { ErrorMessage.unique_items } if duplicate
|
|
562
563
|
end
|
|
563
564
|
|
|
564
565
|
prefix_items = schema["prefixItems"]
|
|
@@ -600,7 +601,7 @@ module Schemurai
|
|
|
600
601
|
minimum = schema.fetch("minContains", 1)
|
|
601
602
|
maximum = schema.fetch("maxContains", Float::INFINITY)
|
|
602
603
|
unless matched.length.between?(minimum, maximum)
|
|
603
|
-
add_error("contains"
|
|
604
|
+
add_error("contains") { ErrorMessage.contains(matched.length, minimum, maximum) }
|
|
604
605
|
end
|
|
605
606
|
evaluated.concat(matched)
|
|
606
607
|
end
|
|
@@ -623,7 +624,7 @@ module Schemurai
|
|
|
623
624
|
limit(schema, "minProperties", value.length) { |actual, expected| actual >= expected }
|
|
624
625
|
|
|
625
626
|
Array(schema["required"]).each do |name|
|
|
626
|
-
add_error("required"
|
|
627
|
+
add_error("required") { ErrorMessage.required(name) } unless value.key?(name)
|
|
627
628
|
end
|
|
628
629
|
|
|
629
630
|
properties = schema.fetch("properties", {})
|
|
@@ -657,7 +658,7 @@ module Schemurai
|
|
|
657
658
|
next unless value.key?(name)
|
|
658
659
|
if dependency.is_a?(Array)
|
|
659
660
|
dependency.each do |required_name|
|
|
660
|
-
add_error("dependencies"
|
|
661
|
+
add_error("dependencies") { ErrorMessage.dependent_required(name, required_name) } unless value.key?(required_name)
|
|
661
662
|
end
|
|
662
663
|
else
|
|
663
664
|
result = evaluate_at(node.child("dependencies", name), value, MISSING_SEGMENT, "dependencies", name)
|
|
@@ -669,7 +670,7 @@ module Schemurai
|
|
|
669
670
|
next unless value.key?(name)
|
|
670
671
|
required_names.each do |required_name|
|
|
671
672
|
unless value.key?(required_name)
|
|
672
|
-
add_error("dependentRequired"
|
|
673
|
+
add_error("dependentRequired") { ErrorMessage.dependent_required(name, required_name) }
|
|
673
674
|
end
|
|
674
675
|
end
|
|
675
676
|
end
|
|
@@ -707,7 +708,7 @@ module Schemurai
|
|
|
707
708
|
return unless schema.key?(keyword)
|
|
708
709
|
return if yield(actual, schema[keyword])
|
|
709
710
|
|
|
710
|
-
add_error(keyword
|
|
711
|
+
add_error(keyword) { ErrorMessage.size(keyword, schema[keyword], actual) }
|
|
711
712
|
end
|
|
712
713
|
|
|
713
714
|
private def type?(value, type)
|
|
@@ -803,10 +804,12 @@ module Schemurai
|
|
|
803
804
|
JSON.parse(decoded)
|
|
804
805
|
rescue ArgumentError, JSON::ParserError
|
|
805
806
|
keyword = (schema["contentEncoding"] == "base64") ? "contentEncoding" : "contentMediaType"
|
|
806
|
-
add_error(keyword
|
|
807
|
+
add_error(keyword) do
|
|
808
|
+
(keyword == "contentEncoding") ? ErrorMessage.content_encoding : ErrorMessage.content_media_type
|
|
809
|
+
end
|
|
807
810
|
end
|
|
808
811
|
|
|
809
|
-
private def add_error(keyword, message, append_keyword: true)
|
|
812
|
+
private def add_error(keyword, message = nil, append_keyword: true)
|
|
810
813
|
@error_count += 1
|
|
811
814
|
if @error_callback
|
|
812
815
|
schema_keyword = append_keyword ? keyword : MISSING_SEGMENT
|
|
@@ -815,7 +818,7 @@ module Schemurai
|
|
|
815
818
|
keyword: keyword,
|
|
816
819
|
instance_path: pointer(@instance_path),
|
|
817
820
|
schema_path: pointer(@schema_path, schema_keyword),
|
|
818
|
-
message: message
|
|
821
|
+
message: message || yield
|
|
819
822
|
)
|
|
820
823
|
)
|
|
821
824
|
end
|
data/lib/schemurai/version.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "base64"
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "../evaluation"
|
|
6
|
+
require_relative "../error_message"
|
|
6
7
|
require_relative "compiler"
|
|
7
8
|
|
|
8
9
|
module Schemurai
|
|
@@ -132,7 +133,7 @@ module Schemurai
|
|
|
132
133
|
case opcode
|
|
133
134
|
when :boolean
|
|
134
135
|
if operand == false
|
|
135
|
-
add_error("falseSchema",
|
|
136
|
+
add_error("falseSchema", append_keyword: false) { Internal::ErrorMessage.false_schema }
|
|
136
137
|
evaluation = Evaluation.invalid
|
|
137
138
|
end
|
|
138
139
|
when :ref
|
|
@@ -142,25 +143,25 @@ module Schemurai
|
|
|
142
143
|
when :dynamic_ref
|
|
143
144
|
evaluation = evaluation.merge(evaluate_dynamic_ref(program, operand, instance))
|
|
144
145
|
when :type_null
|
|
145
|
-
check_compiled_type("null", instance.nil
|
|
146
|
+
check_compiled_type("null", instance.nil?, instance)
|
|
146
147
|
when :type_boolean
|
|
147
|
-
check_compiled_type("boolean", instance == true || instance == false)
|
|
148
|
+
check_compiled_type("boolean", instance == true || instance == false, instance)
|
|
148
149
|
when :type_object
|
|
149
|
-
check_compiled_type("object", instance.is_a?(Hash))
|
|
150
|
+
check_compiled_type("object", instance.is_a?(Hash), instance)
|
|
150
151
|
when :type_array
|
|
151
|
-
check_compiled_type("array", instance.is_a?(Array))
|
|
152
|
+
check_compiled_type("array", instance.is_a?(Array), instance)
|
|
152
153
|
when :type_number
|
|
153
|
-
check_compiled_type("number", number?(instance))
|
|
154
|
+
check_compiled_type("number", number?(instance), instance)
|
|
154
155
|
when :type_integer
|
|
155
|
-
check_compiled_type("integer", integer?(instance))
|
|
156
|
+
check_compiled_type("integer", integer?(instance), instance)
|
|
156
157
|
when :type_string
|
|
157
|
-
check_compiled_type("string", instance.is_a?(String))
|
|
158
|
+
check_compiled_type("string", instance.is_a?(String), instance)
|
|
158
159
|
when :types
|
|
159
160
|
check_compiled_types(operand, instance)
|
|
160
161
|
when :enum
|
|
161
|
-
add_error("enum"
|
|
162
|
+
add_error("enum") { Internal::ErrorMessage.enum } unless operand.any? { |candidate| json_equal?(candidate, instance) }
|
|
162
163
|
when :const
|
|
163
|
-
add_error("const"
|
|
164
|
+
add_error("const") { Internal::ErrorMessage.const } unless json_equal?(operand, instance)
|
|
164
165
|
when :allOf, :anyOf, :oneOf, :not, :conditional
|
|
165
166
|
evaluation = evaluation.merge(check_combiner(opcode, operand, instance))
|
|
166
167
|
when :number
|
|
@@ -271,15 +272,15 @@ module Schemurai
|
|
|
271
272
|
active[program.node.object_id] ||= {}
|
|
272
273
|
end
|
|
273
274
|
|
|
274
|
-
private def check_compiled_type(name, valid)
|
|
275
|
-
add_error("type"
|
|
275
|
+
private def check_compiled_type(name, valid, value)
|
|
276
|
+
add_error("type") { Internal::ErrorMessage.type(name, value) } unless valid
|
|
276
277
|
end
|
|
277
278
|
|
|
278
279
|
private def check_compiled_types(rules, value)
|
|
279
280
|
instance_mask = instance_type_mask(value, integer: (rules.mask & TYPE_INTEGER) != 0)
|
|
280
281
|
return unless (rules.mask & instance_mask).zero?
|
|
281
282
|
|
|
282
|
-
add_error("type"
|
|
283
|
+
add_error("type") { Internal::ErrorMessage.type(rules.names, value) }
|
|
283
284
|
end
|
|
284
285
|
|
|
285
286
|
private def instance_type_mask(value, integer:)
|
|
@@ -314,22 +315,26 @@ module Schemurai
|
|
|
314
315
|
mask = rules.mask
|
|
315
316
|
actual = decimal(value)
|
|
316
317
|
if (mask & MAXIMUM) != 0 && actual > rules.maximum
|
|
317
|
-
add_error("maximum"
|
|
318
|
+
add_error("maximum") { Internal::ErrorMessage.numeric_limit("maximum", rules.maximum) }
|
|
318
319
|
end
|
|
319
320
|
if (mask & MINIMUM) != 0 && actual < rules.minimum
|
|
320
|
-
add_error("minimum"
|
|
321
|
+
add_error("minimum") { Internal::ErrorMessage.numeric_limit("minimum", rules.minimum) }
|
|
321
322
|
end
|
|
322
323
|
if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
|
|
323
|
-
add_error("exclusiveMaximum"
|
|
324
|
+
add_error("exclusiveMaximum") do
|
|
325
|
+
Internal::ErrorMessage.numeric_limit("exclusiveMaximum", rules.exclusive_maximum)
|
|
326
|
+
end
|
|
324
327
|
end
|
|
325
328
|
if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
|
|
326
|
-
add_error("exclusiveMinimum"
|
|
329
|
+
add_error("exclusiveMinimum") do
|
|
330
|
+
Internal::ErrorMessage.numeric_limit("exclusiveMinimum", rules.exclusive_minimum)
|
|
331
|
+
end
|
|
327
332
|
end
|
|
328
333
|
return if (mask & MULTIPLE_OF).zero?
|
|
329
334
|
|
|
330
335
|
divisor = rules.multiple_of
|
|
331
336
|
valid = divisor.positive? && actual.remainder(divisor).zero?
|
|
332
|
-
add_error("multipleOf"
|
|
337
|
+
add_error("multipleOf") { Internal::ErrorMessage.multiple_of(divisor) } unless valid
|
|
333
338
|
end
|
|
334
339
|
|
|
335
340
|
private def valid_string?(rules, value)
|
|
@@ -350,20 +355,20 @@ module Schemurai
|
|
|
350
355
|
private def check_string(rules, value)
|
|
351
356
|
length = value.length
|
|
352
357
|
if rules.has_max_length && length > rules.max_length
|
|
353
|
-
add_error("maxLength"
|
|
358
|
+
add_error("maxLength") { Internal::ErrorMessage.size("maxLength", rules.max_length, length) }
|
|
354
359
|
end
|
|
355
360
|
if rules.has_min_length && length < rules.min_length
|
|
356
|
-
add_error("minLength"
|
|
361
|
+
add_error("minLength") { Internal::ErrorMessage.size("minLength", rules.min_length, length) }
|
|
357
362
|
end
|
|
358
363
|
if rules.has_pattern && !ecma_regexp(rules.pattern).match?(value)
|
|
359
|
-
add_error("pattern"
|
|
364
|
+
add_error("pattern") { Internal::ErrorMessage.pattern(rules.pattern) }
|
|
360
365
|
end
|
|
361
366
|
if format_asserted?(rules) && !rules.format.call(value)
|
|
362
|
-
add_error("format"
|
|
367
|
+
add_error("format") { Internal::ErrorMessage.format(rules.format.name) }
|
|
363
368
|
end
|
|
364
369
|
check_content(rules, value) if @validate_content
|
|
365
370
|
rescue RegexpError
|
|
366
|
-
add_error("pattern"
|
|
371
|
+
add_error("pattern") { Internal::ErrorMessage.invalid_pattern(rules.pattern) }
|
|
367
372
|
end
|
|
368
373
|
|
|
369
374
|
private def format_asserted?(rules)
|
|
@@ -385,7 +390,9 @@ module Schemurai
|
|
|
385
390
|
JSON.parse(decoded)
|
|
386
391
|
rescue ArgumentError, JSON::ParserError
|
|
387
392
|
keyword = rules.decode_base64 ? "contentEncoding" : "contentMediaType"
|
|
388
|
-
add_error(keyword
|
|
393
|
+
add_error(keyword) do
|
|
394
|
+
(keyword == "contentEncoding") ? Internal::ErrorMessage.content_encoding : Internal::ErrorMessage.content_media_type
|
|
395
|
+
end
|
|
389
396
|
end
|
|
390
397
|
|
|
391
398
|
private def valid_array?(rules, value)
|
|
@@ -437,16 +444,16 @@ module Schemurai
|
|
|
437
444
|
private def check_array(rules, value, prior_evaluation)
|
|
438
445
|
evaluated = []
|
|
439
446
|
if rules.has_max_items && value.length > rules.max_items
|
|
440
|
-
add_error("maxItems"
|
|
447
|
+
add_error("maxItems") { Internal::ErrorMessage.size("maxItems", rules.max_items, value.length) }
|
|
441
448
|
end
|
|
442
449
|
if rules.has_min_items && value.length < rules.min_items
|
|
443
|
-
add_error("minItems"
|
|
450
|
+
add_error("minItems") { Internal::ErrorMessage.size("minItems", rules.min_items, value.length) }
|
|
444
451
|
end
|
|
445
452
|
if rules.unique
|
|
446
453
|
duplicate = value.each_with_index.any? do |item, index|
|
|
447
454
|
value[0...index].any? { |previous| json_equal?(previous, item) }
|
|
448
455
|
end
|
|
449
|
-
add_error("uniqueItems"
|
|
456
|
+
add_error("uniqueItems") { Internal::ErrorMessage.unique_items } if duplicate
|
|
450
457
|
end
|
|
451
458
|
|
|
452
459
|
if (prefix_items = rules.prefix_items)
|
|
@@ -483,7 +490,9 @@ module Schemurai
|
|
|
483
490
|
trial_at(contains, value[index], index, "contains").valid?
|
|
484
491
|
end
|
|
485
492
|
unless matched.length.between?(rules.min_contains, rules.max_contains)
|
|
486
|
-
add_error("contains"
|
|
493
|
+
add_error("contains") do
|
|
494
|
+
Internal::ErrorMessage.contains(matched.length, rules.min_contains, rules.max_contains)
|
|
495
|
+
end
|
|
487
496
|
end
|
|
488
497
|
evaluated.concat(matched)
|
|
489
498
|
end
|
|
@@ -548,14 +557,18 @@ module Schemurai
|
|
|
548
557
|
private def check_object(rules, value, prior_evaluation)
|
|
549
558
|
evaluated = []
|
|
550
559
|
if rules.has_max_properties && value.length > rules.max_properties
|
|
551
|
-
add_error("maxProperties"
|
|
560
|
+
add_error("maxProperties") do
|
|
561
|
+
Internal::ErrorMessage.size("maxProperties", rules.max_properties, value.length)
|
|
562
|
+
end
|
|
552
563
|
end
|
|
553
564
|
if rules.has_min_properties && value.length < rules.min_properties
|
|
554
|
-
add_error("minProperties"
|
|
565
|
+
add_error("minProperties") do
|
|
566
|
+
Internal::ErrorMessage.size("minProperties", rules.min_properties, value.length)
|
|
567
|
+
end
|
|
555
568
|
end
|
|
556
569
|
if (required = rules.required)
|
|
557
570
|
required.each do |name|
|
|
558
|
-
add_error("required"
|
|
571
|
+
add_error("required") { Internal::ErrorMessage.required(name) } unless value.key?(name)
|
|
559
572
|
end
|
|
560
573
|
end
|
|
561
574
|
|
|
@@ -588,7 +601,7 @@ module Schemurai
|
|
|
588
601
|
if dependency.is_a?(Array)
|
|
589
602
|
dependency.each do |required_name|
|
|
590
603
|
unless value.key?(required_name)
|
|
591
|
-
add_error("dependencies"
|
|
604
|
+
add_error("dependencies") { Internal::ErrorMessage.dependent_required(name, required_name) }
|
|
592
605
|
end
|
|
593
606
|
end
|
|
594
607
|
else
|
|
@@ -600,7 +613,7 @@ module Schemurai
|
|
|
600
613
|
next unless value.key?(name)
|
|
601
614
|
required_names.each do |required_name|
|
|
602
615
|
unless value.key?(required_name)
|
|
603
|
-
add_error("dependentRequired"
|
|
616
|
+
add_error("dependentRequired") { Internal::ErrorMessage.dependent_required(name, required_name) }
|
|
604
617
|
end
|
|
605
618
|
end
|
|
606
619
|
end
|
|
@@ -633,7 +646,7 @@ module Schemurai
|
|
|
633
646
|
result if result.valid?
|
|
634
647
|
end
|
|
635
648
|
if matches.empty?
|
|
636
|
-
add_error("anyOf"
|
|
649
|
+
add_error("anyOf") { Internal::ErrorMessage.any_of }
|
|
637
650
|
else
|
|
638
651
|
matches.each { |result| evaluation = evaluation.merge(result) }
|
|
639
652
|
end
|
|
@@ -645,10 +658,10 @@ module Schemurai
|
|
|
645
658
|
if matches.length == 1
|
|
646
659
|
evaluation = evaluation.merge(matches.first)
|
|
647
660
|
else
|
|
648
|
-
add_error("oneOf"
|
|
661
|
+
add_error("oneOf") { Internal::ErrorMessage.one_of(matches.length) }
|
|
649
662
|
end
|
|
650
663
|
when :not
|
|
651
|
-
add_error("not"
|
|
664
|
+
add_error("not") { Internal::ErrorMessage.not } if trial_at(operand, value, MISSING_SEGMENT, "not").valid?
|
|
652
665
|
when :conditional
|
|
653
666
|
condition = trial_at(operand.condition, value, MISSING_SEGMENT, "if")
|
|
654
667
|
evaluation = evaluation.merge(condition) if condition.valid?
|
|
@@ -694,7 +707,7 @@ module Schemurai
|
|
|
694
707
|
@instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
|
|
695
708
|
end
|
|
696
709
|
|
|
697
|
-
private def add_error(keyword, message, append_keyword: true)
|
|
710
|
+
private def add_error(keyword, message = nil, append_keyword: true)
|
|
698
711
|
@error_count += 1
|
|
699
712
|
return false unless @errors
|
|
700
713
|
|
|
@@ -703,7 +716,7 @@ module Schemurai
|
|
|
703
716
|
keyword: keyword,
|
|
704
717
|
instance_path: pointer(@instance_path),
|
|
705
718
|
schema_path: pointer(@schema_path, final_segment),
|
|
706
|
-
message: message
|
|
719
|
+
message: message || yield
|
|
707
720
|
)
|
|
708
721
|
false
|
|
709
722
|
end
|
data/lib/schemurai.rb
CHANGED
|
@@ -34,10 +34,13 @@ module Schemurai
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# Public value type describing one failed JSON Schema assertion.
|
|
38
38
|
#
|
|
39
|
-
# +
|
|
40
|
-
# to the root instance or schema.
|
|
39
|
+
# +keyword+ identifies the failed keyword. +instance_path+ and +schema_path+
|
|
40
|
+
# are JSON Pointers; an empty string points to the root instance or schema.
|
|
41
|
+
# +message+ is a human-readable String. Its presence and type are public API,
|
|
42
|
+
# but its exact wording is not. Use +keyword+ and the paths for programmatic
|
|
43
|
+
# error handling.
|
|
41
44
|
class ValidationError < Data.define(:keyword, :instance_path, :schema_path, :message)
|
|
42
45
|
# Returns the error fields as a Hash.
|
|
43
46
|
def to_h
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schemurai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuya KOHARA (oakcask)
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- lib/schemurai/dialects/draft2019_09.rb
|
|
43
43
|
- lib/schemurai/dialects/draft2020_12.rb
|
|
44
44
|
- lib/schemurai/dialects/draft7.rb
|
|
45
|
+
- lib/schemurai/error_message.rb
|
|
45
46
|
- lib/schemurai/evaluation.rb
|
|
46
47
|
- lib/schemurai/evaluator.rb
|
|
47
48
|
- lib/schemurai/formats.rb
|
|
@@ -61,7 +62,7 @@ licenses:
|
|
|
61
62
|
metadata:
|
|
62
63
|
homepage_uri: https://github.com/oakcask/schemurai
|
|
63
64
|
source_code_uri: https://github.com/oakcask/schemurai/tree/main
|
|
64
|
-
documentation_uri: https://rubydoc.info/gems/schemurai/2.
|
|
65
|
+
documentation_uri: https://rubydoc.info/gems/schemurai/2.1.0
|
|
65
66
|
rubygems_mfa_required: 'true'
|
|
66
67
|
rdoc_options:
|
|
67
68
|
- "--main"
|