schemurai 3.1.0-x86_64-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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +395 -0
  4. data/lib/schemurai/backend.rb +34 -0
  5. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  6. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  7. data/lib/schemurai/container_checks.rb +534 -0
  8. data/lib/schemurai/decimal.rb +11 -0
  9. data/lib/schemurai/dialect.rb +106 -0
  10. data/lib/schemurai/dialect_keywords.rb +91 -0
  11. data/lib/schemurai/dialects/draft2019_09.rb +27 -0
  12. data/lib/schemurai/dialects/draft2020_12.rb +27 -0
  13. data/lib/schemurai/dialects/draft7.rb +27 -0
  14. data/lib/schemurai/ecma_regexp.rb +46 -0
  15. data/lib/schemurai/error_message.rb +102 -0
  16. data/lib/schemurai/evaluation.rb +63 -0
  17. data/lib/schemurai/evaluation_limits.rb +65 -0
  18. data/lib/schemurai/evaluation_pool.rb +66 -0
  19. data/lib/schemurai/evaluator.rb +861 -0
  20. data/lib/schemurai/formats.rb +403 -0
  21. data/lib/schemurai/json_pointer.rb +11 -0
  22. data/lib/schemurai/limit_budget.rb +78 -0
  23. data/lib/schemurai/limit_exceeded.rb +18 -0
  24. data/lib/schemurai/limited_evaluator.rb +43 -0
  25. data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
  26. data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
  27. data/lib/schemurai/meta_schemas/draft7.rb +121 -0
  28. data/lib/schemurai/meta_schemas.rb +28 -0
  29. data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
  30. data/lib/schemurai/native/diagnostics.rb +53 -0
  31. data/lib/schemurai/native/evaluator.rb +87 -0
  32. data/lib/schemurai/native/null_numeric.rb +23 -0
  33. data/lib/schemurai/number_checks.rb +128 -0
  34. data/lib/schemurai/schema_domain.rb +95 -0
  35. data/lib/schemurai/schema_graph.rb +538 -0
  36. data/lib/schemurai/schema_node.rb +55 -0
  37. data/lib/schemurai/string_checks.rb +166 -0
  38. data/lib/schemurai/type_checks.rb +59 -0
  39. data/lib/schemurai/unique_items.rb +68 -0
  40. data/lib/schemurai/value_equality.rb +72 -0
  41. data/lib/schemurai/version.rb +6 -0
  42. data/lib/schemurai/vm/compiler.rb +410 -0
  43. data/lib/schemurai/vm/evaluator.rb +663 -0
  44. data/lib/schemurai/vm/limited_evaluator.rb +27 -0
  45. data/lib/schemurai.rb +307 -0
  46. data/lib/schemurai_native.rb +4 -0
  47. data/sig/schemurai.rbs +108 -0
  48. metadata +121 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c00a32ecafec6236c7388c796820927005741e1eae12a5262d5366996887b0f0
4
+ data.tar.gz: 9da3a657e747265fe2715ab51ac15ad90fa863bec2db4ef24fd2efc947c4619b
5
+ SHA512:
6
+ metadata.gz: '09a726ce41e47fab7e4683633d7f6d44819cf1bad4e5bec07612472c41146f24563fde88818549f20b7d001a407def2a007fba5b606062a044efd43d1842a517'
7
+ data.tar.gz: e135537f2b8edb27ee819d5c5be6b81cc09628c4ec58883664d12b085c7654b8283ffcfae99633f6d6e6d80bdce53528a97a9513525463e25597613833af5790
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
+ [![Gem Version](https://badge.fury.io/rb/schemurai.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/schemurai)
4
+ [![test / rspec](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml/badge.svg)](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml)
5
+ [![lint / rubocop](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml/badge.svg)](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml)
6
+ [![lint / rbs](https://github.com/oakcask/schemurai/actions/workflows/lint--rbs.yaml/badge.svg)](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