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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +74 -0
  3. data/LICENSE +21 -0
  4. data/NOTICE.md +28 -0
  5. data/README.md +459 -0
  6. data/docs/ARCHITECTURE.md +256 -0
  7. data/docs/COMPATIBILITY.md +198 -0
  8. data/docs/FEASIBILITY.md +207 -0
  9. data/docs/SUPPORT_MATRIX.md +66 -0
  10. data/docs/VERIFICATION.md +128 -0
  11. data/dry-validation-rust.gemspec +58 -0
  12. data/lib/dry/schema.rb +6 -0
  13. data/lib/dry/validation/rust/block_keyword_parameters.rb +20 -0
  14. data/lib/dry/validation/rust/config.rb +74 -0
  15. data/lib/dry/validation/rust/contract/result.rb +180 -0
  16. data/lib/dry/validation/rust/contract/values.rb +73 -0
  17. data/lib/dry/validation/rust/contract.rb +400 -0
  18. data/lib/dry/validation/rust/errors.rb +14 -0
  19. data/lib/dry/validation/rust/evaluator.rb +295 -0
  20. data/lib/dry/validation/rust/failures.rb +57 -0
  21. data/lib/dry/validation/rust/generated_predicates.rb +14 -0
  22. data/lib/dry/validation/rust/macros.rb +45 -0
  23. data/lib/dry/validation/rust/message.rb +41 -0
  24. data/lib/dry/validation/rust/message_backend.rb +115 -0
  25. data/lib/dry/validation/rust/message_set.rb +159 -0
  26. data/lib/dry/validation/rust/native.rb +25 -0
  27. data/lib/dry/validation/rust/native.so +0 -0
  28. data/lib/dry/validation/rust/path.rb +65 -0
  29. data/lib/dry/validation/rust/path_trie.rb +57 -0
  30. data/lib/dry/validation/rust/result.rb +3 -0
  31. data/lib/dry/validation/rust/rule.rb +62 -0
  32. data/lib/dry/validation/rust/schema/dsl.rb +76 -0
  33. data/lib/dry/validation/rust/schema/field_builder.rb +156 -0
  34. data/lib/dry/validation/rust/schema/field_definition.rb +99 -0
  35. data/lib/dry/validation/rust/schema/predicate_block.rb +56 -0
  36. data/lib/dry/validation/rust/schema/processor_hooks.rb +46 -0
  37. data/lib/dry/validation/rust/schema/result.rb +67 -0
  38. data/lib/dry/validation/rust/schema/ruby_type_processor.rb +44 -0
  39. data/lib/dry/validation/rust/schema.rb +323 -0
  40. data/lib/dry/validation/rust/values.rb +3 -0
  41. data/lib/dry/validation/rust/version.rb +10 -0
  42. data/lib/dry/validation/rust.rb +55 -0
  43. data/lib/dry/validation.rb +66 -0
  44. data/lib/dry-schema.rb +3 -0
  45. data/lib/dry-validation.rb +3 -0
  46. data/lib/dry_validation_rust.rb +3 -0
  47. data/predicates.yml +67 -0
  48. data/rust-toolchain.toml +9 -0
  49. 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