dry-validation-rust 0.1.0.pre5-aarch64-linux
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/CHANGELOG.md +74 -0
- data/LICENSE +21 -0
- data/NOTICE.md +28 -0
- data/README.md +459 -0
- data/docs/ARCHITECTURE.md +256 -0
- data/docs/COMPATIBILITY.md +198 -0
- data/docs/FEASIBILITY.md +207 -0
- data/docs/SUPPORT_MATRIX.md +66 -0
- data/docs/VERIFICATION.md +128 -0
- data/dry-validation-rust.gemspec +58 -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 +57 -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 +76 -0
- data/lib/dry/validation/rust/schema/field_builder.rb +156 -0
- data/lib/dry/validation/rust/schema/field_definition.rb +99 -0
- data/lib/dry/validation/rust/schema/predicate_block.rb +56 -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 +44 -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
- metadata +233 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Support matrix
|
|
2
|
+
|
|
3
|
+
This matrix describes the public support target for each planned gem line. It
|
|
4
|
+
does not widen the implemented API surface; feature support remains documented
|
|
5
|
+
in [COMPATIBILITY.md](COMPATIBILITY.md).
|
|
6
|
+
|
|
7
|
+
## Versioned targets
|
|
8
|
+
|
|
9
|
+
| Gem line | Ruby | Rust MSRV | Platforms | Upstream reference | Status |
|
|
10
|
+
| -------- | ------- | --------- | -------------------------------------------------- | -------------------------------------------- | ------ |
|
|
11
|
+
| 0.1.x | 3.3-3.5 | 1.75 | Source build; all CI workflows use the pinned MSRV | `dry-validation` 1.11.1, `dry-schema` 1.16.0 | alpha |
|
|
12
|
+
| 0.2.x | TBD | TBD | Native gems under evaluation | pinned before beta | beta |
|
|
13
|
+
|
|
14
|
+
## Support policy
|
|
15
|
+
|
|
16
|
+
- `Dry::Validation::Rust::Contract` is the primary supported API for the
|
|
17
|
+
`0.1.x` line.
|
|
18
|
+
- Familiar dry-validation-style syntax is supported only for the compatible
|
|
19
|
+
subset covered by tests and documented in [COMPATIBILITY.md](COMPATIBILITY.md).
|
|
20
|
+
- Exact compatibility mode owns upstream-like require paths and constants. It
|
|
21
|
+
is experimental, opt-in, and must run in a process isolated from upstream
|
|
22
|
+
`dry-validation` and `dry-schema`.
|
|
23
|
+
- Runtime support means source builds and test coverage for the listed matrix,
|
|
24
|
+
not precompiled native gems unless a later row explicitly says so.
|
|
25
|
+
|
|
26
|
+
## Semantic versioning policy
|
|
27
|
+
|
|
28
|
+
Releases follow [Semantic Versioning 2.0.0](https://semver.org/). The version
|
|
29
|
+
in `Dry::Validation::Rust::VERSION` is authoritative for the Ruby gem. A
|
|
30
|
+
release must use at least the bump shown below; multiple changes use the
|
|
31
|
+
largest applicable bump.
|
|
32
|
+
|
|
33
|
+
Before 1.0, a breaking change to a supported surface requires the next minor
|
|
34
|
+
line (for example, `0.1.x` to `0.2.0`). From 1.0 onward, it requires the next
|
|
35
|
+
major line. This preserves the `0.1.x` side-by-side API promise while keeping
|
|
36
|
+
the eventual stable-release policy conventional.
|
|
37
|
+
|
|
38
|
+
| Change type | Minimum version bump |
|
|
39
|
+
| ------------------------------------------------------------------------------------------------ | -------------------------------------------- |
|
|
40
|
+
| Backwards-compatible bug fix, documentation correction, or internal native implementation change | Patch (`0.x.y`) |
|
|
41
|
+
| New backwards-compatible Ruby predicate or schema feature | Minor (`0.x.0`) |
|
|
42
|
+
| Addition of a supported precompiled platform gem | Minor (`0.x.0`) |
|
|
43
|
+
| Increase to the documented Rust MSRV | Minor (`0.x.0`) |
|
|
44
|
+
| Removal of a supported platform, Ruby version, or precompiled platform gem | Next minor before 1.0; major from 1.0 onward |
|
|
45
|
+
| Incompatible change or removal in the documented public side-by-side Ruby API | Next minor before 1.0; major from 1.0 onward |
|
|
46
|
+
| Native ABI-incompatible change, including an incompatible Magnus or CRuby ABI support change | Next minor before 1.0; major from 1.0 onward |
|
|
47
|
+
|
|
48
|
+
The compatibility shim is experimental and excluded from the public API
|
|
49
|
+
compatibility guarantee. Changes to its documented support status still follow
|
|
50
|
+
this policy when they remove a listed runtime or platform target. A change to
|
|
51
|
+
the Rust crate's internal implementation is not an ABI break by itself; the
|
|
52
|
+
ABI rule applies when it changes which CRuby/Magnus ABI combinations the gem
|
|
53
|
+
supports or requires users to rebuild incompatible native artifacts.
|
|
54
|
+
|
|
55
|
+
## Product architecture note
|
|
56
|
+
|
|
57
|
+
The current gem contains both the safe namespace and the exact compatibility
|
|
58
|
+
shim. If the exact shim becomes expensive to maintain or its collision surface
|
|
59
|
+
creates user confusion, the intended split is:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
dry-validation-rust
|
|
63
|
+
dry-validation-rust-compat
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
No split is planned for `0.1.x` without concrete maintenance evidence.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Verification Guide
|
|
2
|
+
|
|
3
|
+
Status: living document.
|
|
4
|
+
Last updated: 2026-07-29.
|
|
5
|
+
|
|
6
|
+
This document describes how to verify the claims made in the project
|
|
7
|
+
documentation. Every claim in ARCHITECTURE.md, COMPATIBILITY.md, and the
|
|
8
|
+
README should be traceable to a command or test described here.
|
|
9
|
+
|
|
10
|
+
## Quick Verification
|
|
11
|
+
|
|
12
|
+
Run the full verification suite:
|
|
13
|
+
|
|
14
|
+
script/verify
|
|
15
|
+
|
|
16
|
+
This executes:
|
|
17
|
+
|
|
18
|
+
1. Ruby unit and integration tests (`bundle exec rake test`).
|
|
19
|
+
2. Differential compatibility tests against pinned upstream.
|
|
20
|
+
3. RuboCop lint.
|
|
21
|
+
4. Package metadata audit.
|
|
22
|
+
|
|
23
|
+
## Milestone Verification Status
|
|
24
|
+
|
|
25
|
+
| Milestone | Verification Method | Status |
|
|
26
|
+
| ------------------------ | ---------------------------------------------------------------------------------- | ----------- |
|
|
27
|
+
| A — Trustworthy Baseline | `script/verify`, CI workflows, package audit | ✅ Verified |
|
|
28
|
+
| B — Common Schema Subset | Differential corpus (80+ cases), Rust unit tests (28), malformed-input corpus (64) | ✅ Verified |
|
|
29
|
+
| C — Ordinary Rules | `rules_test.rb`, differential rule cases | 🔵 Partial |
|
|
30
|
+
| D — Performance Proof | `benchmark/schema_throughput.rb` (no published results yet) | ⚪ Pending |
|
|
31
|
+
| E–G | Not yet applicable | ⚪ Pending |
|
|
32
|
+
|
|
33
|
+
## Ruby-Side Evidence
|
|
34
|
+
|
|
35
|
+
### Unit Tests
|
|
36
|
+
|
|
37
|
+
bundle exec rake test
|
|
38
|
+
|
|
39
|
+
Covers: schema definition, coercion, predicates, rules, result API, messages,
|
|
40
|
+
config, contract DSL, evaluator, path, failures, values.
|
|
41
|
+
|
|
42
|
+
### Differential Compatibility
|
|
43
|
+
|
|
44
|
+
bundle exec ruby test/differential_compatibility_test.rb
|
|
45
|
+
|
|
46
|
+
Runs each fixture case in an isolated subprocess against both this gem and
|
|
47
|
+
pinned upstream `dry-validation` 1.11.1. Compares:
|
|
48
|
+
|
|
49
|
+
- Output values (deep equality).
|
|
50
|
+
- Error messages (deep equality).
|
|
51
|
+
- Unsupported constructs raise the expected error class.
|
|
52
|
+
|
|
53
|
+
Fixtures live in `test/fixtures/differential/`.
|
|
54
|
+
|
|
55
|
+
### Unsupported Constructs
|
|
56
|
+
|
|
57
|
+
Six cases verify that unsupported upstream features (hints, i18n, monads,
|
|
58
|
+
macros, each with complex blocks, dry-schema composition) raise
|
|
59
|
+
`UnsupportedFeatureError` rather than silently producing wrong results.
|
|
60
|
+
|
|
61
|
+
## Rust-Side Evidence
|
|
62
|
+
|
|
63
|
+
### Unit Tests
|
|
64
|
+
|
|
65
|
+
cd ext/dry_validation_rust && cargo test
|
|
66
|
+
|
|
67
|
+
21 tests covering:
|
|
68
|
+
|
|
69
|
+
- Plan deserialization and version checking (`plan.rs`).
|
|
70
|
+
- Coercion edge cases: `"Infinity"`, `"NaN"`, `"1_000"`, empty strings,
|
|
71
|
+
overflow, unicode (`coercion.rs`).
|
|
72
|
+
- Predicate evaluation: boundary values, type mismatches (`predicates.rs`).
|
|
73
|
+
- Message interpolation: token substitution, missing tokens (`messages.rs`).
|
|
74
|
+
|
|
75
|
+
### MSRV
|
|
76
|
+
|
|
77
|
+
The repository pins Rust 1.75.0 in `rust-toolchain.toml`; all CI workflows use
|
|
78
|
+
that exact toolchain. Verify the locked crate and its test suite explicitly
|
|
79
|
+
with:
|
|
80
|
+
|
|
81
|
+
cargo +1.75.0 check --locked --manifest-path ext/dry_validation_rust/Cargo.toml
|
|
82
|
+
cargo +1.75.0 test --locked --manifest-path ext/dry_validation_rust/Cargo.toml
|
|
83
|
+
|
|
84
|
+
### Clippy
|
|
85
|
+
|
|
86
|
+
cd ext/dry_validation_rust && cargo clippy -- -D warnings
|
|
87
|
+
|
|
88
|
+
### Malformed-Input Resilience
|
|
89
|
+
|
|
90
|
+
64-input seeded corpus in `test/malformed_input_test.rb` exercises the Rust
|
|
91
|
+
engine with:
|
|
92
|
+
|
|
93
|
+
- Deeply nested structures (up to depth 200, guarded by recursion limit).
|
|
94
|
+
- Oversized strings (1 MB+).
|
|
95
|
+
- Mixed-type arrays.
|
|
96
|
+
- Null bytes and invalid UTF-8 sequences.
|
|
97
|
+
- Extreme numeric values (MAX_INT, MIN_INT, MAX_FLOAT, NaN, Infinity).
|
|
98
|
+
|
|
99
|
+
## Compatibility Matrix
|
|
100
|
+
|
|
101
|
+
See `COMPATIBILITY.md` for the feature-by-feature matrix. Each row marked
|
|
102
|
+
"✅ implemented and covered" must have at least one differential fixture case.
|
|
103
|
+
|
|
104
|
+
## Benchmarks
|
|
105
|
+
|
|
106
|
+
See `benchmark/schema_throughput.rb`. No published results yet — Milestone D
|
|
107
|
+
will produce `docs/BENCHMARKS.md`.
|
|
108
|
+
|
|
109
|
+
## CI Workflows
|
|
110
|
+
|
|
111
|
+
| Workflow | Trigger | Purpose |
|
|
112
|
+
| ------------------- | ---------------- | ------------------------------------------------- |
|
|
113
|
+
| `ci.yml` | push, PR | Tests + lint + compile |
|
|
114
|
+
| `compatibility.yml` | push, PR, weekly | Differential suite against pinned upstream |
|
|
115
|
+
| `fuzz.yml` | weekly, manual | Five-minute `cargo fuzz` plan-deserialization run |
|
|
116
|
+
| `package.yml` | push, PR | Gemspec/Cargo metadata audit |
|
|
117
|
+
| `security.yml` | push, PR, weekly | `cargo audit` + `bundle audit` |
|
|
118
|
+
|
|
119
|
+
## Reproducing a Specific Claim
|
|
120
|
+
|
|
121
|
+
| Claim | Command |
|
|
122
|
+
| -------------------------------- | ----------------------------------------------------------------- |
|
|
123
|
+
| "80+ differential cases pass" | `bundle exec ruby test/differential_compatibility_test.rb` |
|
|
124
|
+
| "21 Rust unit tests pass" | `cd ext/dry_validation_rust && cargo test` |
|
|
125
|
+
| "MSRV build and tests pass" | `cargo +1.75.0 check --locked … && cargo +1.75.0 test --locked …` |
|
|
126
|
+
| "64 malformed inputs handled" | `bundle exec ruby test/malformed_input_test.rb` |
|
|
127
|
+
| "Package metadata is consistent" | `script/verify` (package audit step) |
|
|
128
|
+
| "RuboCop clean" | `bundle exec rubocop` |
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/dry/validation/rust/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'dry-validation-rust'
|
|
7
|
+
spec.version = Dry::Validation::Rust::VERSION
|
|
8
|
+
spec.authors = ['Alexey Tomilov']
|
|
9
|
+
|
|
10
|
+
spec.summary = 'Experimental Rust-backed validation contracts with a dry-validation-like API'
|
|
11
|
+
spec.description = <<~DESCRIPTION
|
|
12
|
+
A hybrid Ruby/Rust validation gem that compiles a documented subset of
|
|
13
|
+
dry-validation's declarative schema DSL into a native Rust execution plan
|
|
14
|
+
while retaining Ruby rule blocks and Ruby-owned dynamic behavior.
|
|
15
|
+
DESCRIPTION
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
spec.required_ruby_version = '>= 3.3'
|
|
18
|
+
spec.homepage = 'https://github.com/alex-tomilov/dry-validation-rust'
|
|
19
|
+
|
|
20
|
+
spec.files = [
|
|
21
|
+
'CHANGELOG.md',
|
|
22
|
+
'LICENSE',
|
|
23
|
+
'NOTICE.md',
|
|
24
|
+
'predicates.yml',
|
|
25
|
+
'README.md',
|
|
26
|
+
'dry-validation-rust.gemspec',
|
|
27
|
+
'rust-toolchain.toml',
|
|
28
|
+
'docs/ARCHITECTURE.md',
|
|
29
|
+
'docs/COMPATIBILITY.md',
|
|
30
|
+
'docs/FEASIBILITY.md',
|
|
31
|
+
'docs/SUPPORT_MATRIX.md',
|
|
32
|
+
'docs/VERIFICATION.md',
|
|
33
|
+
Dir['lib/**/*.rb'],
|
|
34
|
+
'ext/dry_validation_rust/Cargo.lock',
|
|
35
|
+
'ext/dry_validation_rust/Cargo.toml',
|
|
36
|
+
'ext/dry_validation_rust/extconf.rb',
|
|
37
|
+
Dir['ext/dry_validation_rust/benches/**/*.rs'],
|
|
38
|
+
Dir['ext/dry_validation_rust/src/**/*.rs']
|
|
39
|
+
].flatten.sort
|
|
40
|
+
spec.require_paths = ['lib']
|
|
41
|
+
spec.extensions = ['ext/dry_validation_rust/extconf.rb']
|
|
42
|
+
spec.add_dependency 'bigdecimal', '>= 3.1', '< 5.0'
|
|
43
|
+
spec.add_dependency 'rb_sys', '~> 0.9'
|
|
44
|
+
spec.add_development_dependency 'memory_profiler', '~> 1.1'
|
|
45
|
+
spec.add_development_dependency 'minitest', '~> 6.0'
|
|
46
|
+
spec.add_development_dependency 'mutex_m', '~> 0.2'
|
|
47
|
+
spec.add_development_dependency 'ostruct', '~> 0.6'
|
|
48
|
+
spec.add_development_dependency 'rake', '~> 13.1'
|
|
49
|
+
spec.add_development_dependency 'rake-compiler', '~> 1.3'
|
|
50
|
+
spec.add_development_dependency 'rake-compiler-dock', '~> 1.12'
|
|
51
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
|
52
|
+
|
|
53
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
54
|
+
spec.metadata['source_code_uri'] = 'https://github.com/alex-tomilov/dry-validation-rust'
|
|
55
|
+
spec.metadata['changelog_uri'] = 'https://github.com/alex-tomilov/dry-validation-rust/blob/main/CHANGELOG.md'
|
|
56
|
+
spec.metadata['documentation_uri'] = 'https://github.com/alex-tomilov/dry-validation-rust/blob/main/README.md'
|
|
57
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/alex-tomilov/dry-validation-rust/issues'
|
|
58
|
+
end
|
data/lib/dry/schema.rb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The experimental replacement ships only the minimal schema factories needed
|
|
4
|
+
# by its contract DSL. Loading this entrypoint also loads exact compatibility
|
|
5
|
+
# mode; it is not the full upstream dry-schema gem.
|
|
6
|
+
require 'dry/validation'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
module Validation
|
|
5
|
+
module Rust
|
|
6
|
+
# @api private
|
|
7
|
+
module BlockKeywordParameters
|
|
8
|
+
EMPTY = [].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def extract(block)
|
|
13
|
+
block.parameters.filter_map do |kind, name|
|
|
14
|
+
name if %i[key keyreq].include?(kind)
|
|
15
|
+
end.freeze
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
module Validation
|
|
5
|
+
module Rust
|
|
6
|
+
# Configures the message backend used by compiled schemas.
|
|
7
|
+
class MessageConfig
|
|
8
|
+
BACKENDS = { yaml: YamlBackend, i18n: I18nBackend }.freeze
|
|
9
|
+
|
|
10
|
+
# @return [:yaml, :i18n, Class] the selected built-in identifier or custom backend class.
|
|
11
|
+
attr_reader :backend
|
|
12
|
+
attr_accessor :default_locale, :top_namespace, :load_paths
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@backend = :yaml
|
|
16
|
+
@default_locale = :en
|
|
17
|
+
@top_namespace = :dry_validation
|
|
18
|
+
@load_paths = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Selects a built-in backend or a custom {MessageBackend} subclass.
|
|
22
|
+
#
|
|
23
|
+
# @param backend [:yaml, :i18n, Class] backend identifier or adapter class.
|
|
24
|
+
# @raise [ArgumentError] if the backend is unsupported.
|
|
25
|
+
def backend=(backend)
|
|
26
|
+
@backend = BACKENDS.key?(backend) ? backend : validate_backend_class(backend)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dup
|
|
30
|
+
copy = super
|
|
31
|
+
copy.load_paths = load_paths.dup
|
|
32
|
+
copy
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @api private
|
|
36
|
+
def backend_class
|
|
37
|
+
BACKENDS.fetch(backend, backend)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def validate_backend_class(backend)
|
|
43
|
+
return backend if backend.is_a?(Class) && backend < MessageBackend
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, backend_error(backend)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def backend_error(backend)
|
|
49
|
+
"messages.backend must be :yaml, :i18n, or a MessageBackend subclass; got #{backend.inspect}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Config
|
|
54
|
+
attr_reader :validate_keys
|
|
55
|
+
attr_accessor :messages
|
|
56
|
+
|
|
57
|
+
def initialize
|
|
58
|
+
@validate_keys = false
|
|
59
|
+
@messages = MessageConfig.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def validate_keys=(value)
|
|
63
|
+
@validate_keys = !!value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def dup
|
|
67
|
+
copy = super
|
|
68
|
+
copy.messages = messages.dup
|
|
69
|
+
copy
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
module Validation
|
|
5
|
+
module Rust
|
|
6
|
+
class Contract
|
|
7
|
+
# The outcome of calling a {Contract}.
|
|
8
|
+
#
|
|
9
|
+
# A result contains coerced output, schema and rule failures, and the
|
|
10
|
+
# context supplied to the call. Use {#success?} or {#failure?} to check
|
|
11
|
+
# the outcome, {#to_h} to read the output, and {#errors} to inspect
|
|
12
|
+
# failures.
|
|
13
|
+
#
|
|
14
|
+
# @example Validating input and matching the output
|
|
15
|
+
# result = UserContract.new.call(name: "Ada")
|
|
16
|
+
#
|
|
17
|
+
# case result
|
|
18
|
+
# in { name: String => name }
|
|
19
|
+
# name # => "Ada"
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @see Contract#call
|
|
23
|
+
class Result
|
|
24
|
+
# @return [Schema::Result] structural validation outcome.
|
|
25
|
+
attr_reader :schema_result
|
|
26
|
+
# @return [Hash] context supplied to the contract call.
|
|
27
|
+
attr_reader :context
|
|
28
|
+
|
|
29
|
+
# Creates a result from a schema result and call context.
|
|
30
|
+
#
|
|
31
|
+
# @param schema_result [Schema::Result] structural validation outcome
|
|
32
|
+
# @param context [Hash] context supplied to the contract call
|
|
33
|
+
# @return [Result]
|
|
34
|
+
def initialize(schema_result, context = {})
|
|
35
|
+
@schema_result = schema_result
|
|
36
|
+
@context = context
|
|
37
|
+
@rule_messages = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns validated output wrapped in a Values object.
|
|
41
|
+
#
|
|
42
|
+
# @return [Values] coerced output with key and path access
|
|
43
|
+
def values
|
|
44
|
+
@values ||= Values.new(schema_result.to_h)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns schema and rule errors, optionally with display options.
|
|
48
|
+
#
|
|
49
|
+
# Call {MessageSet#messages} on the returned set for its immutable
|
|
50
|
+
# message-object view, or {MessageSet#to_h} for nested error hashes.
|
|
51
|
+
#
|
|
52
|
+
# @param options [Hash] message rendering options; pass `full: true`
|
|
53
|
+
# to include full message text
|
|
54
|
+
# @return [MessageSet] combined schema and rule errors
|
|
55
|
+
def errors(options = {})
|
|
56
|
+
set = MessageSet.new([*schema_result.messages, *@rule_messages], options)
|
|
57
|
+
options.empty? ? set : set.with(options)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Adds a rule message before finalization and returns this result.
|
|
61
|
+
#
|
|
62
|
+
# @param message [Message] rule failure to add
|
|
63
|
+
# @return [Result] this result
|
|
64
|
+
def add_error(message)
|
|
65
|
+
@rule_messages << message
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns true when no schema or rule messages exist.
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
def success?
|
|
73
|
+
errors.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns true when at least one schema or rule message exists.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def failure?
|
|
80
|
+
!success?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns whether a message exists at or below a path.
|
|
84
|
+
#
|
|
85
|
+
# @param key [Symbol, String, Array, Hash] key or supported path specification
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
def error?(key)
|
|
88
|
+
path = Path.parse(key)
|
|
89
|
+
errors.any? { |message| Path.prefix?(message.path, path) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns whether a schema message exists at or below a path.
|
|
93
|
+
#
|
|
94
|
+
# @param key [Symbol, String, Array, Hash] key or supported path specification
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def schema_error?(key)
|
|
97
|
+
schema_result.error?(key)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns whether a rule message exists at or below a path.
|
|
101
|
+
#
|
|
102
|
+
# @param key [Symbol, String, Array, Hash] key or supported path specification
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
def rule_error?(key)
|
|
105
|
+
path = Path.parse(key)
|
|
106
|
+
@rule_messages.any? { |message| Path.prefix?(message.path, path) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns whether a base-level rule message exists.
|
|
110
|
+
#
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def base_rule_error?
|
|
113
|
+
@rule_messages.any?(&:base?)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Reads a validated value by key or path.
|
|
117
|
+
#
|
|
118
|
+
# @param key [Symbol, String, Array] key or supported path specification
|
|
119
|
+
# @return [Object, nil] coerced value, if present
|
|
120
|
+
def [](key)
|
|
121
|
+
values[key]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns whether validated output contains a key or path.
|
|
125
|
+
#
|
|
126
|
+
# @param key [Symbol, String, Array] key or supported path specification
|
|
127
|
+
# @return [Boolean]
|
|
128
|
+
def key?(key)
|
|
129
|
+
values.key?(key)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns validated output as a Hash.
|
|
133
|
+
#
|
|
134
|
+
# @return [Hash] coerced output
|
|
135
|
+
def to_h
|
|
136
|
+
values.to_h
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Returns a diagnostic representation of output, errors, and context.
|
|
140
|
+
#
|
|
141
|
+
# @return [String]
|
|
142
|
+
def inspect
|
|
143
|
+
if context.empty?
|
|
144
|
+
"#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect}>"
|
|
145
|
+
else
|
|
146
|
+
"#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect} context=#{context.inspect}>"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Deconstructs coerced output for Hash pattern matching.
|
|
151
|
+
#
|
|
152
|
+
# This lets a result match as though it were its output hash, such as
|
|
153
|
+
# `in { name: String => name }`. The call context is not included in
|
|
154
|
+
# this matching form; use {#deconstruct} for positional matching.
|
|
155
|
+
#
|
|
156
|
+
# @param keys [Array<Symbol>, nil] requested keys, or +nil+ for all keys
|
|
157
|
+
# @return [Hash] output entries available to the pattern
|
|
158
|
+
def deconstruct_keys(keys)
|
|
159
|
+
values.deconstruct_keys(keys)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Supports tuple pattern matching as values and context.
|
|
163
|
+
#
|
|
164
|
+
# @return [Array<(Values, Hash)>] coerced output and call context
|
|
165
|
+
def deconstruct
|
|
166
|
+
[values, context]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Prevents further rule-message mutation and returns this result.
|
|
170
|
+
#
|
|
171
|
+
# @return [Result] this finalized result
|
|
172
|
+
def finalize!
|
|
173
|
+
@rule_messages.freeze
|
|
174
|
+
self
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
module Validation
|
|
5
|
+
module Rust
|
|
6
|
+
class Contract
|
|
7
|
+
class Values
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
# @return [Hash] validated output data.
|
|
11
|
+
attr_reader :data
|
|
12
|
+
|
|
13
|
+
# Wraps validated output data for rule and result access.
|
|
14
|
+
def initialize(data)
|
|
15
|
+
@data = data
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Reads a value by key, path, or supported multi-path specification.
|
|
19
|
+
def [](*args)
|
|
20
|
+
return data.dig(*args) if args.length > 1
|
|
21
|
+
|
|
22
|
+
spec = args.fetch(0)
|
|
23
|
+
if spec.is_a?(Hash) && spec.values.first.is_a?(Array)
|
|
24
|
+
head = spec.keys.first
|
|
25
|
+
return spec.values.first.map { |tail| Path.fetch(data, [head, *Path.parse(tail)], nil) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
value = Path.fetch(data, spec)
|
|
29
|
+
value.equal?(Path::Undefined) ? nil : value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns whether data contains a key or path.
|
|
33
|
+
def key?(key)
|
|
34
|
+
Path.key?(data, key)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Iterates through the underlying output Hash.
|
|
38
|
+
def each(&)
|
|
39
|
+
data.each(&)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Fetches a value using Hash#fetch semantics.
|
|
43
|
+
def fetch(*, &)
|
|
44
|
+
data.fetch(*, &)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the underlying output Hash.
|
|
48
|
+
def to_h
|
|
49
|
+
data
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Supports Hash pattern matching against output data.
|
|
53
|
+
def deconstruct_keys(keys)
|
|
54
|
+
keys ? data.slice(*keys) : data
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Reports public Hash methods delegated to underlying data.
|
|
58
|
+
def respond_to_missing?(name, include_private = false)
|
|
59
|
+
data.respond_to?(name, include_private) || super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def method_missing(name, ...)
|
|
65
|
+
return data.public_send(name, ...) if data.respond_to?(name)
|
|
66
|
+
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|