schemurai 1.0.0 → 2.0.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 +59 -3
- data/lib/schemurai/backend.rb +32 -0
- data/lib/schemurai/evaluator.rb +6 -2
- 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 +798 -0
- data/lib/schemurai.rb +164 -18
- metadata +20 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2b340649fb4d070d05390ec257487c1f37e45287ef16325f0cfe3e4fb10ae72
|
|
4
|
+
data.tar.gz: e9e93301abe43e21ff297b66a3d9cb4d43376c8ac2b80ee4e650c1705f5dbc99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a134b68cc4c6a2fc991822d63f841e6bec085a1c33109aca32e75feebd4cd95ca28dad001835591b200515170aa44bbf2c32275a610b2f7acc878d41f8a46bab
|
|
7
|
+
data.tar.gz: f9fd8b9be4432b61d153be039e39e59b0749ada777e15ebbebf007c69d237d356f5b6feea1c790d8e51201e6bedda0cc26e0c533f499e49062e461f78fa03bd3
|
data/README.md
CHANGED
|
@@ -93,6 +93,11 @@ validator. Repeatedly compiling the same schema object with one registry reuses
|
|
|
93
93
|
its compiled schema graph. `Schemurai.validate` and `.valid?` continue
|
|
94
94
|
to accept raw JSON-like schemas and perform compilation internally.
|
|
95
95
|
|
|
96
|
+
Schema inputs are checked recursively before compilation. They must use the
|
|
97
|
+
built-in JSON-shaped Ruby classes described in
|
|
98
|
+
[`docs/compatibility-domain.md`](docs/compatibility-domain.md); rejected values
|
|
99
|
+
are never retained by a registry.
|
|
100
|
+
|
|
96
101
|
To resolve external references, pass a mapping of URIs to schemas using
|
|
97
102
|
`schemas:`.
|
|
98
103
|
|
|
@@ -111,7 +116,7 @@ validation for `contentEncoding` and `contentMediaType` with `content: true`.
|
|
|
111
116
|
Enable optional format assertions with `format: true`; support for each format is
|
|
112
117
|
listed separately below.
|
|
113
118
|
|
|
114
|
-
### Thread / Ractor
|
|
119
|
+
### Thread / Ractor support
|
|
115
120
|
|
|
116
121
|
To share a registry between threads or Ractors, finish registering schemas and
|
|
117
122
|
make the registry shareable first. This eagerly compiles every registered
|
|
@@ -122,7 +127,9 @@ registry = Schemurai::SchemaRegistry.new(
|
|
|
122
127
|
schemas: {
|
|
123
128
|
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 },
|
|
124
129
|
"https://example.test/value" => { "$ref" => "https://example.test/positive" }
|
|
125
|
-
}
|
|
130
|
+
},
|
|
131
|
+
# enable virtual machine backend that is faster for iterative validation.
|
|
132
|
+
backend: :vm
|
|
126
133
|
)
|
|
127
134
|
registry.make_shareable
|
|
128
135
|
|
|
@@ -134,7 +141,46 @@ validator = registry.validator_for("https://example.test/value")
|
|
|
134
141
|
`ResolutionError` if a reference cannot be resolved. After it returns,
|
|
135
142
|
`validator_for` is read-only and may be called concurrently, while `compile` is
|
|
136
143
|
no longer available. A `Validator` contains per-validation mutable state and
|
|
137
|
-
must not be shared between threads or Ractors.
|
|
144
|
+
must not be shared between threads or Ractors. With the VM backend, the registry
|
|
145
|
+
compiles and shares its bytecode before this transition, so those validators do
|
|
146
|
+
not compile separate instruction streams in each Ractor.
|
|
147
|
+
|
|
148
|
+
### Meta-schema validation
|
|
149
|
+
|
|
150
|
+
Meta-schema validation is opt-in so ordinary compilation does not pay its
|
|
151
|
+
additional time and memory cost. Set `validate_schema: true` when creating a
|
|
152
|
+
`SchemaRegistry` to validate every schema registered through `schemas:` and
|
|
153
|
+
every schema passed to `compile`. The convenience `compile`, `validate`, and
|
|
154
|
+
`valid?` methods accept the same option and apply it to their internal registry.
|
|
155
|
+
Schemas without `$schema` use the Draft 7 meta-schema. An invalid schema raises
|
|
156
|
+
`Schemurai::InvalidSchemaError`, whose `result` contains the detailed validation
|
|
157
|
+
errors.
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
Schemurai.compile(schema, validate_schema: true)
|
|
161
|
+
|
|
162
|
+
registry = Schemurai::SchemaRegistry.new(
|
|
163
|
+
schemas: external_schemas,
|
|
164
|
+
validate_schema: true
|
|
165
|
+
)
|
|
166
|
+
validator = registry.compile(schema)
|
|
167
|
+
|
|
168
|
+
result = Schemurai.validate_schema(schema)
|
|
169
|
+
result.valid? # whether the schema itself is valid
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`SchemaRegistry#validate_schema` and `#valid_schema?` perform explicit checks
|
|
173
|
+
regardless of the registry option and reuse meta-schema graph data within that
|
|
174
|
+
registry. The stateful validator used for a check is temporary; no global
|
|
175
|
+
validator is retained.
|
|
176
|
+
|
|
177
|
+
### Backend selection
|
|
178
|
+
|
|
179
|
+
`Schemurai.backend`, `SchemaRegistry#backend`, and `Validator#backend` expose
|
|
180
|
+
the actual backend. Pass `backend: :ruby` to force the Ruby oracle or
|
|
181
|
+
`backend: :vm` to ahead-of-time compile schemas for the validator virtual machine.
|
|
182
|
+
Schema graph construction remains shared Ruby infrastructure. Environment-based selection
|
|
183
|
+
is documented in [`docs/backend-selection.md`](docs/backend-selection.md).
|
|
138
184
|
|
|
139
185
|
## JSON Schema conformance
|
|
140
186
|
|
|
@@ -195,6 +241,16 @@ Run the test suite with:
|
|
|
195
241
|
bundle exec rspec
|
|
196
242
|
```
|
|
197
243
|
|
|
244
|
+
Verify the reviewed official-suite classifications with:
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
ruby script/oracle-cases --summary
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The serialized oracle tools are `script/oracle-runner` and
|
|
251
|
+
`script/oracle-compare`. Ruby interpreter and YJIT baseline instructions are in
|
|
252
|
+
[`benchmark/baselines/README.md`](benchmark/baselines/README.md).
|
|
253
|
+
|
|
198
254
|
Run the linter with:
|
|
199
255
|
|
|
200
256
|
```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
|
data/lib/schemurai/evaluator.rb
CHANGED
|
@@ -9,6 +9,8 @@ module Schemurai
|
|
|
9
9
|
class Evaluator
|
|
10
10
|
MISSING_SEGMENT = Object.new.freeze
|
|
11
11
|
|
|
12
|
+
def backend = :ruby
|
|
13
|
+
|
|
12
14
|
def initialize(graph, root, content: false, format: false)
|
|
13
15
|
@validate_content = content
|
|
14
16
|
@validate_format = format
|
|
@@ -28,6 +30,7 @@ module Schemurai
|
|
|
28
30
|
@error_callback = nil
|
|
29
31
|
@error_count = 0
|
|
30
32
|
@track_dynamic_scope = @graph.dynamic_scope?
|
|
33
|
+
@dynamic_scope = nil
|
|
31
34
|
@instance_path = nil
|
|
32
35
|
@schema_path = nil
|
|
33
36
|
evaluate_valid(@root, instance)
|
|
@@ -37,6 +40,7 @@ module Schemurai
|
|
|
37
40
|
@error_callback = callback
|
|
38
41
|
@error_count = 0
|
|
39
42
|
@track_dynamic_scope = @graph.dynamic_scope?
|
|
43
|
+
@dynamic_scope = nil
|
|
40
44
|
@instance_path = []
|
|
41
45
|
@schema_path = []
|
|
42
46
|
evaluate(@root, instance)
|
|
@@ -411,7 +415,7 @@ module Schemurai
|
|
|
411
415
|
target = @graph.resolve(node, reference)
|
|
412
416
|
return target unless reference.to_s.end_with?("#") && target.schema.is_a?(Hash) && target.schema["$recursiveAnchor"] == true
|
|
413
417
|
|
|
414
|
-
@dynamic_scope.filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
|
|
418
|
+
Array(@dynamic_scope).filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
|
|
415
419
|
end
|
|
416
420
|
|
|
417
421
|
private def dynamic_target(node, reference)
|
|
@@ -420,7 +424,7 @@ module Schemurai
|
|
|
420
424
|
return target if raw_fragment.nil? || raw_fragment.empty? || raw_fragment.start_with?("/")
|
|
421
425
|
return target unless target.schema.is_a?(Hash) && target.schema["$dynamicAnchor"] == raw_fragment
|
|
422
426
|
|
|
423
|
-
@dynamic_scope.each do |resource|
|
|
427
|
+
Array(@dynamic_scope).each do |resource|
|
|
424
428
|
dynamic = @graph.dynamic_anchor(resource, raw_fragment)
|
|
425
429
|
return dynamic if dynamic
|
|
426
430
|
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
|
|
@@ -13,8 +13,46 @@ module Schemurai
|
|
|
13
13
|
"$dynamicAnchor" => "meta",
|
|
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
|
+
"$dynamicRef" => {"type" => "string"},
|
|
21
|
+
"$dynamicAnchor" => {"type" => "string", "pattern" => "^[A-Za-z_][-A-Za-z0-9._]*$"},
|
|
22
|
+
"$recursiveRef" => {"type" => "string"},
|
|
23
|
+
"$recursiveAnchor" => {"type" => "string", "pattern" => "^[A-Za-z_][-A-Za-z0-9._]*$"},
|
|
24
|
+
"$vocabulary" => {"type" => "object", "additionalProperties" => {"type" => "boolean"}},
|
|
25
|
+
"$comment" => {"type" => "string"},
|
|
16
26
|
"$defs" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
|
|
17
27
|
"definitions" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
|
|
28
|
+
"prefixItems" => {
|
|
29
|
+
"type" => "array",
|
|
30
|
+
"minItems" => 1,
|
|
31
|
+
"items" => {"$dynamicRef" => "#meta"}
|
|
32
|
+
},
|
|
33
|
+
"items" => {"$dynamicRef" => "#meta"},
|
|
34
|
+
"contains" => {"$dynamicRef" => "#meta"},
|
|
35
|
+
"additionalProperties" => {"$dynamicRef" => "#meta"},
|
|
36
|
+
"properties" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
|
|
37
|
+
"patternProperties" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
|
|
38
|
+
"dependentSchemas" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
|
|
39
|
+
"dependencies" => {
|
|
40
|
+
"type" => "object",
|
|
41
|
+
"additionalProperties" => {
|
|
42
|
+
"anyOf" => [
|
|
43
|
+
{"$dynamicRef" => "#meta"},
|
|
44
|
+
{"type" => "array", "items" => {"type" => "string"}, "uniqueItems" => true}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"propertyNames" => {"$dynamicRef" => "#meta"},
|
|
49
|
+
"if" => {"$dynamicRef" => "#meta"},
|
|
50
|
+
"then" => {"$dynamicRef" => "#meta"},
|
|
51
|
+
"else" => {"$dynamicRef" => "#meta"},
|
|
52
|
+
"allOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
|
|
53
|
+
"anyOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
|
|
54
|
+
"oneOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
|
|
55
|
+
"not" => {"$dynamicRef" => "#meta"},
|
|
18
56
|
"type" => {
|
|
19
57
|
"anyOf" => [
|
|
20
58
|
{"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
|
|
@@ -26,20 +64,83 @@ module Schemurai
|
|
|
26
64
|
}
|
|
27
65
|
]
|
|
28
66
|
},
|
|
67
|
+
"enum" => {"type" => "array"},
|
|
68
|
+
"multipleOf" => {"type" => "number", "exclusiveMinimum" => 0},
|
|
69
|
+
"maximum" => {"type" => "number"},
|
|
70
|
+
"exclusiveMaximum" => {"type" => "number"},
|
|
71
|
+
"minimum" => {"type" => "number"},
|
|
72
|
+
"exclusiveMinimum" => {"type" => "number"},
|
|
29
73
|
"minLength" => {"type" => "integer", "minimum" => 0},
|
|
30
74
|
"maxLength" => {"type" => "integer", "minimum" => 0},
|
|
75
|
+
"pattern" => {"type" => "string"},
|
|
31
76
|
"minItems" => {"type" => "integer", "minimum" => 0},
|
|
32
77
|
"maxItems" => {"type" => "integer", "minimum" => 0},
|
|
78
|
+
"uniqueItems" => {"type" => "boolean"},
|
|
33
79
|
"minContains" => {"type" => "integer", "minimum" => 0},
|
|
34
80
|
"maxContains" => {"type" => "integer", "minimum" => 0},
|
|
35
81
|
"minProperties" => {"type" => "integer", "minimum" => 0},
|
|
36
|
-
"maxProperties" => {"type" => "integer", "minimum" => 0}
|
|
82
|
+
"maxProperties" => {"type" => "integer", "minimum" => 0},
|
|
83
|
+
"required" => {
|
|
84
|
+
"type" => "array",
|
|
85
|
+
"items" => {"type" => "string"},
|
|
86
|
+
"uniqueItems" => true
|
|
87
|
+
},
|
|
88
|
+
"dependentRequired" => {
|
|
89
|
+
"type" => "object",
|
|
90
|
+
"additionalProperties" => {
|
|
91
|
+
"type" => "array",
|
|
92
|
+
"items" => {"type" => "string"},
|
|
93
|
+
"uniqueItems" => true
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"title" => {"type" => "string"},
|
|
97
|
+
"description" => {"type" => "string"},
|
|
98
|
+
"deprecated" => {"type" => "boolean"},
|
|
99
|
+
"readOnly" => {"type" => "boolean"},
|
|
100
|
+
"writeOnly" => {"type" => "boolean"},
|
|
101
|
+
"examples" => {"type" => "array"},
|
|
102
|
+
"format" => {"type" => "string"},
|
|
103
|
+
"contentEncoding" => {"type" => "string"},
|
|
104
|
+
"contentMediaType" => {"type" => "string"},
|
|
105
|
+
"contentSchema" => {"$dynamicRef" => "#meta"}
|
|
37
106
|
}
|
|
38
107
|
}.freeze
|
|
39
108
|
|
|
40
109
|
MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
|
|
41
110
|
|
|
42
|
-
|
|
111
|
+
COMPONENT_KEYWORDS = {
|
|
112
|
+
"core" => %w[
|
|
113
|
+
$id $schema $ref $anchor $dynamicRef $dynamicAnchor $vocabulary $comment $defs
|
|
114
|
+
],
|
|
115
|
+
"applicator" => %w[
|
|
116
|
+
prefixItems items contains additionalProperties properties patternProperties
|
|
117
|
+
dependentSchemas propertyNames if then else allOf anyOf oneOf not
|
|
118
|
+
],
|
|
119
|
+
"unevaluated" => %w[unevaluatedItems unevaluatedProperties],
|
|
120
|
+
"validation" => %w[
|
|
121
|
+
type const enum multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
|
|
122
|
+
maxLength minLength pattern maxItems minItems uniqueItems maxContains minContains
|
|
123
|
+
maxProperties minProperties required dependentRequired
|
|
124
|
+
],
|
|
125
|
+
"meta-data" => %w[title description default deprecated readOnly writeOnly examples],
|
|
126
|
+
"format-annotation" => %w[format],
|
|
127
|
+
"format-assertion" => %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/2020-12/meta/#{name}"
|
|
133
|
+
component = {
|
|
134
|
+
"$schema" => META_SCHEMA_URI,
|
|
135
|
+
"$id" => uri,
|
|
136
|
+
"$dynamicAnchor" => "meta",
|
|
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
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
module SchemaDomain
|
|
6
|
+
module_function def validate!(schema, label: "schema")
|
|
7
|
+
unless schema.equal?(true) || schema.equal?(false) || schema.instance_of?(Hash)
|
|
8
|
+
invalid!(label, "must be a boolean or an object")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
validate_value!(schema, label, {})
|
|
12
|
+
schema
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module_function def validate_registry!(schemas)
|
|
16
|
+
invalid!("schemas", "must be an object") unless schemas.instance_of?(Hash)
|
|
17
|
+
|
|
18
|
+
schemas.each do |uri, schema|
|
|
19
|
+
invalid!("schemas", "contains a non-string URI key") unless uri.instance_of?(String)
|
|
20
|
+
|
|
21
|
+
validate!(schema, label: "schemas[#{uri.inspect}]")
|
|
22
|
+
end
|
|
23
|
+
schemas
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module_function def validate_value!(value, path, ancestors)
|
|
27
|
+
case value
|
|
28
|
+
when nil, true, false
|
|
29
|
+
nil
|
|
30
|
+
when String, Integer
|
|
31
|
+
invalid!(path, "uses a subclass of #{value.class.superclass}") unless supported_exact_scalar?(value)
|
|
32
|
+
when Float
|
|
33
|
+
invalid!(path, "must contain only finite numbers") unless value.finite?
|
|
34
|
+
invalid!(path, "uses a Float subclass") unless value.instance_of?(Float)
|
|
35
|
+
when Array
|
|
36
|
+
invalid!(path, "uses an Array subclass") unless value.instance_of?(Array)
|
|
37
|
+
descend!(value, path, ancestors) do
|
|
38
|
+
value.each_with_index { |item, index| validate_value!(item, pointer(path, index), ancestors) }
|
|
39
|
+
end
|
|
40
|
+
when Hash
|
|
41
|
+
invalid!(path, "uses a Hash subclass") unless value.instance_of?(Hash)
|
|
42
|
+
descend!(value, path, ancestors) do
|
|
43
|
+
value.each do |key, item|
|
|
44
|
+
invalid!(path, "contains a non-string object key") unless key.instance_of?(String)
|
|
45
|
+
|
|
46
|
+
validate_value!(item, pointer(path, key), ancestors)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
invalid!(path, "contains unsupported #{value.class}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
private_class_method :validate_value!
|
|
54
|
+
|
|
55
|
+
module_function def supported_exact_scalar?(value)
|
|
56
|
+
value.instance_of?(String) || value.instance_of?(Integer)
|
|
57
|
+
end
|
|
58
|
+
private_class_method :supported_exact_scalar?
|
|
59
|
+
|
|
60
|
+
module_function def descend!(value, path, ancestors)
|
|
61
|
+
invalid!(path, "contains a recursive container") if ancestors.key?(value.object_id)
|
|
62
|
+
|
|
63
|
+
ancestors[value.object_id] = true
|
|
64
|
+
yield
|
|
65
|
+
ensure
|
|
66
|
+
ancestors.delete(value.object_id)
|
|
67
|
+
end
|
|
68
|
+
private_class_method :descend!
|
|
69
|
+
|
|
70
|
+
module_function def pointer(path, segment)
|
|
71
|
+
escaped = segment.to_s.gsub("~", "~0").gsub("/", "~1")
|
|
72
|
+
"#{path}/#{escaped}"
|
|
73
|
+
end
|
|
74
|
+
private_class_method :pointer
|
|
75
|
+
|
|
76
|
+
module_function def invalid!(path, reason)
|
|
77
|
+
raise Error, "invalid JSON-shaped #{path}: #{reason}"
|
|
78
|
+
end
|
|
79
|
+
private_class_method :invalid!
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "meta_schemas/draft7"
|
|
|
8
8
|
require_relative "meta_schemas/draft2019_09"
|
|
9
9
|
require_relative "meta_schemas/draft2020_12"
|
|
10
10
|
require_relative "schema_node"
|
|
11
|
+
require_relative "schema_domain"
|
|
11
12
|
|
|
12
13
|
module Schemurai
|
|
13
14
|
module Internal
|
|
@@ -43,9 +44,11 @@ module Schemurai
|
|
|
43
44
|
attr_reader :resources, :uri_registry, :nodes
|
|
44
45
|
|
|
45
46
|
def initialize(schemas: {}, dialect: Dialect.resolve)
|
|
47
|
+
SchemaDomain.validate_registry!(schemas)
|
|
46
48
|
@external_schemas = schemas.dup
|
|
47
49
|
@indexed_external_schemas = nil
|
|
48
50
|
@compiled_roots = {}.compare_by_identity
|
|
51
|
+
@meta_schema_roots = {}
|
|
49
52
|
@resources = {}
|
|
50
53
|
@uri_registry = {}
|
|
51
54
|
@nodes = []
|
|
@@ -58,10 +61,10 @@ module Schemurai
|
|
|
58
61
|
# evaluators sharing this graph may reach the dynamic reference.
|
|
59
62
|
@dynamic_scope = !schemas.empty?
|
|
60
63
|
@default_dialect = dialect
|
|
61
|
-
@shareable = false
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def compile(schema, base_uri: nil, dialect: @default_dialect)
|
|
67
|
+
SchemaDomain.validate!(schema)
|
|
65
68
|
roots = @compiled_roots.fetch(schema) { @compiled_roots[schema] = {} }
|
|
66
69
|
cache_key = [base_uri.to_s, dialect]
|
|
67
70
|
roots.fetch(cache_key) do
|
|
@@ -72,12 +75,24 @@ module Schemurai
|
|
|
72
75
|
end
|
|
73
76
|
end
|
|
74
77
|
|
|
78
|
+
def meta_schema_root(schema)
|
|
79
|
+
SchemaDomain.validate!(schema)
|
|
80
|
+
uri = if schema.is_a?(Hash) && schema.key?("$schema")
|
|
81
|
+
schema["$schema"].to_s
|
|
82
|
+
else
|
|
83
|
+
@default_dialect.uri
|
|
84
|
+
end
|
|
85
|
+
root = (@meta_schema_roots[uri] ||= compile({"$ref" => uri}))
|
|
86
|
+
resolve(root, uri)
|
|
87
|
+
root
|
|
88
|
+
end
|
|
89
|
+
|
|
75
90
|
def resolve(node, reference)
|
|
76
91
|
if (resolved = @resolved_refs&.[](node))&.key?(reference)
|
|
77
92
|
return resolved[reference]
|
|
78
93
|
end
|
|
79
94
|
|
|
80
|
-
if
|
|
95
|
+
if Ractor.shareable?(self)
|
|
81
96
|
raise ResolutionError, "reference #{reference.inspect} was not resolved before sharing"
|
|
82
97
|
end
|
|
83
98
|
|
|
@@ -94,7 +109,7 @@ module Schemurai
|
|
|
94
109
|
end
|
|
95
110
|
|
|
96
111
|
def make_shareable
|
|
97
|
-
return self if
|
|
112
|
+
return self if Ractor.shareable?(self)
|
|
98
113
|
|
|
99
114
|
@external_schemas.each_key do |uri|
|
|
100
115
|
index_external(strip_fragment(uri.to_s), @default_dialect)
|
|
@@ -113,7 +128,6 @@ module Schemurai
|
|
|
113
128
|
end
|
|
114
129
|
|
|
115
130
|
@resolved_refs ||= {}
|
|
116
|
-
@shareable = true
|
|
117
131
|
Ractor.make_shareable(self)
|
|
118
132
|
end
|
|
119
133
|
|
|
@@ -336,9 +350,10 @@ module Schemurai
|
|
|
336
350
|
private def index_external(document_uri, fallback_dialect)
|
|
337
351
|
return if @indexed_external_schemas&.[](document_uri)
|
|
338
352
|
|
|
339
|
-
if (
|
|
353
|
+
if (meta_schema = MetaSchemas.resolve(document_uri))
|
|
354
|
+
dialect = Dialect.resolve(meta_schema["$schema"]) || fallback_dialect
|
|
340
355
|
compile_atomically do |changes|
|
|
341
|
-
compile_document(meta_schema,
|
|
356
|
+
compile_document(meta_schema, document_uri, dialect, changes)
|
|
342
357
|
end
|
|
343
358
|
(@indexed_external_schemas ||= {})[document_uri] = true
|
|
344
359
|
return
|