schemurai 2.2.3 → 3.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -7
  3. data/ext/schemurai_native/Cargo.lock +99 -0
  4. data/ext/schemurai_native/Cargo.toml +21 -0
  5. data/ext/schemurai_native/build.rs +35 -0
  6. data/ext/schemurai_native/extconf.rb +63 -0
  7. data/ext/schemurai_native/glue.c +780 -0
  8. data/ext/schemurai_native/protocol.def +63 -0
  9. data/ext/schemurai_native/protocol.h +10 -0
  10. data/ext/schemurai_native/src/budget.rs +101 -0
  11. data/ext/schemurai_native/src/content.rs +574 -0
  12. data/ext/schemurai_native/src/diagnostics.rs +1135 -0
  13. data/ext/schemurai_native/src/enumeration.rs +154 -0
  14. data/ext/schemurai_native/src/evaluation.rs +219 -0
  15. data/ext/schemurai_native/src/format.rs +357 -0
  16. data/ext/schemurai_native/src/graph.rs +50 -0
  17. data/ext/schemurai_native/src/instance_graph.rs +84 -0
  18. data/ext/schemurai_native/src/lib.rs +3139 -0
  19. data/ext/schemurai_native/src/memo.rs +1149 -0
  20. data/ext/schemurai_native/src/memo_scope.rs +240 -0
  21. data/ext/schemurai_native/src/number.rs +1054 -0
  22. data/ext/schemurai_native/src/pattern.rs +431 -0
  23. data/ext/schemurai_native/src/scope.rs +420 -0
  24. data/ext/schemurai_native/src/structural.rs +86 -0
  25. data/ext/schemurai_native/src/tracking.rs +687 -0
  26. data/lib/schemurai/backend.rb +3 -1
  27. data/lib/schemurai/evaluator.rb +45 -3
  28. data/lib/schemurai/limit_budget.rb +78 -0
  29. data/lib/schemurai/limit_exceeded.rb +18 -0
  30. data/lib/schemurai/limited_evaluator.rb +100 -0
  31. data/lib/schemurai/native/diagnostics.rb +52 -0
  32. data/lib/schemurai/native/evaluator.rb +59 -0
  33. data/lib/schemurai/schema_domain.rb +12 -7
  34. data/lib/schemurai/schema_graph.rb +10 -1
  35. data/lib/schemurai/unique_items.rb +2 -0
  36. data/lib/schemurai/version.rb +1 -1
  37. data/lib/schemurai/vm/compiler.rb +7 -3
  38. data/lib/schemurai/vm/evaluator.rb +50 -3
  39. data/lib/schemurai/vm/limited_evaluator.rb +97 -0
  40. data/lib/schemurai.rb +81 -33
  41. data/sig/schemurai.rbs +21 -13
  42. metadata +36 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4620544f45920ada46eda1beff7cbb6461fb1633031f78d2964c4e963716d82
4
- data.tar.gz: b0a66f4e369181d9182a10715d1622924ebac68cfe32a1169b844714a7b6a038
3
+ metadata.gz: 8fde4b5a2e6ccf979c4948fd13476c8250acf053346de28c4d62b32ed0c92e4f
4
+ data.tar.gz: 7e3a5fb2dfa17195b1a45cde045f7256a1c5f3687f19f8d2c7b821980ffe2ab1
5
5
  SHA512:
6
- metadata.gz: b6dd9a631fa90e9600e0a60ba4e2ecfca492497a69376a11801cf1a4bfb799fb71eea3546f417b394279b7c3795ace0d2ff8422c8fa8c0b9c9cf6e7530ca8f86
7
- data.tar.gz: b8b2e1bdaccdfc420b1aa2cec9fc2bef856157d450ccb547dcf230693b4b959baa77f9901b9df920c5c73bd6ef982b468249655397c0160e55c3b57772b1c2c5
6
+ metadata.gz: 42c0e94ce30932d09c9f1617592338f2386076eca4d176f1e1a9461557ea723abf512f94d7c68fb52a392021e9b45cba4ab329ee3669754b6d0a996f0319a07f
7
+ data.tar.gz: 68b8c4e585cd40e11d4d7fbae8d9345f52a4bfca437704b86999eab79f219cf52e276b623faf0271cd49c4044c596f544399be77ba8a8fd36946f20524708cc2
data/README.md CHANGED
@@ -5,13 +5,18 @@
5
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
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
7
 
8
- A small, light-weight-dependency JSON Schema validator for Ruby supporting Draft 7,
9
- Draft 2019-09, and Draft 2020-12. It covers the required cases in the official
10
- JSON-Schema-Test-Suite, as well as the applicable optional tests for numeric
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
11
12
  precision, ECMA-262 regular expressions, content validation, anchors, and dynamic
12
13
  references. The dialect is selected from the schema's `$schema` URI; schemas that
13
14
  omit `$schema` use Draft 7 for compatibility.
14
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
+
15
20
  Schemurai ships RBS declarations for its public API in `sig/schemurai.rbs`.
16
21
 
17
22
  ```ruby
@@ -180,9 +185,12 @@ validator = registry.validator_for("https://example.test/value")
180
185
  `ResolutionError` if a reference cannot be resolved. After it returns,
181
186
  `validator_for` is read-only and may be called concurrently, while `compile` is
182
187
  no longer available. A `Validator` contains per-validation mutable state and
183
- must not be shared between threads or Ractors. With the VM backend, the registry
184
- compiles and shares its bytecode before this transition, so those validators do
185
- not compile separate instruction streams in each Ractor.
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 VM fallback evaluation state belong to each
193
+ validator. Preparing these tables adds time and memory to `make_shareable`.
186
194
 
187
195
  ### Meta-schema validation
188
196
 
@@ -213,11 +221,77 @@ regardless of the registry option and reuse meta-schema graph data within that
213
221
  registry. The stateful validator used for a check is temporary; no global
214
222
  validator is retained.
215
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-to-VM fallback. It checks at entry, before returning and
259
+ every 256 checkpoints in evaluation, container scans and structural traversal.
260
+ Ruby string/content operations also check before and after expensive calls.
261
+ Zero seconds stops even a trivial call. Expiry raises `LimitExceeded` with
262
+ `limit == :timeout`; `maximum` and `attempted` are configured and elapsed seconds.
263
+ Native returns an exhaustion status and releases Rust resources before Ruby
264
+ raises. A limit hit is terminal and does not trigger fallback.
265
+
266
+ This implementation never uses Ruby `Timeout`, timer threads, signals or
267
+ asynchronous exception injection. It is cooperative: one regexp search, scalar
268
+ operation, JSON decode, allocation or custom format callback may overrun until
269
+ the next checkpoint. GC and descheduling count toward elapsed time; the polling
270
+ interval bounds skipped checkpoints, not elapsed overrun. Compilation,
271
+ registration and automatic compile-time meta-schema validation are outside the
272
+ timeout. Explicit meta-schema checks can set their own timeout.
273
+
274
+ Use the error cap to bound diagnostic count and the timeout to bound prolonged
275
+ traversal. They protect different resources: a deadline does not bound the
276
+ number of diagnostics, and an error cap cannot stop a long successful check.
277
+ Input byte size and nesting depth belong at the parsing boundary. An error cap
278
+ does not bound the size of an individual message. Preempting individual scalar
279
+ operations requires operation-specific limits or a supervised worker process.
280
+ The backends retain their respective Ruby, VM and native execution paths and
281
+ native memoization. Timed native bulk pattern/format/content and Fixnum contains
282
+ scans use per-item evaluation so successful scans can poll.
283
+
216
284
  ### Backend selection
217
285
 
218
286
  `Schemurai.backend`, `SchemaRegistry#backend`, and `Validator#backend` expose
219
- the actual backend. Pass `backend: :ruby` to force the Ruby oracle or
287
+ the selected backend. Pass `backend: :ruby` to force the Ruby oracle or
220
288
  `backend: :vm` to ahead-of-time compile schemas for the validator virtual machine.
289
+ Pass `backend: :native` to use the experimental Rust VM extension. Building the
290
+ native extension requires a C toolchain, Ruby development headers, and Rust
291
+ 1.93 or newer. If cargo is absent, gem installation skips the optional native
292
+ extension and Ruby/VM remain available; explicitly selecting `:native` then
293
+ raises `LoadError`. Native execution borrows Ruby values directly; its current coverage and
294
+ remaining VM delegation are documented below.
221
295
  Schema graph construction remains shared Ruby infrastructure. Environment-based selection
222
296
  is documented in [`docs/backend-selection.md`](docs/backend-selection.md).
223
297
 
@@ -0,0 +1,99 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "aho-corasick"
7
+ version = "1.1.5"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
10
+ dependencies = [
11
+ "memchr",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "base64-simd"
16
+ version = "0.8.0"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
19
+ dependencies = [
20
+ "outref",
21
+ "vsimd",
22
+ ]
23
+
24
+ [[package]]
25
+ name = "base64-turbo"
26
+ version = "0.5.0"
27
+ source = "registry+https://github.com/rust-lang/crates.io-index"
28
+ checksum = "865a269d8d8fc97f16122eae7a74e7e9dffa2df68d5f067cb1f494e9984528db"
29
+ dependencies = [
30
+ "cpufeatures",
31
+ ]
32
+
33
+ [[package]]
34
+ name = "cpufeatures"
35
+ version = "0.3.1"
36
+ source = "registry+https://github.com/rust-lang/crates.io-index"
37
+ checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
38
+ dependencies = [
39
+ "libc",
40
+ ]
41
+
42
+ [[package]]
43
+ name = "libc"
44
+ version = "0.2.190"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
47
+
48
+ [[package]]
49
+ name = "memchr"
50
+ version = "2.8.3"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
53
+
54
+ [[package]]
55
+ name = "outref"
56
+ version = "0.5.2"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
59
+
60
+ [[package]]
61
+ name = "regex-automata"
62
+ version = "0.4.18"
63
+ source = "registry+https://github.com/rust-lang/crates.io-index"
64
+ checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
65
+ dependencies = [
66
+ "aho-corasick",
67
+ "memchr",
68
+ "regex-syntax",
69
+ ]
70
+
71
+ [[package]]
72
+ name = "regex-syntax"
73
+ version = "0.8.11"
74
+ source = "registry+https://github.com/rust-lang/crates.io-index"
75
+ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
76
+
77
+ [[package]]
78
+ name = "ryu"
79
+ version = "1.0.23"
80
+ source = "registry+https://github.com/rust-lang/crates.io-index"
81
+ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
82
+
83
+ [[package]]
84
+ name = "schemurai-native"
85
+ version = "3.0.0"
86
+ dependencies = [
87
+ "base64-simd",
88
+ "base64-turbo",
89
+ "memchr",
90
+ "regex-automata",
91
+ "regex-syntax",
92
+ "ryu",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "vsimd"
97
+ version = "0.8.0"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "schemurai-native"
3
+ version = "3.0.0"
4
+ edition = "2024"
5
+ rust-version = "1.93"
6
+ publish = false
7
+
8
+ [dependencies]
9
+ base64-simd = "0.8.0"
10
+ base64-turbo = "0.5.0"
11
+ memchr = "2.8.3"
12
+ regex-automata = "0.4.18"
13
+ regex-syntax = "0.8.11"
14
+ ryu = "1.0.23"
15
+
16
+ [lib]
17
+ crate-type = ["staticlib"]
18
+
19
+ [profile.release]
20
+ lto = true
21
+ codegen-units = 1
@@ -0,0 +1,35 @@
1
+ use std::{env, fs, path::PathBuf};
2
+
3
+ fn main() {
4
+ println!("cargo:rerun-if-changed=protocol.def");
5
+ let definitions = fs::read_to_string("protocol.def").expect("read native protocol");
6
+ let mut output = String::new();
7
+ for line in definitions.lines() {
8
+ let (fields, opcode) = if let Some(fields) = line.strip_prefix("SN_OPCODE(") {
9
+ (fields, true)
10
+ } else if let Some(fields) = line.strip_prefix("SN_CONSTANT(") {
11
+ (fields, false)
12
+ } else {
13
+ continue;
14
+ };
15
+ let fields: Vec<_> = fields
16
+ .strip_suffix(')')
17
+ .expect("protocol closing parenthesis")
18
+ .split(',')
19
+ .map(str::trim)
20
+ .collect();
21
+ assert_eq!(fields.len(), if opcode { 3 } else { 2 });
22
+ let name = fields[0];
23
+ assert!(
24
+ name.bytes()
25
+ .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == b'_')
26
+ );
27
+ let value: i32 = fields[if opcode { 2 } else { 1 }]
28
+ .parse()
29
+ .expect("protocol integer value");
30
+ let prefix = if opcode { "OP_" } else { "" };
31
+ output.push_str(&format!("pub const {prefix}{name}: i32 = {value};\n"));
32
+ }
33
+ let directory = PathBuf::from(env::var_os("OUT_DIR").expect("Cargo output directory"));
34
+ fs::write(directory.join("protocol.rs"), output).expect("write native protocol constants");
35
+ }
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+
5
+ unless find_executable("cargo")
6
+ warn "cargo was not found; skipping the optional native backend (Ruby and VM remain available)"
7
+ File.write("Makefile", dummy_makefile(".").join)
8
+ exit
9
+ end
10
+ unless enable_config("portable-hash-walk", false)
11
+ if have_func("rb_hash_stlike_foreach", ["ruby.h", "ruby/st.h"])
12
+ # CRuby 3.4/4.0 identity Hashes always use ST storage. Check the private
13
+ # storage flag against a fresh interpreter before enabling the read hint.
14
+ ar_proof = !CROSS_COMPILING && try_run(<<~C)
15
+ #include <ruby.h>
16
+ #include <ruby/version.h>
17
+ #if (RUBY_API_VERSION_MAJOR == 3 && RUBY_API_VERSION_MINOR == 4) || \
18
+ (RUBY_API_VERSION_MAJOR == 4 && RUBY_API_VERSION_MINOR == 0)
19
+ int main(int argc, char **argv) {
20
+ ruby_sysinit(&argc, &argv);
21
+ RUBY_INIT_STACK;
22
+ ruby_init();
23
+ int status = 0;
24
+ ID identity = rb_intern_const("compare_by_identity");
25
+ for (int n = 0; n <= 8; n++) {
26
+ VALUE h = rb_hash_new();
27
+ VALUE id = rb_funcall(rb_hash_new(), identity, 0);
28
+ for (int i = 0; i < n; i++) {
29
+ rb_hash_aset(h, INT2FIX(i), INT2FIX(i));
30
+ rb_hash_aset(id, INT2FIX(i), INT2FIX(i));
31
+ }
32
+ VALUE hashes[] = {h, rb_obj_freeze(rb_hash_dup(h)),
33
+ rb_funcall(rb_hash_dup(h), identity, 0), id};
34
+ for (int i = 0; i < 4; i++) {
35
+ int st = RB_FL_TEST_RAW(hashes[i], RUBY_FL_USER3) != 0;
36
+ if (st != (i >= 2)) status = 1;
37
+ }
38
+ RB_GC_GUARD(h);
39
+ RB_GC_GUARD(id);
40
+ }
41
+ return ruby_cleanup(status);
42
+ }
43
+ #else
44
+ int main(void) { return 1; }
45
+ #endif
46
+ C
47
+ $defs << "-DSCHEMURAI_AR_HASH_PROOF" if ar_proof # rubocop:disable Style/GlobalVars -- mkmf build configuration.
48
+ end
49
+ end
50
+ $LOCAL_LIBS << " $(srcdir)/target/release/libschemurai_native.a" # rubocop:disable Style/GlobalVars -- mkmf build configuration.
51
+ $LOCAL_LIBS << " -lpthread -ldl -lm" unless /mswin|mingw/ =~ RUBY_PLATFORM # rubocop:disable Style/GlobalVars -- mkmf build configuration.
52
+ create_makefile("schemurai_native")
53
+ File.open("Makefile", "a") do |file|
54
+ file.puts <<~MAKE
55
+
56
+ $(TARGET_SO): $(srcdir)/target/release/libschemurai_native.a
57
+
58
+ glue.o: $(srcdir)/protocol.h $(srcdir)/protocol.def
59
+
60
+ $(srcdir)/target/release/libschemurai_native.a: $(srcdir)/Cargo.toml $(srcdir)/Cargo.lock $(srcdir)/build.rs $(srcdir)/protocol.def $(wildcard $(srcdir)/src/*.rs)
61
+ \tcargo build --manifest-path $(srcdir)/Cargo.toml --release --locked
62
+ MAKE
63
+ end