schemurai 2.0.0 → 2.2.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 +40 -2
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluation.rb +19 -2
- data/lib/schemurai/evaluator.rb +24 -21
- data/lib/schemurai/schema_domain.rb +27 -19
- data/lib/schemurai/schema_graph.rb +70 -37
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +166 -129
- data/lib/schemurai/vm/evaluator.rb +523 -221
- data/lib/schemurai.rb +6 -3
- data/sig/schemurai.rbs +157 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01b0c89354b90232337525b7fe75c063053fecd2965036bb7ff600d2b4e823cb
|
|
4
|
+
data.tar.gz: f45549ac8cf41eae0c2b28220ca93fe87042cec2aa7c7926382a00f8f65c105d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 618d494971d7a1745075dae9b9e6155562f9c0f0fb058e923cd457f26bb062be2858db9f2ffaf711d035ee83490179e005869106f3ae29ef0c8011512bed2af9
|
|
7
|
+
data.tar.gz: 6ecd49a7320b6b936067808a2a0bd07d6e497418427d0e1612d624dd703a71c6eae99f9ae27ab0a94c2946c3a6495fdc2c11bf665b7d59a9fad628f285af0f5e
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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
|
+
[](https://github.com/oakcask/schemurai/actions/workflows/lint--rbs.yaml)
|
|
7
|
+
|
|
3
8
|
A small, light-weight-dependency JSON Schema validator for Ruby supporting Draft 7,
|
|
4
9
|
Draft 2019-09, and Draft 2020-12. It covers the required cases in the official
|
|
5
10
|
JSON-Schema-Test-Suite, as well as the applicable optional tests for numeric
|
|
@@ -7,6 +12,8 @@ precision, ECMA-262 regular expressions, content validation, anchors, and dynami
|
|
|
7
12
|
references. The dialect is selected from the schema's `$schema` URI; schemas that
|
|
8
13
|
omit `$schema` use Draft 7 for compatibility.
|
|
9
14
|
|
|
15
|
+
Schemurai ships RBS declarations for its public API in `sig/schemurai.rbs`.
|
|
16
|
+
|
|
10
17
|
```ruby
|
|
11
18
|
require "schemurai"
|
|
12
19
|
|
|
@@ -116,6 +123,38 @@ validation for `contentEncoding` and `contentMediaType` with `content: true`.
|
|
|
116
123
|
Enable optional format assertions with `format: true`; support for each format is
|
|
117
124
|
listed separately below.
|
|
118
125
|
|
|
126
|
+
### Validation errors
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
result = Schemurai.validate(
|
|
130
|
+
{ "$ref" => "https://example.test/positive" },
|
|
131
|
+
-1,
|
|
132
|
+
schemas: { "https://example.test/positive" => { "type" => "integer", "minimum" => 1 } }
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
validation_error = result.errors[0]
|
|
136
|
+
p validation_error.keyword # => "minimum"
|
|
137
|
+
p validation_error.instance_path # => ""
|
|
138
|
+
p validation_error.schema_path # => "/$ref/minimum"
|
|
139
|
+
p validation_error.message # => "number must be greater than or equal to 1"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`Result#errors` contains `Schemurai::ValidationError` objects.
|
|
143
|
+
Check the following attributes to investigate schema errors:
|
|
144
|
+
|
|
145
|
+
- `keyword` identifies the failed JSON Schema keyword. Schemurai uses
|
|
146
|
+
`falseSchema` for a boolean `false` schema, which has no keyword of its own.
|
|
147
|
+
- `instance_path` is a JSON Pointer to the value that failed validation.
|
|
148
|
+
- `schema_path` is a JSON Pointer to the schema location that produced the
|
|
149
|
+
error.
|
|
150
|
+
- `message` is a human-readable `String` describing the failure.
|
|
151
|
+
|
|
152
|
+
An empty path points to the instance or schema root. Error order is stable.
|
|
153
|
+
The presence and type of `message` are part of the public API, but its exact
|
|
154
|
+
wording is not. Use `keyword`, `instance_path`, and `schema_path`, rather than
|
|
155
|
+
matching `message`, when handling errors programmatically. `ValidationError#to_h`
|
|
156
|
+
returns these four attributes as a Hash.
|
|
157
|
+
|
|
119
158
|
### Thread / Ractor support
|
|
120
159
|
|
|
121
160
|
To share a registry between threads or Ractors, finish registering schemas and
|
|
@@ -248,8 +287,7 @@ ruby script/oracle-cases --summary
|
|
|
248
287
|
```
|
|
249
288
|
|
|
250
289
|
The serialized oracle tools are `script/oracle-runner` and
|
|
251
|
-
`script/oracle-compare`.
|
|
252
|
-
[`benchmark/baselines/README.md`](benchmark/baselines/README.md).
|
|
290
|
+
`script/oracle-compare`.
|
|
253
291
|
|
|
254
292
|
Run the linter with:
|
|
255
293
|
|
|
@@ -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/evaluation.rb
CHANGED
|
@@ -34,10 +34,27 @@ module Schemurai
|
|
|
34
34
|
|
|
35
35
|
def merge(other)
|
|
36
36
|
return self.class.invalid unless valid? && other.valid?
|
|
37
|
+
return self if other.evaluated_properties.empty? && other.evaluated_items.empty?
|
|
38
|
+
return other if evaluated_properties.empty? && evaluated_items.empty?
|
|
39
|
+
|
|
40
|
+
properties = if evaluated_properties.empty?
|
|
41
|
+
other.evaluated_properties
|
|
42
|
+
elsif other.evaluated_properties.empty?
|
|
43
|
+
evaluated_properties
|
|
44
|
+
else
|
|
45
|
+
evaluated_properties | other.evaluated_properties
|
|
46
|
+
end
|
|
47
|
+
items = if evaluated_items.empty?
|
|
48
|
+
other.evaluated_items
|
|
49
|
+
elsif other.evaluated_items.empty?
|
|
50
|
+
evaluated_items
|
|
51
|
+
else
|
|
52
|
+
evaluated_items | other.evaluated_items
|
|
53
|
+
end
|
|
37
54
|
|
|
38
55
|
self.class.valid(
|
|
39
|
-
evaluated_properties:
|
|
40
|
-
evaluated_items:
|
|
56
|
+
evaluated_properties: properties,
|
|
57
|
+
evaluated_items: items
|
|
41
58
|
)
|
|
42
59
|
end
|
|
43
60
|
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
|
|
@@ -8,7 +8,7 @@ module Schemurai
|
|
|
8
8
|
invalid!(label, "must be a boolean or an object")
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
validate_value!(schema, label, {})
|
|
11
|
+
validate_value!(schema, label, [], {})
|
|
12
12
|
schema
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -23,31 +23,37 @@ module Schemurai
|
|
|
23
23
|
schemas
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
module_function def validate_value!(value, path, ancestors)
|
|
26
|
+
module_function def validate_value!(value, label, path, ancestors)
|
|
27
27
|
case value
|
|
28
28
|
when nil, true, false
|
|
29
29
|
nil
|
|
30
30
|
when String, Integer
|
|
31
|
-
|
|
31
|
+
invalid_at!(label, path, "uses a subclass of #{value.class.superclass}") unless supported_exact_scalar?(value)
|
|
32
32
|
when Float
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
invalid_at!(label, path, "must contain only finite numbers") unless value.finite?
|
|
34
|
+
invalid_at!(label, path, "uses a Float subclass") unless value.instance_of?(Float)
|
|
35
35
|
when Array
|
|
36
|
-
|
|
37
|
-
descend!(value, path, ancestors) do
|
|
38
|
-
value.each_with_index
|
|
36
|
+
invalid_at!(label, path, "uses an Array subclass") unless value.instance_of?(Array)
|
|
37
|
+
descend!(value, label, path, ancestors) do
|
|
38
|
+
value.each_with_index do |item, index|
|
|
39
|
+
path << index
|
|
40
|
+
validate_value!(item, label, path, ancestors)
|
|
41
|
+
path.pop
|
|
42
|
+
end
|
|
39
43
|
end
|
|
40
44
|
when Hash
|
|
41
|
-
|
|
42
|
-
descend!(value, path, ancestors) do
|
|
45
|
+
invalid_at!(label, path, "uses a Hash subclass") unless value.instance_of?(Hash)
|
|
46
|
+
descend!(value, label, path, ancestors) do
|
|
43
47
|
value.each do |key, item|
|
|
44
|
-
|
|
48
|
+
invalid_at!(label, path, "contains a non-string object key") unless key.instance_of?(String)
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
path << key
|
|
51
|
+
validate_value!(item, label, path, ancestors)
|
|
52
|
+
path.pop
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
55
|
else
|
|
50
|
-
|
|
56
|
+
invalid_at!(label, path, "contains unsupported #{value.class}")
|
|
51
57
|
end
|
|
52
58
|
end
|
|
53
59
|
private_class_method :validate_value!
|
|
@@ -57,8 +63,8 @@ module Schemurai
|
|
|
57
63
|
end
|
|
58
64
|
private_class_method :supported_exact_scalar?
|
|
59
65
|
|
|
60
|
-
module_function def descend!(value, path, ancestors)
|
|
61
|
-
|
|
66
|
+
module_function def descend!(value, label, path, ancestors)
|
|
67
|
+
invalid_at!(label, path, "contains a recursive container") if ancestors.key?(value.object_id)
|
|
62
68
|
|
|
63
69
|
ancestors[value.object_id] = true
|
|
64
70
|
yield
|
|
@@ -67,11 +73,13 @@ module Schemurai
|
|
|
67
73
|
end
|
|
68
74
|
private_class_method :descend!
|
|
69
75
|
|
|
70
|
-
module_function def
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
module_function def invalid_at!(label, path, reason)
|
|
77
|
+
location = path.reduce(label.dup) do |result, segment|
|
|
78
|
+
result << "/" << segment.to_s.gsub("~", "~0").gsub("/", "~1")
|
|
79
|
+
end
|
|
80
|
+
invalid!(location, reason)
|
|
73
81
|
end
|
|
74
|
-
private_class_method :
|
|
82
|
+
private_class_method :invalid_at!
|
|
75
83
|
|
|
76
84
|
module_function def invalid!(path, reason)
|
|
77
85
|
raise Error, "invalid JSON-shaped #{path}: #{reason}"
|
|
@@ -13,6 +13,44 @@ require_relative "schema_domain"
|
|
|
13
13
|
module Schemurai
|
|
14
14
|
module Internal
|
|
15
15
|
class SchemaGraph
|
|
16
|
+
class CompilationChanges
|
|
17
|
+
EMPTY_HASH = {}.freeze
|
|
18
|
+
EMPTY_ARRAY = [].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :nodes
|
|
21
|
+
attr_accessor :requires_dynamic_scope
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@nodes = []
|
|
25
|
+
@requires_dynamic_scope = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resources = @resources || EMPTY_HASH
|
|
29
|
+
def mutable_resources = (@resources ||= {})
|
|
30
|
+
|
|
31
|
+
def resource_nodes = @resource_nodes || EMPTY_HASH
|
|
32
|
+
def mutable_resource_nodes = (@resource_nodes ||= {})
|
|
33
|
+
|
|
34
|
+
def uri_registry = @uri_registry || EMPTY_HASH
|
|
35
|
+
def mutable_uri_registry = (@uri_registry ||= {})
|
|
36
|
+
|
|
37
|
+
def dynamic_anchors = @dynamic_anchors || EMPTY_HASH
|
|
38
|
+
def mutable_dynamic_anchors = (@dynamic_anchors ||= {})
|
|
39
|
+
|
|
40
|
+
def custom_dialects = @custom_dialects || EMPTY_HASH
|
|
41
|
+
def mutable_custom_dialects = (@custom_dialects ||= {})
|
|
42
|
+
|
|
43
|
+
def document_locations
|
|
44
|
+
@document_locations || EMPTY_ARRAY
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mutable_document_locations
|
|
48
|
+
@document_locations ||= []
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_constant :CompilationChanges
|
|
53
|
+
|
|
16
54
|
class Resource
|
|
17
55
|
attr_reader :uri, :root
|
|
18
56
|
|
|
@@ -65,13 +103,15 @@ module Schemurai
|
|
|
65
103
|
|
|
66
104
|
def compile(schema, base_uri: nil, dialect: @default_dialect)
|
|
67
105
|
SchemaDomain.validate!(schema)
|
|
68
|
-
roots =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
roots = (@compiled_roots[schema] ||= {})
|
|
107
|
+
base_uri = base_uri.to_s
|
|
108
|
+
cache_key = [base_uri, dialect]
|
|
109
|
+
root = roots[cache_key]
|
|
110
|
+
return root if root
|
|
111
|
+
|
|
112
|
+
roots[cache_key] = compile_atomically do |changes|
|
|
113
|
+
root_dialect = dialect_for(schema, dialect, changes)
|
|
114
|
+
compile_document(schema, base_uri, root_dialect, changes)
|
|
75
115
|
end
|
|
76
116
|
end
|
|
77
117
|
|
|
@@ -132,16 +172,7 @@ module Schemurai
|
|
|
132
172
|
end
|
|
133
173
|
|
|
134
174
|
private def compilation_changes
|
|
135
|
-
|
|
136
|
-
resources: {},
|
|
137
|
-
resource_nodes: {},
|
|
138
|
-
uri_registry: {},
|
|
139
|
-
nodes: [],
|
|
140
|
-
document_locations: [],
|
|
141
|
-
dynamic_anchors: {},
|
|
142
|
-
custom_dialects: {},
|
|
143
|
-
requires_dynamic_scope: false
|
|
144
|
-
}
|
|
175
|
+
CompilationChanges.new
|
|
145
176
|
end
|
|
146
177
|
|
|
147
178
|
private def compile_atomically
|
|
@@ -152,22 +183,22 @@ module Schemurai
|
|
|
152
183
|
end
|
|
153
184
|
|
|
154
185
|
private def commit(changes)
|
|
155
|
-
resources.update(changes
|
|
156
|
-
changes
|
|
186
|
+
resources.update(changes.resources)
|
|
187
|
+
changes.resource_nodes.each do |resource, resource_nodes|
|
|
157
188
|
resource_nodes.each_value { |node| resource.add(node) }
|
|
158
189
|
end
|
|
159
|
-
uri_registry.update(changes
|
|
160
|
-
nodes.concat(changes
|
|
190
|
+
uri_registry.update(changes.uri_registry)
|
|
191
|
+
nodes.concat(changes.nodes)
|
|
161
192
|
if @nodes_by_document_location
|
|
162
|
-
changes
|
|
193
|
+
changes.document_locations.each do |document_key, schema_path, node|
|
|
163
194
|
(@nodes_by_document_location[document_key] ||= {})[schema_path] = node
|
|
164
195
|
end
|
|
165
196
|
end
|
|
166
|
-
changes
|
|
197
|
+
changes.dynamic_anchors.each do |resource, anchors|
|
|
167
198
|
(@dynamic_anchors[resource] ||= {}).update(anchors)
|
|
168
199
|
end
|
|
169
|
-
(@custom_dialects ||= {}).update(changes
|
|
170
|
-
@dynamic_scope = true if changes
|
|
200
|
+
(@custom_dialects ||= {}).update(changes.custom_dialects) unless changes.custom_dialects.empty?
|
|
201
|
+
@dynamic_scope = true if changes.requires_dynamic_scope
|
|
171
202
|
end
|
|
172
203
|
|
|
173
204
|
private def resolve_uncached(node, reference)
|
|
@@ -236,7 +267,7 @@ module Schemurai
|
|
|
236
267
|
private def compile_node(schema, inherited_base, dialect, schema_path, resource_path, resource, document_key, changes)
|
|
237
268
|
hash_schema = schema.is_a?(Hash)
|
|
238
269
|
if hash_schema && (schema.key?("$recursiveRef") || schema.key?("$dynamicRef"))
|
|
239
|
-
changes
|
|
270
|
+
changes.requires_dynamic_scope = true
|
|
240
271
|
end
|
|
241
272
|
dialect = dialect_for(schema, dialect, changes) if hash_schema && schema.key?("$schema")
|
|
242
273
|
if hash_schema && dialect.format_assertion? && schema.key?("format") &&
|
|
@@ -263,11 +294,13 @@ module Schemurai
|
|
|
263
294
|
resource_path: resource_path,
|
|
264
295
|
document_key: document_key
|
|
265
296
|
)
|
|
266
|
-
changes
|
|
267
|
-
|
|
297
|
+
changes.nodes << node
|
|
298
|
+
if @nodes_by_document_location
|
|
299
|
+
changes.mutable_document_locations << [document_key, schema_path, node]
|
|
300
|
+
end
|
|
268
301
|
|
|
269
302
|
if hash_schema && schema.key?("$id") && !exclusive_ref && !base.empty?
|
|
270
|
-
changes[
|
|
303
|
+
changes.mutable_uri_registry[base] = node
|
|
271
304
|
if starts_resource
|
|
272
305
|
resource = register_resource(strip_fragment(base), node, changes)
|
|
273
306
|
end
|
|
@@ -276,7 +309,7 @@ module Schemurai
|
|
|
276
309
|
resource ||= register_resource(strip_fragment(base), node, changes)
|
|
277
310
|
unless node.resource
|
|
278
311
|
node.resource = resource
|
|
279
|
-
(changes[
|
|
312
|
+
(changes.mutable_resource_nodes[resource] ||= {})[node.resource_path] = node unless node.equal?(resource.root)
|
|
280
313
|
end
|
|
281
314
|
|
|
282
315
|
if hash_schema && !exclusive_ref
|
|
@@ -315,24 +348,24 @@ module Schemurai
|
|
|
315
348
|
return resource if resource && resource.root.equal?(root)
|
|
316
349
|
return resource if resource
|
|
317
350
|
|
|
318
|
-
changes[
|
|
351
|
+
changes.mutable_resources[uri] = Resource.new(uri, root)
|
|
319
352
|
end
|
|
320
353
|
|
|
321
354
|
private def resource_at(uri, changes)
|
|
322
|
-
changes
|
|
355
|
+
changes.resources.fetch(uri) { resources[uri] }
|
|
323
356
|
end
|
|
324
357
|
|
|
325
358
|
private def register_uri_unless_present(uri, node, changes)
|
|
326
|
-
return if changes
|
|
359
|
+
return if changes.uri_registry.key?(uri) || uri_registry.key?(uri)
|
|
327
360
|
|
|
328
|
-
changes[
|
|
361
|
+
changes.mutable_uri_registry[uri] = node
|
|
329
362
|
end
|
|
330
363
|
|
|
331
364
|
private def register_anchor(node, resource, name, changes, dynamic: false)
|
|
332
365
|
return unless name.is_a?(String) && !name.empty?
|
|
333
366
|
|
|
334
|
-
changes[
|
|
335
|
-
((changes[
|
|
367
|
+
changes.mutable_uri_registry["#{resource.uri}##{name}"] = node
|
|
368
|
+
((changes.mutable_dynamic_anchors[resource] ||= {})[name] = node) if dynamic
|
|
336
369
|
end
|
|
337
370
|
|
|
338
371
|
private def node_by_document_location(document_key, schema_path)
|
|
@@ -392,7 +425,7 @@ module Schemurai
|
|
|
392
425
|
meta_schema = @external_schemas[uri] || @external_schemas[uri.delete_suffix("#")]
|
|
393
426
|
return unless meta_schema.is_a?(Hash) && meta_schema["$vocabulary"].is_a?(Hash)
|
|
394
427
|
|
|
395
|
-
custom_dialects = changes ? changes
|
|
428
|
+
custom_dialects = changes ? changes.mutable_custom_dialects : (@custom_dialects ||= {})
|
|
396
429
|
return custom_dialects[uri] if custom_dialects.key?(uri)
|
|
397
430
|
return @custom_dialects[uri] if @custom_dialects&.key?(uri)
|
|
398
431
|
|
data/lib/schemurai/version.rb
CHANGED