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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.markdownlint.yml +9 -0
- data/.rubocop.yml +124 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.yardopts +4 -0
- data/AGENTS.md +376 -0
- data/CHANGELOG.md +145 -0
- data/CODE_OF_CONDUCT.md +42 -0
- data/CONTRIBUTING.md +159 -0
- data/GOVERNANCE.md +68 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +132 -0
- data/LICENSE +21 -0
- data/NOTICE.md +28 -0
- data/README.md +484 -0
- data/Rakefile +350 -0
- data/SECURITY.md +98 -0
- data/SECURITY_AUDIT.md +33 -0
- data/SUMMARY.md +30 -0
- data/SUPPORT.md +67 -0
- data/book.toml +9 -0
- data/codecov.yml +11 -0
- data/compatibility.yml +453 -0
- data/deny.toml +7 -0
- data/dry-validation-rust.gemspec +56 -0
- data/ext/dry_validation_rust/fuzz/.gitignore +5 -0
- data/ext/dry_validation_rust/fuzz/corpus/parse_plan/basic_params.json +1 -0
- data/lib/dry/schema.rb +6 -0
- data/lib/dry/validation/rust/block_keyword_parameters.rb +20 -0
- data/lib/dry/validation/rust/config.rb +74 -0
- data/lib/dry/validation/rust/contract/result.rb +180 -0
- data/lib/dry/validation/rust/contract/values.rb +73 -0
- data/lib/dry/validation/rust/contract.rb +400 -0
- data/lib/dry/validation/rust/errors.rb +14 -0
- data/lib/dry/validation/rust/evaluator.rb +295 -0
- data/lib/dry/validation/rust/failures.rb +57 -0
- data/lib/dry/validation/rust/generated_predicates.rb +14 -0
- data/lib/dry/validation/rust/macros.rb +45 -0
- data/lib/dry/validation/rust/message.rb +41 -0
- data/lib/dry/validation/rust/message_backend.rb +115 -0
- data/lib/dry/validation/rust/message_set.rb +159 -0
- data/lib/dry/validation/rust/native.rb +25 -0
- data/lib/dry/validation/rust/native.so +0 -0
- data/lib/dry/validation/rust/path.rb +65 -0
- data/lib/dry/validation/rust/path_trie.rb +64 -0
- data/lib/dry/validation/rust/result.rb +3 -0
- data/lib/dry/validation/rust/rule.rb +62 -0
- data/lib/dry/validation/rust/schema/dsl.rb +84 -0
- data/lib/dry/validation/rust/schema/field_builder.rb +156 -0
- data/lib/dry/validation/rust/schema/field_definition.rb +101 -0
- data/lib/dry/validation/rust/schema/predicate_block.rb +57 -0
- data/lib/dry/validation/rust/schema/processor_hooks.rb +46 -0
- data/lib/dry/validation/rust/schema/result.rb +67 -0
- data/lib/dry/validation/rust/schema/ruby_type_processor.rb +59 -0
- data/lib/dry/validation/rust/schema.rb +323 -0
- data/lib/dry/validation/rust/values.rb +3 -0
- data/lib/dry/validation/rust/version.rb +10 -0
- data/lib/dry/validation/rust.rb +55 -0
- data/lib/dry/validation.rb +66 -0
- data/lib/dry-schema.rb +3 -0
- data/lib/dry-validation.rb +3 -0
- data/lib/dry_validation_rust.rb +3 -0
- data/predicates.yml +67 -0
- data/rust-toolchain.toml +9 -0
- data/supply-chain/audits.toml +4 -0
- data/supply-chain/config.toml +368 -0
- data/supply-chain/imports.lock +4 -0
- data/support_matrix.yml +9 -0
- 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.
|