schemurai 3.1.0-aarch64-linux-gnu
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +395 -0
- data/lib/schemurai/backend.rb +34 -0
- data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
- data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
- data/lib/schemurai/container_checks.rb +534 -0
- data/lib/schemurai/decimal.rb +11 -0
- data/lib/schemurai/dialect.rb +106 -0
- data/lib/schemurai/dialect_keywords.rb +91 -0
- data/lib/schemurai/dialects/draft2019_09.rb +27 -0
- data/lib/schemurai/dialects/draft2020_12.rb +27 -0
- data/lib/schemurai/dialects/draft7.rb +27 -0
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluation.rb +63 -0
- data/lib/schemurai/evaluation_limits.rb +65 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +861 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +43 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
- data/lib/schemurai/native/diagnostics.rb +53 -0
- data/lib/schemurai/native/evaluator.rb +87 -0
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/schema_domain.rb +95 -0
- data/lib/schemurai/schema_graph.rb +538 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/unique_items.rb +68 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +6 -0
- data/lib/schemurai/vm/compiler.rb +410 -0
- data/lib/schemurai/vm/evaluator.rb +663 -0
- data/lib/schemurai/vm/limited_evaluator.rb +27 -0
- data/lib/schemurai.rb +307 -0
- data/lib/schemurai_native.rb +4 -0
- data/sig/schemurai.rbs +108 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b50600c8fec139b80be2f5dc491c2101b910ee2296cb8e8e43c66b7945782455
|
|
4
|
+
data.tar.gz: 3e64675f1dbea62439c8349e5cba7fe3c5d9bd3ef84123128644a2b3d75be0a5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7ed079091fb79063529718afaa62446ebe28bd384bafbd839efc6e5af639663dd6941a9616bf6121e0dbfa86c5cd19044ba07a86bd25ab6f86d74a2fb969de65
|
|
7
|
+
data.tar.gz: 2e7e249a2ed487ba8bce8b08680e0ae5433be48e242d442ec3b0d94a3e3eb798b637380d2c16c04ce229edffe7b93a3a902140d0716a22c3d18f31f0c663e594
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kuya Kohara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# Schemurai
|
|
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
|
+
|
|
8
|
+
A JSON Schema validator for Ruby with reusable compiled schemas and detailed
|
|
9
|
+
validation errors. It supports Draft 7, Draft 2019-09, and Draft 2020-12 and
|
|
10
|
+
covers the required cases in the official JSON-Schema-Test-Suite, as well as
|
|
11
|
+
the applicable optional tests for numeric
|
|
12
|
+
precision, ECMA-262 regular expressions, content validation, anchors, and dynamic
|
|
13
|
+
references. The dialect is selected from the schema's `$schema` URI; schemas that
|
|
14
|
+
omit `$schema` use Draft 7 for compatibility.
|
|
15
|
+
|
|
16
|
+
Choose the default Ruby evaluator, the Ruby VM backend, or the optional
|
|
17
|
+
experimental Rust native backend. See [backend selection](#backend-selection)
|
|
18
|
+
for setup and requirements.
|
|
19
|
+
|
|
20
|
+
Schemurai ships RBS declarations for its public API in `sig/schemurai.rbs`.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "schemurai"
|
|
24
|
+
|
|
25
|
+
schema = {
|
|
26
|
+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
27
|
+
"type" => "object",
|
|
28
|
+
"properties" => {
|
|
29
|
+
"order_id" => { "type" => "string", "pattern" => "^ord_[0-9]{6}$" },
|
|
30
|
+
"customer" => { "$ref" => "#/$defs/customer" },
|
|
31
|
+
"items" => {
|
|
32
|
+
"type" => "array",
|
|
33
|
+
"minItems" => 1,
|
|
34
|
+
"items" => { "$ref" => "#/$defs/line_item" }
|
|
35
|
+
},
|
|
36
|
+
"created_at" => { "type" => "string", "format" => "date-time" }
|
|
37
|
+
},
|
|
38
|
+
"required" => %w[order_id customer items created_at],
|
|
39
|
+
"additionalProperties" => false,
|
|
40
|
+
"$defs" => {
|
|
41
|
+
"customer" => {
|
|
42
|
+
"type" => "object",
|
|
43
|
+
"properties" => {
|
|
44
|
+
"id" => { "type" => "integer", "minimum" => 1 },
|
|
45
|
+
"name" => { "type" => "string", "minLength" => 1 },
|
|
46
|
+
"tier" => { "enum" => %w[standard premium] }
|
|
47
|
+
},
|
|
48
|
+
"required" => %w[id name],
|
|
49
|
+
"additionalProperties" => false
|
|
50
|
+
},
|
|
51
|
+
"line_item" => {
|
|
52
|
+
"type" => "object",
|
|
53
|
+
"properties" => {
|
|
54
|
+
"sku" => { "type" => "string", "pattern" => "^SKU-[A-Z0-9]{8}$" },
|
|
55
|
+
"quantity" => { "type" => "integer", "minimum" => 1 },
|
|
56
|
+
"unit_price" => { "type" => "number", "minimum" => 0 }
|
|
57
|
+
},
|
|
58
|
+
"required" => %w[sku quantity unit_price],
|
|
59
|
+
"additionalProperties" => false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
order = {
|
|
65
|
+
"order_id" => "ord_123456",
|
|
66
|
+
"customer" => { "id" => 42, "name" => "Ada Lovelace", "tier" => "premium" },
|
|
67
|
+
"items" => [
|
|
68
|
+
{ "sku" => "SKU-ABC12345", "quantity" => 2, "unit_price" => 19.95 }
|
|
69
|
+
],
|
|
70
|
+
"created_at" => "2026-08-31T10:15:00+09:00"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
validator = Schemurai.compile(schema, format: true)
|
|
74
|
+
validator.valid?(order) # => true
|
|
75
|
+
|
|
76
|
+
invalid_order = order.merge(
|
|
77
|
+
"items" => [order.fetch("items").first.merge("quantity" => 0)]
|
|
78
|
+
)
|
|
79
|
+
result = validator.validate(invalid_order)
|
|
80
|
+
result.valid? # => false
|
|
81
|
+
result.errors.each do |error|
|
|
82
|
+
puts "#{error.instance_path}: #{error.message}"
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For repeated validation, compile the schema once and reuse the validator. A
|
|
87
|
+
schema registry owns the compiled resource graph, so schemas compiled by the
|
|
88
|
+
same registry also share their compiled external references.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
registry = Schemurai::SchemaRegistry.new(
|
|
92
|
+
schemas: {
|
|
93
|
+
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 }
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
validator = registry.compile({"$ref" => "https://example.test/positive"})
|
|
97
|
+
|
|
98
|
+
validator.valid?(1) # => true
|
|
99
|
+
validator.valid?(0) # => false
|
|
100
|
+
validator.validate(0).errors # detailed errors, without recompiling the schema
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`Schemurai.compile` is a convenience for compiling a standalone
|
|
104
|
+
validator. Repeatedly compiling the same schema object with one registry reuses
|
|
105
|
+
its compiled schema graph. `Schemurai.validate` and `.valid?` continue
|
|
106
|
+
to accept raw JSON-like schemas and perform compilation internally.
|
|
107
|
+
|
|
108
|
+
Schema inputs are checked recursively before compilation. They must use the
|
|
109
|
+
built-in JSON-shaped Ruby classes described in
|
|
110
|
+
[`docs/compatibility-domain.md`](docs/compatibility-domain.md); rejected values
|
|
111
|
+
are never retained by a registry.
|
|
112
|
+
|
|
113
|
+
To resolve external references, pass a mapping of URIs to schemas using
|
|
114
|
+
`schemas:`.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
Schemurai.valid?(
|
|
118
|
+
{ "$ref" => "https://example.test/positive" },
|
|
119
|
+
3,
|
|
120
|
+
schemas: { "https://example.test/positive" => { "type" => "integer", "minimum" => 1 } }
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Draft 2019-09 and Draft 2020-12 support their dialect-specific keywords, including
|
|
125
|
+
`$recursiveRef` / `$dynamicRef`, `$defs`, `dependentSchemas`, `dependentRequired`,
|
|
126
|
+
`minContains`, `maxContains`, and the `unevaluated*` applicators. Enable optional
|
|
127
|
+
validation for `contentEncoding` and `contentMediaType` with `content: true`.
|
|
128
|
+
Enable optional format assertions with `format: true`; support for each format is
|
|
129
|
+
listed separately below.
|
|
130
|
+
|
|
131
|
+
### Validation errors
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
result = Schemurai.validate(
|
|
135
|
+
{ "$ref" => "https://example.test/positive" },
|
|
136
|
+
-1,
|
|
137
|
+
schemas: { "https://example.test/positive" => { "type" => "integer", "minimum" => 1 } }
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
validation_error = result.errors[0]
|
|
141
|
+
p validation_error.keyword # => "minimum"
|
|
142
|
+
p validation_error.instance_path # => ""
|
|
143
|
+
p validation_error.schema_path # => "/$ref/minimum"
|
|
144
|
+
p validation_error.message # => "number must be greater than or equal to 1"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`Result#errors` contains `Schemurai::ValidationError` objects.
|
|
148
|
+
Check the following attributes to investigate schema errors:
|
|
149
|
+
|
|
150
|
+
- `keyword` identifies the failed JSON Schema keyword. Schemurai uses
|
|
151
|
+
`falseSchema` for a boolean `false` schema, which has no keyword of its own.
|
|
152
|
+
- `instance_path` is a JSON Pointer to the value that failed validation.
|
|
153
|
+
- `schema_path` is a JSON Pointer to the schema location that produced the
|
|
154
|
+
error.
|
|
155
|
+
- `message` is a human-readable `String` describing the failure.
|
|
156
|
+
|
|
157
|
+
An empty path points to the instance or schema root. Error order is stable.
|
|
158
|
+
The presence and type of `message` are part of the public API, but its exact
|
|
159
|
+
wording is not. Use `keyword`, `instance_path`, and `schema_path`, rather than
|
|
160
|
+
matching `message`, when handling errors programmatically. `ValidationError#to_h`
|
|
161
|
+
returns these four attributes as a Hash.
|
|
162
|
+
|
|
163
|
+
### Thread / Ractor support
|
|
164
|
+
|
|
165
|
+
To share a registry between threads or Ractors, finish registering schemas and
|
|
166
|
+
make the registry shareable first. This eagerly compiles every registered
|
|
167
|
+
schema, resolves all references, and makes the registry deeply immutable.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
registry = Schemurai::SchemaRegistry.new(
|
|
171
|
+
schemas: {
|
|
172
|
+
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 },
|
|
173
|
+
"https://example.test/value" => { "$ref" => "https://example.test/positive" }
|
|
174
|
+
},
|
|
175
|
+
# enable virtual machine backend that is faster for iterative validation.
|
|
176
|
+
backend: :vm
|
|
177
|
+
)
|
|
178
|
+
registry.make_shareable
|
|
179
|
+
|
|
180
|
+
# Each thread or Ractor creates and owns its validator.
|
|
181
|
+
validator = registry.validator_for("https://example.test/value")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`make_shareable` calls `Ractor.make_shareable` internally. It raises a
|
|
185
|
+
`ResolutionError` if a reference cannot be resolved. After it returns,
|
|
186
|
+
`validator_for` is read-only and may be called concurrently, while `compile` is
|
|
187
|
+
no longer available. A `Validator` contains per-validation mutable state and
|
|
188
|
+
must not be shared between threads or Ractors. With the VM and native backends,
|
|
189
|
+
the registry compiles and shares its bytecode before this transition. The native
|
|
190
|
+
backend also prepares shared native instruction tables for all four combinations
|
|
191
|
+
of `content` and `format`; `validator_for` does not recompile them in each Ractor.
|
|
192
|
+
Native regex search caches and Ruby runtime helper state belong to each
|
|
193
|
+
validator. Preparing these tables adds time and memory to `make_shareable`.
|
|
194
|
+
|
|
195
|
+
### Meta-schema validation
|
|
196
|
+
|
|
197
|
+
Meta-schema validation is opt-in so ordinary compilation does not pay its
|
|
198
|
+
additional time and memory cost. Set `validate_schema: true` when creating a
|
|
199
|
+
`SchemaRegistry` to validate every schema registered through `schemas:` and
|
|
200
|
+
every schema passed to `compile`. The convenience `compile`, `validate`, and
|
|
201
|
+
`valid?` methods accept the same option and apply it to their internal registry.
|
|
202
|
+
Schemas without `$schema` use the Draft 7 meta-schema. An invalid schema raises
|
|
203
|
+
`Schemurai::InvalidSchemaError`, whose `result` contains the detailed validation
|
|
204
|
+
errors.
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
Schemurai.compile(schema, validate_schema: true)
|
|
208
|
+
|
|
209
|
+
registry = Schemurai::SchemaRegistry.new(
|
|
210
|
+
schemas: external_schemas,
|
|
211
|
+
validate_schema: true
|
|
212
|
+
)
|
|
213
|
+
validator = registry.compile(schema)
|
|
214
|
+
|
|
215
|
+
result = Schemurai.validate_schema(schema)
|
|
216
|
+
result.valid? # whether the schema itself is valid
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`SchemaRegistry#validate_schema` and `#valid_schema?` perform explicit checks
|
|
220
|
+
regardless of the registry option and reuse meta-schema graph data within that
|
|
221
|
+
registry. The stateful validator used for a check is temporary; no global
|
|
222
|
+
validator is retained.
|
|
223
|
+
|
|
224
|
+
### Opt-in validation limits (experimental)
|
|
225
|
+
|
|
226
|
+
`max_errors:` and `timeout:` are optional kwargs. Both default to `nil`
|
|
227
|
+
(unlimited) and apply independently to each validation call. `max_errors` must
|
|
228
|
+
be a nonnegative Integer; `timeout` must be finite nonnegative Integer or Float
|
|
229
|
+
seconds. No settings object is needed.
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
validator = Schemurai.compile(schema, max_errors: 100, timeout: 0.1)
|
|
233
|
+
begin
|
|
234
|
+
result = validator.validate(order)
|
|
235
|
+
rescue Schemurai::LimitExceeded => error
|
|
236
|
+
# Evaluation is incomplete; no partial result is returned.
|
|
237
|
+
warn "Validation stopped at #{error.limit}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
validator.validate(order, max_errors: 10) # retains the compiled timeout
|
|
241
|
+
validator.validate(order, timeout: nil) # retains the compiled error cap
|
|
242
|
+
validator.validate(order, max_errors: nil, timeout: nil) # disables both
|
|
243
|
+
validator.valid?(order) # applies the timeout, ignores max_errors
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`SchemaRegistry#compile`, `#validator_for`, the convenience `validate` / `valid?`
|
|
247
|
+
methods and explicit meta-schema checks also accept these kwargs. Overrides
|
|
248
|
+
replace only the specified option. Settings belong to each compiled validator;
|
|
249
|
+
a shareable registry retains no mutable invocation counters.
|
|
250
|
+
|
|
251
|
+
The error cap checks before constructing the next emitted diagnostic. Exactly
|
|
252
|
+
`max_errors` errors may be returned; attempting another raises `LimitExceeded`
|
|
253
|
+
with `limit == :max_errors`, `maximum` and `attempted` counts. Suppressed
|
|
254
|
+
applicator trials do not consume the cap. Boolean checks ignore the error cap
|
|
255
|
+
and use the ordinary evaluator when no timeout is set.
|
|
256
|
+
|
|
257
|
+
The timeout uses a monotonic elapsed-time deadline, including diagnostic
|
|
258
|
+
rendering and native runtime helper calls and retries. It checks at entry,
|
|
259
|
+
before returning and every 256 checkpoints in evaluation, container scans and
|
|
260
|
+
structural traversal.
|
|
261
|
+
Ruby string/content operations also check before and after expensive calls.
|
|
262
|
+
Zero seconds stops even a trivial call. Expiry raises `LimitExceeded` with
|
|
263
|
+
`limit == :timeout`; `maximum` and `attempted` are configured and elapsed seconds.
|
|
264
|
+
Native returns an exhaustion status and releases Rust resources before Ruby
|
|
265
|
+
raises. A limit hit is terminal. Native validation does not fall back to VM
|
|
266
|
+
execution, with or without invocation limits.
|
|
267
|
+
|
|
268
|
+
This implementation never uses Ruby `Timeout`, timer threads, signals or
|
|
269
|
+
asynchronous exception injection. It is cooperative: one regexp search, scalar
|
|
270
|
+
operation, JSON decode, allocation or custom format callback may overrun until
|
|
271
|
+
the next checkpoint. GC and descheduling count toward elapsed time; the polling
|
|
272
|
+
interval bounds skipped checkpoints, not elapsed overrun. Compilation,
|
|
273
|
+
registration and automatic compile-time meta-schema validation are outside the
|
|
274
|
+
timeout. Explicit meta-schema checks can set their own timeout.
|
|
275
|
+
|
|
276
|
+
Use the error cap to bound diagnostic count and the timeout to bound prolonged
|
|
277
|
+
traversal. They protect different resources: a deadline does not bound the
|
|
278
|
+
number of diagnostics, and an error cap cannot stop a long successful check.
|
|
279
|
+
Input byte size and nesting depth belong at the parsing boundary. An error cap
|
|
280
|
+
does not bound the size of an individual message. Preempting individual scalar
|
|
281
|
+
operations requires operation-specific limits or a supervised worker process.
|
|
282
|
+
The backends retain their respective Ruby, VM and native execution paths and
|
|
283
|
+
native memoization. Timed native bulk pattern/format/content and Fixnum contains
|
|
284
|
+
scans use per-item evaluation so successful scans can poll.
|
|
285
|
+
|
|
286
|
+
### Backend selection
|
|
287
|
+
|
|
288
|
+
`Schemurai.backend`, `SchemaRegistry#backend`, and `Validator#backend` expose
|
|
289
|
+
the selected backend. Pass `backend: :ruby` to force the Ruby oracle or
|
|
290
|
+
`backend: :vm` to ahead-of-time compile schemas for the validator virtual machine.
|
|
291
|
+
Pass `backend: :native` to use the experimental Rust VM extension. Releases
|
|
292
|
+
include precompiled gems on RubyGems.org for CRuby 4.0 on these platforms:
|
|
293
|
+
|
|
294
|
+
| OS | CPU | libc | Gem platform |
|
|
295
|
+
| --- | --- | --- | --- |
|
|
296
|
+
| Linux | x64 | glibc 2.36+ | `x86_64-linux-gnu` |
|
|
297
|
+
| Linux | arm64 | glibc 2.36+ | `aarch64-linux-gnu` |
|
|
298
|
+
| Linux | x64 | musl (Alpine 3.23+) | `x86_64-linux-musl` |
|
|
299
|
+
| Linux | arm64 | musl (Alpine 3.23+) | `aarch64-linux-musl` |
|
|
300
|
+
| macOS 14+ | x64 | — | `x86_64-darwin` |
|
|
301
|
+
| macOS 14+ | arm64 | — | `arm64-darwin` |
|
|
302
|
+
|
|
303
|
+
RubyGems/Bundler selects the matching platform gem during normal installation;
|
|
304
|
+
these gems need no C compiler, Rust, or Ruby development headers. Each platform
|
|
305
|
+
gem bundles its supported Ruby minor ABIs and restricts `required_ruby_version`
|
|
306
|
+
to that range. Ruby patch releases share the same binary. To build from source,
|
|
307
|
+
use `gem install schemurai --platform ruby` or Bundler's `force_ruby_platform`.
|
|
308
|
+
See [native gem releases](docs/native-gem-releases.md) for the build matrix.
|
|
309
|
+
|
|
310
|
+
Building the native extension from source requires a C toolchain, Ruby
|
|
311
|
+
development headers, and Rust 1.93 or newer. If cargo is absent, installation of
|
|
312
|
+
the source gem skips the optional native
|
|
313
|
+
extension and Ruby/VM remain available; explicitly selecting `:native` then
|
|
314
|
+
raises `LoadError`. Native execution borrows Ruby values directly; its current coverage and
|
|
315
|
+
remaining VM delegation are documented below.
|
|
316
|
+
Schema graph construction remains shared Ruby infrastructure. Environment-based selection
|
|
317
|
+
is documented in [`docs/backend-selection.md`](docs/backend-selection.md).
|
|
318
|
+
|
|
319
|
+
## JSON Schema conformance
|
|
320
|
+
|
|
321
|
+
| Capability | Draft 7 | Draft 2019-09 | Draft 2020-12 |
|
|
322
|
+
| --- | --- | --- | --- |
|
|
323
|
+
| Required JSON-Schema-Test-Suite cases | Supported | Supported | Supported |
|
|
324
|
+
| Dialect-specific references | `$ref` | `$ref`, `$recursiveRef` | `$ref`, `$dynamicRef` |
|
|
325
|
+
| `unevaluatedItems` / `unevaluatedProperties` | Not applicable | Supported | Supported |
|
|
326
|
+
| `contentEncoding` / `contentMediaType` assertions | Opt-in[^content] | Opt-in[^content] | Opt-in[^content] |
|
|
327
|
+
|
|
328
|
+
The required-suite row covers every required case for the listed dialect in the
|
|
329
|
+
[official JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
|
|
330
|
+
The applicable top-level optional cases are also tested, including arbitrary
|
|
331
|
+
precision numbers, ECMA-262 regular expressions, anchors, cross-draft references,
|
|
332
|
+
and dynamic references.
|
|
333
|
+
|
|
334
|
+
### Format assertion support
|
|
335
|
+
|
|
336
|
+
Formats are annotations by default in each standard dialect. Pass `format: true`
|
|
337
|
+
to enable the supported assertions below.[^format]
|
|
338
|
+
|
|
339
|
+
| Format | Assertion support |
|
|
340
|
+
| --- | --- |
|
|
341
|
+
| `date` | Supported |
|
|
342
|
+
| `time` | Supported |
|
|
343
|
+
| `date-time` | Supported |
|
|
344
|
+
| `duration` | Supported |
|
|
345
|
+
| `email` | Not supported |
|
|
346
|
+
| `idn-email` | Not supported |
|
|
347
|
+
| `hostname` | Not supported |
|
|
348
|
+
| `idn-hostname` | Not supported |
|
|
349
|
+
| `ipv4` | Supported |
|
|
350
|
+
| `ipv6` | Supported |
|
|
351
|
+
| `uri` | Not supported |
|
|
352
|
+
| `uri-reference` | Not supported |
|
|
353
|
+
| `iri` | Not supported |
|
|
354
|
+
| `iri-reference` | Not supported |
|
|
355
|
+
| `uuid` | Supported |
|
|
356
|
+
| `uri-template` | Not supported |
|
|
357
|
+
| `json-pointer` | Supported |
|
|
358
|
+
| `relative-json-pointer` | Supported |
|
|
359
|
+
| `regex` | Not supported |
|
|
360
|
+
|
|
361
|
+
Unsupported and unknown formats remain annotations when assertion is enabled by
|
|
362
|
+
the caller.
|
|
363
|
+
|
|
364
|
+
[^content]: Pass `content: true` to assert Base64 `contentEncoding` and JSON
|
|
365
|
+
`contentMediaType`. Other encodings and media types remain annotations.
|
|
366
|
+
[^format]: A custom Draft 2020-12 meta-schema that declares the Format-Assertion
|
|
367
|
+
vocabulary can assert supported formats without the option and rejects
|
|
368
|
+
unsupported formats during schema compilation.
|
|
369
|
+
|
|
370
|
+
## Development
|
|
371
|
+
|
|
372
|
+
Run the test suite with:
|
|
373
|
+
|
|
374
|
+
```sh
|
|
375
|
+
bundle exec rspec
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Verify the reviewed official-suite classifications with:
|
|
379
|
+
|
|
380
|
+
```sh
|
|
381
|
+
ruby script/oracle-cases --summary
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The serialized oracle tools are `script/oracle-runner` and
|
|
385
|
+
`script/oracle-compare`.
|
|
386
|
+
|
|
387
|
+
Run the linter with:
|
|
388
|
+
|
|
389
|
+
```sh
|
|
390
|
+
bundle exec rubocop
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## AI Disclosure
|
|
394
|
+
|
|
395
|
+
Large part of this work is generated by OpenAI Codex.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Backend
|
|
5
|
+
VM_FEATURE = "vm/evaluator"
|
|
6
|
+
CHOICES = %i[default ruby vm native].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
|
+
require_relative "native/evaluator" if selection == :native
|
|
22
|
+
selection
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module_function def production_default
|
|
26
|
+
:ruby
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module_function def load_vm!
|
|
30
|
+
require_relative VM_FEATURE
|
|
31
|
+
require_relative "vm/limited_evaluator"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../evaluation"
|
|
4
|
+
require_relative "../error_message"
|
|
5
|
+
|
|
6
|
+
module Schemurai
|
|
7
|
+
module Internal
|
|
8
|
+
module ContainerChecks
|
|
9
|
+
module ArrayDiagnostics
|
|
10
|
+
private def check_array(rules, value, prior_evaluation)
|
|
11
|
+
evaluation = nil
|
|
12
|
+
if rules.max_items && value.length > rules.max_items
|
|
13
|
+
return Evaluation.invalid unless collecting_array_errors?
|
|
14
|
+
|
|
15
|
+
array_error("maxItems", rules, value)
|
|
16
|
+
end
|
|
17
|
+
if rules.min_items && value.length < rules.min_items
|
|
18
|
+
return Evaluation.invalid unless collecting_array_errors?
|
|
19
|
+
|
|
20
|
+
array_error("minItems", rules, value)
|
|
21
|
+
end
|
|
22
|
+
if rules.unique && !unique_items?(value)
|
|
23
|
+
return Evaluation.invalid unless collecting_array_errors?
|
|
24
|
+
|
|
25
|
+
array_error("uniqueItems", rules, value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if (prefix_items = rules.prefix_items)
|
|
29
|
+
prefix_items.each_with_index do |child, index|
|
|
30
|
+
@budget&.checkpoint
|
|
31
|
+
break if index >= value.length
|
|
32
|
+
valid = evaluate_child_at(child, value[index], index, "prefixItems", index)
|
|
33
|
+
return Evaluation.invalid if !valid && !collecting_array_errors?
|
|
34
|
+
(evaluation ||= tracked_evaluation).record_item(index)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
items = rules.items
|
|
39
|
+
if rules.items_list
|
|
40
|
+
items.each_with_index do |child, index|
|
|
41
|
+
@budget&.checkpoint
|
|
42
|
+
break if index >= value.length
|
|
43
|
+
valid = evaluate_child_at(child, value[index], index, "items", index)
|
|
44
|
+
return Evaluation.invalid if !valid && !collecting_array_errors?
|
|
45
|
+
(evaluation ||= tracked_evaluation).record_item(index)
|
|
46
|
+
end
|
|
47
|
+
if value.length > items.length && (additional = rules.additional)
|
|
48
|
+
(items.length...value.length).each do |index|
|
|
49
|
+
@budget&.checkpoint
|
|
50
|
+
valid = evaluate_child_at(additional, value[index], index, "additionalItems")
|
|
51
|
+
return Evaluation.invalid if !valid && !collecting_array_errors?
|
|
52
|
+
(evaluation ||= tracked_evaluation).record_item(index)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
elsif items
|
|
56
|
+
start = rules.prefix_items&.length || 0
|
|
57
|
+
(start...value.length).each do |index|
|
|
58
|
+
@budget&.checkpoint
|
|
59
|
+
valid = evaluate_child_at(items, value[index], index, "items")
|
|
60
|
+
return Evaluation.invalid if !valid && !collecting_array_errors?
|
|
61
|
+
(evaluation ||= tracked_evaluation).record_item(index)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if (contains = rules.contains)
|
|
66
|
+
matched = nil
|
|
67
|
+
index = 0
|
|
68
|
+
while index < value.length
|
|
69
|
+
@budget&.checkpoint
|
|
70
|
+
(matched ||= []) << index if trial(contains, value[index]).valid?
|
|
71
|
+
index += 1
|
|
72
|
+
end
|
|
73
|
+
matched_count = matched ? matched.length : 0
|
|
74
|
+
unless matched_count.between?(rules.min_contains, rules.max_contains)
|
|
75
|
+
return Evaluation.invalid unless collecting_array_errors?
|
|
76
|
+
|
|
77
|
+
array_error("contains", rules, value, matched_count)
|
|
78
|
+
end
|
|
79
|
+
if matched
|
|
80
|
+
matched.each { |index| (evaluation ||= tracked_evaluation).record_item(index) }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if (unevaluated = rules.unevaluated)
|
|
85
|
+
prior_items = prior_evaluation.evaluated_items
|
|
86
|
+
index = 0
|
|
87
|
+
while index < value.length
|
|
88
|
+
@budget&.checkpoint
|
|
89
|
+
if prior_items.include?(index) || evaluation&.evaluated_items&.include?(index)
|
|
90
|
+
index += 1
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
valid = evaluate_child_at(unevaluated, value[index], index, "unevaluatedItems")
|
|
94
|
+
return Evaluation.invalid if !valid && !collecting_array_errors?
|
|
95
|
+
(evaluation ||= tracked_evaluation).record_item(index)
|
|
96
|
+
index += 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
return Evaluation.valid unless evaluation
|
|
100
|
+
|
|
101
|
+
evaluation
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private def collecting_array_errors? = @errors
|
|
105
|
+
|
|
106
|
+
private def array_error(keyword, rules, value, actual = nil)
|
|
107
|
+
add_error(keyword) do
|
|
108
|
+
case keyword
|
|
109
|
+
when "maxItems" then ErrorMessage.size(keyword, rules.max_items, value.length)
|
|
110
|
+
when "minItems" then ErrorMessage.size(keyword, rules.min_items, value.length)
|
|
111
|
+
when "uniqueItems" then ErrorMessage.unique_items
|
|
112
|
+
when "contains" then ErrorMessage.contains(actual, rules.min_contains, rules.max_contains)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|