dry-validation-rust 0.1.0.pre6-x64-mingw-ucrt

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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.markdownlint.yml +9 -0
  4. data/.rubocop.yml +124 -0
  5. data/.ruby-version +1 -0
  6. data/.tool-versions +1 -0
  7. data/.yardopts +4 -0
  8. data/AGENTS.md +376 -0
  9. data/CHANGELOG.md +145 -0
  10. data/CODE_OF_CONDUCT.md +42 -0
  11. data/CONTRIBUTING.md +159 -0
  12. data/GOVERNANCE.md +68 -0
  13. data/Gemfile +15 -0
  14. data/Gemfile.lock +132 -0
  15. data/LICENSE +21 -0
  16. data/NOTICE.md +28 -0
  17. data/README.md +484 -0
  18. data/Rakefile +350 -0
  19. data/SECURITY.md +98 -0
  20. data/SECURITY_AUDIT.md +33 -0
  21. data/SUMMARY.md +30 -0
  22. data/SUPPORT.md +67 -0
  23. data/book.toml +9 -0
  24. data/codecov.yml +11 -0
  25. data/compatibility.yml +453 -0
  26. data/deny.toml +7 -0
  27. data/dry-validation-rust.gemspec +56 -0
  28. data/ext/dry_validation_rust/fuzz/.gitignore +5 -0
  29. data/ext/dry_validation_rust/fuzz/corpus/parse_plan/basic_params.json +1 -0
  30. data/lib/dry/schema.rb +6 -0
  31. data/lib/dry/validation/rust/block_keyword_parameters.rb +20 -0
  32. data/lib/dry/validation/rust/config.rb +74 -0
  33. data/lib/dry/validation/rust/contract/result.rb +180 -0
  34. data/lib/dry/validation/rust/contract/values.rb +73 -0
  35. data/lib/dry/validation/rust/contract.rb +400 -0
  36. data/lib/dry/validation/rust/errors.rb +14 -0
  37. data/lib/dry/validation/rust/evaluator.rb +295 -0
  38. data/lib/dry/validation/rust/failures.rb +57 -0
  39. data/lib/dry/validation/rust/generated_predicates.rb +14 -0
  40. data/lib/dry/validation/rust/macros.rb +45 -0
  41. data/lib/dry/validation/rust/message.rb +41 -0
  42. data/lib/dry/validation/rust/message_backend.rb +115 -0
  43. data/lib/dry/validation/rust/message_set.rb +159 -0
  44. data/lib/dry/validation/rust/native.rb +25 -0
  45. data/lib/dry/validation/rust/native.so +0 -0
  46. data/lib/dry/validation/rust/path.rb +65 -0
  47. data/lib/dry/validation/rust/path_trie.rb +64 -0
  48. data/lib/dry/validation/rust/result.rb +3 -0
  49. data/lib/dry/validation/rust/rule.rb +62 -0
  50. data/lib/dry/validation/rust/schema/dsl.rb +84 -0
  51. data/lib/dry/validation/rust/schema/field_builder.rb +156 -0
  52. data/lib/dry/validation/rust/schema/field_definition.rb +101 -0
  53. data/lib/dry/validation/rust/schema/predicate_block.rb +57 -0
  54. data/lib/dry/validation/rust/schema/processor_hooks.rb +46 -0
  55. data/lib/dry/validation/rust/schema/result.rb +67 -0
  56. data/lib/dry/validation/rust/schema/ruby_type_processor.rb +59 -0
  57. data/lib/dry/validation/rust/schema.rb +323 -0
  58. data/lib/dry/validation/rust/values.rb +3 -0
  59. data/lib/dry/validation/rust/version.rb +10 -0
  60. data/lib/dry/validation/rust.rb +55 -0
  61. data/lib/dry/validation.rb +66 -0
  62. data/lib/dry-schema.rb +3 -0
  63. data/lib/dry-validation.rb +3 -0
  64. data/lib/dry_validation_rust.rb +3 -0
  65. data/predicates.yml +67 -0
  66. data/rust-toolchain.toml +9 -0
  67. data/supply-chain/audits.toml +4 -0
  68. data/supply-chain/config.toml +368 -0
  69. data/supply-chain/imports.lock +4 -0
  70. data/support_matrix.yml +9 -0
  71. metadata +261 -0
data/README.md ADDED
@@ -0,0 +1,484 @@
1
+ # dry-validation-rust
2
+
3
+ `dry-validation-rust` is a performance-oriented hybrid Ruby/Rust validation
4
+ engine with familiar dry-validation-style contract syntax and a precisely
5
+ documented compatible subset. Rust handles the immutable declarative schema
6
+ execution path; Ruby preserves dynamic rules and Ruby-specific semantics.
7
+
8
+ > Note: This is an early-stage project. The side-by-side API is covered by
9
+ > focused tests, differential checks, package verification, and reproducible
10
+ > benchmark evidence, but it is not a full, production-ready drop-in
11
+ > replacement for upstream `dry-validation`.
12
+
13
+ Before adoption, review [the support matrix](docs/SUPPORT_MATRIX.md),
14
+ [compatibility matrix](docs/COMPATIBILITY.md), and
15
+ [verification evidence](docs/VERIFICATION.md) for the exact supported surface,
16
+ platforms, and known boundaries.
17
+
18
+ New users can follow the [Getting started guide](docs/getting-started.md) to
19
+ install the gem and build their first contract.
20
+
21
+ The native extension's [Rust API reference](https://alex-tomilov.github.io/dry-validation-rust/rustdoc/)
22
+ is published with the documentation site.
23
+
24
+ For project participation and reporting routes, see
25
+ [CONTRIBUTING.md](CONTRIBUTING.md), [SUPPORT.md](SUPPORT.md),
26
+ [SECURITY.md](SECURITY.md), [GOVERNANCE.md](GOVERNANCE.md), and the
27
+ [Code of Conduct](CODE_OF_CONDUCT.md). Ask usage questions in
28
+ [GitHub Discussions](https://github.com/alex-tomilov/dry-validation-rust/discussions).
29
+ See the concise [roadmap](docs/ROADMAP.md) for planned outcomes and
30
+ [project-management policy](docs/PROJECT_MANAGEMENT.md) for issue workflow.
31
+
32
+ ## What this project is
33
+
34
+ - Rust owns the immutable schema plan, key lookup and normalization, nested
35
+ traversal, built-in coercion, type checks, native predicates, output
36
+ filtering, and structural error collection.
37
+ - Ruby owns class-level DSL capture, arbitrary rule blocks, injected Ruby
38
+ objects, macros, custom behavior, and Ruby-specific predicate semantics.
39
+
40
+ Rewriting arbitrary Ruby blocks into Rust is neither generally possible nor
41
+ desirable. Calling those blocks through Ruby preserves the feature that makes
42
+ `dry-validation` useful: domain validation can be normal Ruby.
43
+
44
+ ## What this project is not
45
+
46
+ - It is not a proven drop-in replacement for upstream `dry-validation`.
47
+ - It is not a full Rust rewrite of the dry-rb validation stack.
48
+ - It does not claim full upstream compatibility without fixture-backed,
49
+ version-pinned differential evidence.
50
+ - It does not claim general speedups without representative benchmarks.
51
+
52
+ ## Installation
53
+
54
+ When a precompiled gem is published for your platform, install it with:
55
+
56
+ ```bash
57
+ gem install dry-validation-rust
58
+ ```
59
+
60
+ See the [support matrix](docs/SUPPORT_MATRIX.md) for the authoritative version
61
+ and platform status. If a source build is required, see the
62
+ [source-build instructions](docs/getting-started.md#build-from-source).
63
+
64
+ ## Primary safe API
65
+
66
+ For new work, use `require "dry/validation/rust"` and subclass
67
+ `Dry::Validation::Rust::Contract`. The [Getting started guide](docs/getting-started.md)
68
+ has a copy-pasteable contract, rule, error-handling, and web-framework examples.
69
+
70
+ Version, platform, and upstream-reference targets are listed in
71
+ [SUPPORT_MATRIX.md](docs/SUPPORT_MATRIX.md). Supported DSL and semantic
72
+ differences are listed in [COMPATIBILITY.md](docs/COMPATIBILITY.md).
73
+
74
+ ### Side-by-side API stability
75
+
76
+ The public side-by-side API is `Dry::Validation::Rust::Contract`, its nested
77
+ `Result` and `Values` types, and the directly exposed `Schema`, `MessageSet`,
78
+ and `Evaluator` types. Its compatibility policy is defined in the
79
+ [support matrix](docs/SUPPORT_MATRIX.md), with individual classifications in
80
+ [API_STABILITY.md](docs/API_STABILITY.md). The exact-compatibility entrypoints
81
+ are explicitly experimental and are not covered by that policy.
82
+
83
+ ## Loading
84
+
85
+ `require "dry/validation/rust"` exposes only the
86
+ `Dry::Validation::Rust` namespace. It does not define
87
+ `Dry::Validation::Contract` or `Dry::Schema`.
88
+
89
+ The upstream-like `require "dry/validation"` and `require "dry/schema"`
90
+ entrypoints are deprecated and cannot safely coexist with upstream
91
+ `dry-validation` or `dry-schema` in one Ruby process. Use side-by-side mode for
92
+ new work; existing applications should follow the
93
+ [exact-mode migration guide](docs/MIGRATION_FROM_EXACT_MODE.md).
94
+
95
+ ## Supported highlights
96
+
97
+ This section is a summary. Version and platform support is authoritative in
98
+ [SUPPORT_MATRIX.md](docs/SUPPORT_MATRIX.md); feature support is authoritative
99
+ in [COMPATIBILITY.md](docs/COMPATIBILITY.md).
100
+
101
+ - `params`, `json`, and plain `schema` modes.
102
+ - String-key normalization for Params and JSON.
103
+ - Integer, float, decimal, boolean, symbol, Date, DateTime, and Time Params
104
+ coercions.
105
+ - Required/optional keys, `filled`, `maybe`, hashes, arrays, primitive array
106
+ members, and arrays of nested hashes.
107
+ - Numeric and size predicates in Rust; format, inclusion, exclusion, and Ruby
108
+ equality predicates in Ruby for semantic fidelity.
109
+ - Ordered rules that run only when their schema dependencies succeeded.
110
+ - Symbol, dot-string, array, and simple hash rule paths.
111
+ - `value`, `values`, `key?`, `key.failure`, `key(path).failure`,
112
+ `base.failure`, `schema_error?`, `rule_error?`, and
113
+ `base_rule_error?`.
114
+ - `rule.each` with `index:`.
115
+ - Global and class macros, macro arguments, injected `option` values, and
116
+ mutable per-call context.
117
+ - Result hashes, message sets, metadata, full messages, filtering, and Ruby
118
+ pattern matching.
119
+ - Contract inheritance and compatible schema reuse.
120
+
121
+ The complete exclusions and semantic differences are explicit in
122
+ [COMPATIBILITY.md](docs/COMPATIBILITY.md).
123
+
124
+ ## Building from source
125
+
126
+ Meet the [source-build prerequisites](docs/getting-started.md#build-from-source)
127
+ first.
128
+
129
+ Then:
130
+
131
+ ```bash
132
+ bundle install
133
+ bundle exec rake compile
134
+ bundle exec rake test
135
+ ```
136
+
137
+ The source gem declares `rb_sys ~> 0.9` and builds through the ordinary Ruby
138
+ native-extension lifecycle.
139
+
140
+ ## Verification
141
+
142
+ Representative verification evidence, including its pinned runtime versions,
143
+ is recorded in [VERIFICATION.md](docs/VERIFICATION.md).
144
+
145
+ The test suite covers the native plan, coercion modes, nested data, rules,
146
+ rule skipping, array rules, macros, options, context, inheritance, external
147
+ schemas, loading modes, pattern matching, metadata, concurrent calls, malformed
148
+ input resilience, package contents, and differential compatibility fixtures.
149
+
150
+ ## Benchmarking
151
+
152
+ The validation benchmark system has three deliberately separate jobs:
153
+
154
+ 1. an interactive `Benchmark.ips`/`MemoryProfiler` showcase for local
155
+ exploration and screenshots;
156
+ 2. a repeatable publication runner for README, release, post, and CV evidence;
157
+ 3. CI regression gates for detecting performance changes on CI hosts.
158
+
159
+ The detailed protocol and metric wording are documented in
160
+ [`docs/BENCHMARKING.md`](docs/BENCHMARKING.md).
161
+
162
+ ### Interactive comparison
163
+
164
+ Run the representative matrix directly when exploring a change:
165
+
166
+ ```bash
167
+ ruby -Ilib benchmark/schema_throughput.rb
168
+ ENGINE=rust SCENARIO=medium_form ruby -Ilib benchmark/schema_throughput.rb
169
+ ENGINE=upstream SCENARIO=nested_object ruby -Ilib benchmark/schema_throughput.rb
170
+ IPS_WARMUP=3 IPS_TIME=10 MEMORY_PROFILE_N=5000 \
171
+ SCENARIO=array_of_objects ruby -Ilib benchmark/schema_throughput.rb
172
+ ```
173
+
174
+ Text output uses `Benchmark.ips` for warmed throughput, `MemoryProfiler` for
175
+ Ruby-side allocation detail, and the fixed-run path for peak RSS. A single text
176
+ run is useful evidence while developing, but it is not the canonical source for
177
+ README/CV performance claims.
178
+
179
+ The matrix currently covers eleven validation shapes: small, medium, and large
180
+ flat forms; deep nesting; arrays of nested objects; an all-invalid error path;
181
+ sparse optional data; mixed scalar coercions; a large primitive array; wide
182
+ nested data; and a contract that combines declarative schema work with
183
+ Ruby-owned dynamic rules.
184
+
185
+ ### Publication-quality evidence
186
+
187
+ Use the publication runner before updating benchmark claims:
188
+
189
+ ```bash
190
+ bundle exec rake compile
191
+ bundle exec script/benchmark-publication
192
+ ```
193
+
194
+ By default it performs five independent measurements per engine/scenario. It
195
+ first calibrates the iteration count for each scenario, then runs each engine in
196
+ a fresh Ruby process using an engine-specific calibrated `N`, so a faster engine
197
+ is not accidentally measured for a much shorter interval. Engine order alternates
198
+ between runs and scenario order reverses every other run to reduce systematic
199
+ host drift. Successful results are never discarded automatically. The full
200
+ default matrix has 110 measurements, so it performs about 9 minutes of measured
201
+ work before calibration, warmups, and process startup; the runner reports live
202
+ calibration and measurement progress to stderr.
203
+
204
+ The runner records the exact Git SHA and dirty state, Ruby/platform/CPU details,
205
+ YJIT state, Rust toolchain, requested and actually loaded upstream gem versions,
206
+ protocol settings, every raw measurement, medians,
207
+ ranges, median absolute deviation, paired throughput ratios, Ruby allocation
208
+ changes, and peak RSS changes.
209
+
210
+ Results and checkpoints are written under `tmp/benchmarks/` (already ignored by
211
+ Git). If a run is interrupted, resume it from the printed checkpoint path:
212
+
213
+ ```bash
214
+ RESUME_FROM=tmp/benchmarks/publication-...checkpoint.json \
215
+ bundle exec script/benchmark-publication
216
+ ```
217
+
218
+ For a longer final evidence run:
219
+
220
+ ```bash
221
+ RUNS=7 TARGET_SECONDS=10 bundle exec script/benchmark-publication
222
+ ```
223
+
224
+ `RETRIES` applies only to process/tooling failures. The runner does not retry a
225
+ successful measurement merely because it is slow or weakens the speedup claim.
226
+ Publication mode refuses a dirty working tree unless `ALLOW_DIRTY=true` is set;
227
+ dirty runs are exploratory and should not become canonical README/CV evidence.
228
+
229
+ Use `FORMAT=json` on the lower-level benchmark only when tooling needs one
230
+ single fixed-run payload:
231
+
232
+ ```bash
233
+ FORMAT=json ENGINE=all SCENARIO=small_form \
234
+ N=10000 WARMUP=1000 LATENCY_SAMPLES=500 \
235
+ ruby -Ilib benchmark/schema_throughput.rb
236
+ ```
237
+
238
+ ### Process-memory evidence
239
+
240
+ Ruby allocation counters are useful for understanding GC pressure, but an
241
+ allocated-object count is not a whole-process memory measurement. For a
242
+ same-work comparison of the hybrid and upstream implementations, run:
243
+
244
+ ```bash
245
+ bundle exec rake compile
246
+ bundle exec script/benchmark-memory-footprint
247
+ ```
248
+
249
+ The memory runner uses the same validation count and warmup for both engines in
250
+ each scenario. On Linux it records current/peak RSS plus PSS and USS around the
251
+ timed validation loop. Peak RSS includes resident Ruby and Rust/native memory;
252
+ PSS apportions shared pages and USS reports private resident pages. See
253
+ [`docs/MEMORY_BENCHMARKING.md`](docs/MEMORY_BENCHMARKING.md) for exact metric
254
+ semantics and limitations.
255
+
256
+ These are process-footprint metrics, not cumulative bytes allocated over time.
257
+ Ruby object counts from `GC.stat` remain a separate GC-pressure signal.
258
+
259
+ Ruby allocation counters (`GC.stat` and the interactive `MemoryProfiler`
260
+ section) describe Ruby-side allocation activity. Peak RSS is a whole-process
261
+ resident high-water mark and already includes resident Ruby and Rust/native
262
+ memory, but it is not cumulative allocated bytes and it fully counts shared
263
+ resident pages. Use the separate process-memory runner for same-work RSS/PSS/USS
264
+ comparisons. Throughput ratios apply to validation calls after contract/plan
265
+ construction and must not be presented as end-to-end Rails request speedups.
266
+
267
+ Refresh the allocation-regression baseline only after intentionally reviewing
268
+ an allocation change:
269
+
270
+ ```bash
271
+ bundle exec script/record-allocation-baseline
272
+ ```
273
+
274
+ The manual **Record Allocation Baseline** workflow produces the same JSON as an
275
+ artifact without changing the repository. Review its value before replacing
276
+ `benchmark/baseline_allocations.json`; do not accept an allocation regression
277
+ merely by refreshing the baseline.
278
+
279
+ The upstream `dry-validation` gem remains intentionally outside the project
280
+ dependencies. Install the comparison version for the same Ruby before running
281
+ comparison/publication benchmarks. Report the exact upstream version with every
282
+ published result.
283
+
284
+ ### Plan-compilation benchmark
285
+
286
+ Measure native JSON plan deserialization independently of validation calls:
287
+
288
+ ```bash
289
+ cargo bench --locked --manifest-path ext/dry_validation_rust/Cargo.toml --bench plan_compile
290
+ ```
291
+
292
+ CI runs this non-blocking benchmark on Ubuntu and retains the combined
293
+ Criterion reports as the `native-benchmarks` artifact for 30 days. On
294
+ 2026-08-14, the
295
+ following 100-sample Criterion results were measured locally on x86_64 Linux
296
+ (kernel 7.0.0-29-generic, AMD Ryzen 7 5800H) with Rust 1.90.0. Each generated
297
+ Params-mode plan has `validate_keys` enabled; every field is a required string
298
+ with one `min_size(1)` predicate. These figures are local baseline evidence,
299
+ not a cross-host performance guarantee.
300
+
301
+ | Plan size | Criterion estimate (95% confidence interval) | Point estimate |
302
+ | ---------- | -------------------------------------------: | -------------: |
303
+ | 5 fields | 1.8861–1.8954 µs | 1.8905 µs |
304
+ | 50 fields | 20.520–20.686 µs | 20.604 µs |
305
+ | 200 fields | 83.591–87.909 µs | 85.538 µs |
306
+
307
+ ### Coercion benchmark
308
+
309
+ Measure the native Params-mode coercion path independently for common and
310
+ Ruby-fallback literals:
311
+
312
+ ```bash
313
+ cargo bench --locked --manifest-path ext/dry_validation_rust/Cargo.toml --bench coercion
314
+ ```
315
+
316
+ CI runs the plan-compilation and coercion benchmarks non-blockingly on Ubuntu
317
+ and retains their combined Criterion reports as the `native-benchmarks`
318
+ artifact for 30 days.
319
+
320
+ The following coercion results were measured locally on 2026-08-14 with CRuby
321
+ 3.4.4, Rust 1.90.0, and `dry-validation-rust` 0.1.0.pre4 on x86_64 Linux
322
+ (kernel 7.0.0-29-generic, AMD Ryzen 7 5800H). Criterion used 100 samples with
323
+ a 500 ms warm-up and 1 s measurement period per case. These are host-local
324
+ baseline observations, not cross-host performance guarantees. `Infinity` exercises the intentional
325
+ non-finite-float rejection path; the datetime-shaped date literal exercises the Ruby fallback path.
326
+
327
+ | Group | Input | Criterion estimate (95% confidence interval) | Point estimate |
328
+ | ----------------- | ---------------------- | -------------------------------------------: | -------------: |
329
+ | Integer | `42` | 119.60–126.54 ns | 122.99 ns |
330
+ | Integer | `-99` | 116.84–120.98 ns | 118.79 ns |
331
+ | Integer | `1_000` | 129.17–135.89 ns | 132.29 ns |
332
+ | Integer | `0xFF` | 119.38–124.78 ns | 121.89 ns |
333
+ | Float | `3.14` | 131.74–133.14 ns | 132.40 ns |
334
+ | Float | `-2.5e10` | 180.62–181.84 ns | 181.17 ns |
335
+ | Float (rejection) | `Infinity` | 83.982–84.862 ns | 84.402 ns |
336
+ | Boolean | `true` | 80.455–83.671 ns | 81.937 ns |
337
+ | Boolean | `false` | 81.399–86.340 ns | 83.778 ns |
338
+ | Boolean | `1` | 123.77–132.78 ns | 128.38 ns |
339
+ | Boolean | `0` | 132.99–153.74 ns | 143.02 ns |
340
+ | Boolean | `yes` | 99.111–102.16 ns | 100.53 ns |
341
+ | Boolean | `no` | 99.193–100.15 ns | 99.620 ns |
342
+ | Date | `2024-01-01` | 573.70–596.35 ns | 584.53 ns |
343
+ | Date (fallback) | `2024-01-01T12:00:00Z` | 2.7212–3.1991 µs | 2.9518 µs |
344
+ | Decimal | `123.456` | 746.42–781.89 ns | 761.67 ns |
345
+ | Decimal | `0.0000001` | 726.38–738.29 ns | 731.78 ns |
346
+
347
+ ### Predicate benchmark
348
+
349
+ Measure the native comparison, size, and parity predicate paths with Ruby
350
+ values created once before timing:
351
+
352
+ ```bash
353
+ cargo bench --locked --manifest-path ext/dry_validation_rust/Cargo.toml --bench predicates
354
+ ```
355
+
356
+ CI runs the plan-compilation, coercion, and predicate benchmarks non-blockingly
357
+ on Ubuntu and retains their combined Criterion reports as the
358
+ `native-benchmarks` artifact for 30 days.
359
+
360
+ The following results were measured locally on 2026-08-15 with CRuby 3.4.4,
361
+ Rust 1.90.0, and `dry-validation-rust` 0.1.0.pre4 on x86_64 Linux (kernel
362
+ 7.0.0-29-generic, AMD Ryzen 7 5800H). Criterion used 100 samples with a
363
+ 500 ms warm-up and 1 s measurement period per case. Every input passes its
364
+ predicate; values and predicate plans are prepared before the timed loop.
365
+ These are host-local baseline observations, not cross-host performance
366
+ guarantees.
367
+
368
+ | Group | Case | Criterion estimate (95% confidence interval) | Point estimate |
369
+ | ---------- | ----------------- | -------------------------------------------: | -------------: |
370
+ | Comparison | `gt` integer | 8.5136–8.5606 ns | 8.5340 ns |
371
+ | Comparison | `gteq` integer | 8.6687–8.9753 ns | 8.7905 ns |
372
+ | Comparison | `lt` integer | 9.0641–9.5832 ns | 9.3130 ns |
373
+ | Comparison | `lteq` integer | 8.5890–8.6289 ns | 8.6062 ns |
374
+ | Comparison | `gt` float | 7.8649–7.9178 ns | 7.8890 ns |
375
+ | Comparison | `gteq` float | 7.8643–7.8929 ns | 7.8779 ns |
376
+ | Comparison | `lt` float | 7.8780–7.9643 ns | 7.9147 ns |
377
+ | Comparison | `lteq` float | 7.8838–8.6503 ns | 8.2060 ns |
378
+ | Size | `size` string | 33.124–33.230 ns | 33.178 ns |
379
+ | Size | `min_size` string | 35.507–35.567 ns | 35.536 ns |
380
+ | Size | `max_size` string | 32.986–33.263 ns | 33.111 ns |
381
+ | Size | `size` array | 10.187–10.252 ns | 10.216 ns |
382
+ | Size | `min_size` array | 10.253–10.457 ns | 10.338 ns |
383
+ | Size | `max_size` array | 10.160–10.352 ns | 10.228 ns |
384
+ | Size | `size` hash | 10.763–10.849 ns | 10.799 ns |
385
+ | Size | `min_size` hash | 11.384–11.968 ns | 11.639 ns |
386
+ | Size | `max_size` hash | 11.270–11.804 ns | 11.496 ns |
387
+ | Parity | `odd?` integer | 7.8135–7.9664 ns | 7.8716 ns |
388
+ | Parity | `even?` integer | 8.1242–8.2117 ns | 8.1567 ns |
389
+
390
+ ### Full-schema benchmark
391
+
392
+ Measure the native engine end-to-end with plans and Ruby Hash inputs prepared
393
+ before Criterion begins timing:
394
+
395
+ ```bash
396
+ cargo bench --locked --manifest-path ext/dry_validation_rust/Cargo.toml --bench full_schema
397
+ ```
398
+
399
+ The following results were measured locally on 2026-08-15 with CRuby 3.4.4,
400
+ Rust 1.90.0, and `dry-validation-rust` 0.1.0.pre4 on x86_64 Linux (kernel
401
+ 7.0.0-29-generic, AMD Ryzen 7 5800H). Criterion used 100 samples with a
402
+ 3-second warm-up and a 5-second measurement period per scenario. Plans and
403
+ inputs are built once; mixed-validity scenarios cycle their prebuilt inputs.
404
+ These figures measure `Engine::call` only, and are host-local baseline
405
+ evidence—not a comparison with the Ruby contract benchmark or a cross-host
406
+ performance guarantee.
407
+
408
+ | Scenario | Criterion estimate (95% confidence interval) | Point estimate |
409
+ | ---------------- | -------------------------------------------: | -------------: |
410
+ | Small form | 3.9515–3.9641 µs | 3.9573 µs |
411
+ | Medium form | 29.051–29.235 µs | 29.141 µs |
412
+ | Large form | 190.63–191.64 µs | 191.12 µs |
413
+ | 10-level nested | 8.8245–9.1422 µs | 8.9877 µs |
414
+ | 100-object array | 318.85–322.47 µs | 320.55 µs |
415
+ | 20-field invalid | 63.480–64.437 µs | 63.953 µs |
416
+
417
+ ## Representative publication results (2026-08-24)
418
+
419
+ The following publication runs compare the hybrid engine with
420
+ dry-validation 1.11.1 (dry-schema 1.16.0 and dry-types 1.9.1) on CRuby 3.3.7,
421
+ x86_64 Linux, and an AMD Ryzen 7 5800H. They are host-local evidence, not a
422
+ cross-host guarantee or an end-to-end Rails request benchmark.
423
+
424
+ The throughput run measured commit `e26bbb18e90f` in seven isolated Ruby
425
+ processes per engine and scenario, targeting 10 seconds each. It measures
426
+ validation calls after contract/plan construction; ranges show the full set of
427
+ successful measurements.
428
+
429
+ | `SCENARIO` | Rust validations/s, median (range) | Upstream validations/s, median (range) | Median speedup (range) |
430
+ | --------------------- | ---------------------------------: | -------------------------------------: | ---------------------: |
431
+ | `small_form` | 100,083 (84,791–101,865) | 40,111 (36,933–41,111) | 2.50× (2.11–2.56) |
432
+ | `medium_form` | 14,517 (13,099–14,657) | 2,792 (2,439–2,921) | 5.14× (4.97–5.51) |
433
+ | `large_form` | 2,140 (1,816–2,153) | 307 (275–344) | 6.73× (6.24–7.36) |
434
+ | `nested_object` | 56,291 (50,939–62,997) | 19,402 (17,589–20,917) | 2.98× (2.90–3.15) |
435
+ | `array_of_objects` | 4,411 (4,202–4,778) | 800 (709–832) | 5.68× (5.33–5.93) |
436
+ | `all_invalid` | 5,397 (5,057–5,625) | 823 (762–887) | 6.44× (6.16–7.38) |
437
+ | `sparse_optional` | 30,623 (29,528–32,315) | 7,345 (7,073–8,073) | 4.12× (3.66–4.41) |
438
+ | `mixed_types` | 38,408 (36,599–39,227) | 15,420 (14,069–16,033) | 2.48× (2.45–2.69) |
439
+ | `array_of_primitives` | 16,656 (15,718–17,299) | 3,220 (3,083–3,272) | 5.20× (4.80–5.32) |
440
+ | `wide_nested_object` | 8,472 (7,993–8,802) | 3,744 (3,440–4,002) | 2.26× (2.06–2.33) |
441
+ | `ruby_rules` | 18,127 (16,774–19,774) | 8,812 (8,487–9,239) | 2.09× (1.93–2.16) |
442
+
443
+ The separate process-memory run measured commit `0f2f46414bc9`, also with
444
+ seven runs and identical validation count/warmup for both engines within each
445
+ scenario. Peak RSS is a whole-process high-water mark during the loop; the
446
+ Linux-only PSS and USS measurements are taken after it. PSS apportions shared
447
+ pages and USS counts private resident pages.
448
+
449
+ | `SCENARIO` | Peak RSS reduction | PSS reduction | USS reduction | Ruby object reduction |
450
+ | --------------------- | -----------------: | ----------------: | ----------------: | --------------------: |
451
+ | `small_form` | 12.9% (12.6–13.2) | 14.8% (14.5–15.0) | 16.2% (15.8–16.4) | -42.9% |
452
+ | `medium_form` | 12.3% (12.2–12.7) | 14.6% (14.3–14.8) | 15.9% (15.6–16.1) | 63.7% |
453
+ | `large_form` | 11.2% (10.7–11.4) | 12.9% (12.2–13.1) | 14.1% (13.5–14.3) | 74.9% |
454
+ | `nested_object` | 13.1% (12.8–13.4) | 15.3% (15.0–15.6) | 16.7% (16.4–17.0) | -13.3% |
455
+ | `array_of_objects` | 13.0% (12.2–13.1) | 15.1% (14.4–15.4) | 16.4% (15.7–16.7) | 3.6% |
456
+ | `all_invalid` | 9.5% (9.0–9.9) | 10.8% (10.2–11.1) | 12.0% (11.4–12.2) | 78.0% |
457
+ | `sparse_optional` | 15.7% (15.6–16.1) | 18.7% (18.2–18.9) | 20.1% (19.7–20.4) | 39.3% |
458
+ | `mixed_types` | 16.0% (15.4–16.2) | 18.8% (18.5–19.1) | 20.2% (19.9–20.6) | -74.3% |
459
+ | `array_of_primitives` | 12.9% (11.9–13.1) | 14.6% (13.4–14.8) | 15.7% (14.5–15.9) | -2.6% |
460
+ | `wide_nested_object` | 14.8% (14.4–15.2) | 17.1% (16.8–17.7) | 18.5% (18.2–19.1) | -179.6% |
461
+ | `ruby_rules` | 11.4% (10.9–12.3) | 13.1% (12.5–14.2) | 14.4% (13.7–15.4) | 34.2% |
462
+
463
+ Ruby object reduction is a `GC.stat` count, not a byte total; negative values
464
+ mean the hybrid path allocated more Ruby objects. None of RSS, PSS, or USS is a
465
+ measure of cumulative allocated bytes. Reproduce fresh evidence with the
466
+ publication runners above rather than extrapolating these host-local figures.
467
+
468
+ ## Important performance caveat
469
+
470
+ The native engine currently reads and creates Ruby objects, so it runs under
471
+ the GVL. Rust reduces Ruby method dispatch and intermediate DSL execution; it
472
+ does not automatically make validation parallel.
473
+
474
+ A future batch API could copy supported values into Rust-owned memory and
475
+ release the GVL, but serialization/copy cost and Ruby object semantics make
476
+ that a separate feature—not a free property of using Rust.
477
+
478
+ ## License and relationship to dry-rb
479
+
480
+ This code is MIT licensed and independent. `dry-validation` and its related
481
+ dry-rb projects are MIT licensed as well, which permits reimplementation and
482
+ derivative work subject to preserving required notices when source is copied.
483
+ See [NOTICE.md](NOTICE.md). The distinct gem name and explicit non-affiliation
484
+ are intentional.