lite-validation 0.0.1
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/.rubocop.yml +94 -0
- data/Gemfile +18 -0
- data/README.md +770 -0
- data/bench/calibrational.rb +89 -0
- data/bench/comparative.rb +197 -0
- data/bench/functional.rb +107 -0
- data/bench/profile.rb +45 -0
- data/lib/lite/validation/adapters/interfaces/dry.rb +29 -0
- data/lib/lite/validation/error.rb +9 -0
- data/lib/lite/validation/result/abstract/disputable.rb +17 -0
- data/lib/lite/validation/result/abstract/failure.rb +19 -0
- data/lib/lite/validation/result/abstract/refutable.rb +21 -0
- data/lib/lite/validation/result/abstract/success.rb +25 -0
- data/lib/lite/validation/result/abstract.rb +16 -0
- data/lib/lite/validation/structured_error/record.rb +36 -0
- data/lib/lite/validation/structured_error.rb +23 -0
- data/lib/lite/validation/validator/adapters/errors/default.rb +24 -0
- data/lib/lite/validation/validator/adapters/interfaces/default.rb +32 -0
- data/lib/lite/validation/validator/adapters/interfaces/dry.rb +17 -0
- data/lib/lite/validation/validator/adapters/predicates/dry/adapter.rb +50 -0
- data/lib/lite/validation/validator/adapters/predicates/dry/builder.rb +46 -0
- data/lib/lite/validation/validator/adapters/predicates/dry/engine.rb +32 -0
- data/lib/lite/validation/validator/adapters/predicates/dry.rb +3 -0
- data/lib/lite/validation/validator/coordinator/builder.rb +92 -0
- data/lib/lite/validation/validator/coordinator/default.rb +32 -0
- data/lib/lite/validation/validator/coordinator/errors/builder.rb +56 -0
- data/lib/lite/validation/validator/coordinator/errors/dry.rb +29 -0
- data/lib/lite/validation/validator/coordinator/errors/flat.rb +46 -0
- data/lib/lite/validation/validator/coordinator/errors/hierarchical.rb +29 -0
- data/lib/lite/validation/validator/coordinator/instance.rb +30 -0
- data/lib/lite/validation/validator/coordinator.rb +12 -0
- data/lib/lite/validation/validator/helpers/path.rb +49 -0
- data/lib/lite/validation/validator/node/abstract/branch.rb +21 -0
- data/lib/lite/validation/validator/node/abstract/instance.rb +43 -0
- data/lib/lite/validation/validator/node/abstract/leaf.rb +35 -0
- data/lib/lite/validation/validator/node/abstract.rb +25 -0
- data/lib/lite/validation/validator/node/child.rb +44 -0
- data/lib/lite/validation/validator/node/implementation/apply_ruling.rb +44 -0
- data/lib/lite/validation/validator/node/implementation/dig.rb +38 -0
- data/lib/lite/validation/validator/node/implementation/helpers/call_foreign.rb +31 -0
- data/lib/lite/validation/validator/node/implementation/helpers/with_result.rb +23 -0
- data/lib/lite/validation/validator/node/implementation/helpers/yield_strategy.rb +83 -0
- data/lib/lite/validation/validator/node/implementation/helpers/yield_validator.rb +49 -0
- data/lib/lite/validation/validator/node/implementation/identity.rb +90 -0
- data/lib/lite/validation/validator/node/implementation/iteration/iterator.rb +102 -0
- data/lib/lite/validation/validator/node/implementation/iteration.rb +46 -0
- data/lib/lite/validation/validator/node/implementation/navigation.rb +43 -0
- data/lib/lite/validation/validator/node/implementation/predication.rb +61 -0
- data/lib/lite/validation/validator/node/implementation/scoping/evaluator.rb +43 -0
- data/lib/lite/validation/validator/node/implementation/scoping.rb +43 -0
- data/lib/lite/validation/validator/node/implementation/validation.rb +64 -0
- data/lib/lite/validation/validator/node/implementation/wrap.rb +26 -0
- data/lib/lite/validation/validator/node/root.rb +60 -0
- data/lib/lite/validation/validator/node/suspended.rb +33 -0
- data/lib/lite/validation/validator/node.rb +12 -0
- data/lib/lite/validation/validator/option/none.rb +43 -0
- data/lib/lite/validation/validator/option/some/abstract.rb +29 -0
- data/lib/lite/validation/validator/option/some/complex/registry/abstract.rb +67 -0
- data/lib/lite/validation/validator/option/some/complex/registry/node.rb +47 -0
- data/lib/lite/validation/validator/option/some/complex/registry/root.rb +31 -0
- data/lib/lite/validation/validator/option/some/complex/registry.rb +32 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers/abstract/iterable.rb +31 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers/abstract/non_iterable.rb +27 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers/abstract.rb +35 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers/array.rb +41 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers/hash.rb +40 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers/object.rb +34 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers/tuple.rb +47 -0
- data/lib/lite/validation/validator/option/some/complex/wrappers.rb +5 -0
- data/lib/lite/validation/validator/option/some/complex.rb +24 -0
- data/lib/lite/validation/validator/option/some/dig.rb +34 -0
- data/lib/lite/validation/validator/option/some/simple.rb +23 -0
- data/lib/lite/validation/validator/option/some/singular.rb +29 -0
- data/lib/lite/validation/validator/option/some.rb +20 -0
- data/lib/lite/validation/validator/option.rb +20 -0
- data/lib/lite/validation/validator/predicate/abstract/variants.rb +23 -0
- data/lib/lite/validation/validator/predicate/foreign/adapter/input/single.rb +21 -0
- data/lib/lite/validation/validator/predicate/foreign/adapter/input/tuple.rb +21 -0
- data/lib/lite/validation/validator/predicate/foreign/adapter/input.rb +28 -0
- data/lib/lite/validation/validator/predicate/foreign/adapter/ruling/instance.rb +37 -0
- data/lib/lite/validation/validator/predicate/foreign/adapter/ruling.rb +26 -0
- data/lib/lite/validation/validator/predicate/foreign/engine.rb +27 -0
- data/lib/lite/validation/validator/predicate/foreign/variant.rb +33 -0
- data/lib/lite/validation/validator/predicate/foreign/variants.rb +46 -0
- data/lib/lite/validation/validator/predicate/native/builder.rb +72 -0
- data/lib/lite/validation/validator/predicate/native/definite.rb +19 -0
- data/lib/lite/validation/validator/predicate/native/instance.rb +41 -0
- data/lib/lite/validation/validator/predicate/native/optional.rb +34 -0
- data/lib/lite/validation/validator/predicate/registry.rb +47 -0
- data/lib/lite/validation/validator/predicate.rb +17 -0
- data/lib/lite/validation/validator/result/abstract/failure.rb +21 -0
- data/lib/lite/validation/validator/result/abstract/instance.rb +18 -0
- data/lib/lite/validation/validator/result/abstract/success.rb +17 -0
- data/lib/lite/validation/validator/result/abstract.rb +29 -0
- data/lib/lite/validation/validator/result/committed.rb +75 -0
- data/lib/lite/validation/validator/result/disputable/hash.rb +17 -0
- data/lib/lite/validation/validator/result/disputable/instance.rb +43 -0
- data/lib/lite/validation/validator/result/disputable/iterable/array.rb +23 -0
- data/lib/lite/validation/validator/result/disputable/iterable.rb +17 -0
- data/lib/lite/validation/validator/result/disputable/navigable.rb +35 -0
- data/lib/lite/validation/validator/result/disputable.rb +14 -0
- data/lib/lite/validation/validator/result/disputed/abstract/hash.rb +32 -0
- data/lib/lite/validation/validator/result/disputed/abstract/instance.rb +26 -0
- data/lib/lite/validation/validator/result/disputed/iterable/array.rb +42 -0
- data/lib/lite/validation/validator/result/disputed/iterable/hash.rb +38 -0
- data/lib/lite/validation/validator/result/disputed/iterable.rb +20 -0
- data/lib/lite/validation/validator/result/disputed/navigable.rb +59 -0
- data/lib/lite/validation/validator/result/disputed.rb +17 -0
- data/lib/lite/validation/validator/result/refuted.rb +78 -0
- data/lib/lite/validation/validator/result/valid/abstract/collect.rb +42 -0
- data/lib/lite/validation/validator/result/valid/abstract/commit.rb +25 -0
- data/lib/lite/validation/validator/result/valid/abstract/instance.rb +23 -0
- data/lib/lite/validation/validator/result/valid/iterable/array/abstract.rb +24 -0
- data/lib/lite/validation/validator/result/valid/iterable/array/tuples.rb +64 -0
- data/lib/lite/validation/validator/result/valid/iterable/array/values.rb +42 -0
- data/lib/lite/validation/validator/result/valid/iterable/hash.rb +46 -0
- data/lib/lite/validation/validator/result/valid/iterable.rb +33 -0
- data/lib/lite/validation/validator/result/valid/navigable.rb +68 -0
- data/lib/lite/validation/validator/result/valid.rb +21 -0
- data/lib/lite/validation/validator/result.rb +15 -0
- data/lib/lite/validation/validator/ruling/abstract/invalid.rb +59 -0
- data/lib/lite/validation/validator/ruling/abstract/valid.rb +23 -0
- data/lib/lite/validation/validator/ruling/abstract.rb +12 -0
- data/lib/lite/validation/validator/ruling/commit.rb +17 -0
- data/lib/lite/validation/validator/ruling/dispute.rb +21 -0
- data/lib/lite/validation/validator/ruling/invalidate.rb +32 -0
- data/lib/lite/validation/validator/ruling/pass.rb +19 -0
- data/lib/lite/validation/validator/ruling/refute.rb +21 -0
- data/lib/lite/validation/validator/ruling.rb +53 -0
- data/lib/lite/validation/validator/state/instance.rb +46 -0
- data/lib/lite/validation/validator/state/merge_strategy.rb +50 -0
- data/lib/lite/validation/validator/state/unwrap_strategy.rb +31 -0
- data/lib/lite/validation/validator/state.rb +21 -0
- data/lib/lite/validation/validator.rb +15 -0
- data/lib/lite/validation/version.rb +9 -0
- data/lib/lite/validation.rb +8 -0
- metadata +196 -0
data/README.md
ADDED
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
# Lite::Validation
|
|
2
|
+
A validation framework that works with hashes, arrays as well
|
|
3
|
+
as arbitrary objects. Traverse nested data structures, apply validation rules,
|
|
4
|
+
transform input into new shape through an immutable, composable interface
|
|
5
|
+
that treats validation as a general-purpose computational tool.
|
|
6
|
+
|
|
7
|
+
- Extensible wrapper system supports custom collections (like `ActiveRecord::Relation`)
|
|
8
|
+
- Pluggable predicate engines — ships with `Dry::Logic` adapter for declarative validation
|
|
9
|
+
- Configurable result types and error formats
|
|
10
|
+
- Transform data while validating through integrated commit/transformation mechanics
|
|
11
|
+
|
|
12
|
+
Engineered for consistent performance regardless of validation outcome.
|
|
13
|
+
This makes it ideal for high-throughput scenarios where validation serves
|
|
14
|
+
as filtering, decision-making, or data processing logic — not just input sanitization.
|
|
15
|
+
Whether validating inputs that mostly pass or mostly fail, performance remains predictable.
|
|
16
|
+
Perfect for applications that need validation throughout the system:
|
|
17
|
+
API endpoints, background jobs, data pipelines, and anywhere you need reliable
|
|
18
|
+
validation with transformation capabilities.
|
|
19
|
+
|
|
20
|
+
## Getting started
|
|
21
|
+
Before validating data, you'll need to create a **coordinator** —
|
|
22
|
+
a configuration object that defines how the validator integrates
|
|
23
|
+
with your application. The coordinator specifies what types
|
|
24
|
+
to use for results, options, and errors, making the library adaptable
|
|
25
|
+
to different Ruby ecosystems. Here's a basic setup with reasonable defaults,
|
|
26
|
+
the same setup we’ll use throughout the examples in this documentation:
|
|
27
|
+
|
|
28
|
+
```ruby rspec coordinator_dry_hierarchical
|
|
29
|
+
Hierarchical = Coordinator::Builder.define do
|
|
30
|
+
interface_adapter Adapters::Interfaces::Dry
|
|
31
|
+
validation_error_adapter do
|
|
32
|
+
structured_error do |code, message: nil, data: nil|
|
|
33
|
+
StructuredError::Record.instance(code, message: message, data: data)
|
|
34
|
+
end
|
|
35
|
+
internal_error do |id, message: nil, data: nil|
|
|
36
|
+
message ||= case id
|
|
37
|
+
when :value_missing then 'Value is missing'
|
|
38
|
+
when :not_iterable then 'Value is not iterable'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
structured_error(id, message: message, data: data)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
final_error_adapter Coordinator::Errors::Hierarchical
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This coordinator:
|
|
49
|
+
- Uses `Dry::Monads::Result` for success/failure results and also as a stand-in
|
|
50
|
+
for option type when optional value is yielded to a validation block
|
|
51
|
+
- Stores validation errors into the built-in `StructuredError::Record` class
|
|
52
|
+
- Translates internal framework errors into readable messages
|
|
53
|
+
- Organizes final errors in a hierarchical structure
|
|
54
|
+
|
|
55
|
+
We'll cover advanced configuration options [later](#configuration).
|
|
56
|
+
For now, this setup gives you everything needed to start validating.
|
|
57
|
+
|
|
58
|
+
## Validation
|
|
59
|
+
With a coordinator configured, you can create validators.
|
|
60
|
+
Each validator is initialized with the data, coordinator,
|
|
61
|
+
and an optional context object for sharing state across validations.
|
|
62
|
+
|
|
63
|
+
The validator follows an immutable, fluent design. Each validation method
|
|
64
|
+
(`validate`, `at`, `each_at`, `satisfy`) returns either the original validator
|
|
65
|
+
(if unchanged) or a new validator with updated state. Chain your validations
|
|
66
|
+
together, then call `to_result` to get the final outcome.
|
|
67
|
+
|
|
68
|
+
Here's basic scalar validation:
|
|
69
|
+
|
|
70
|
+
```ruby rspec validation_scalar
|
|
71
|
+
result = Lite::Validation::Validator
|
|
72
|
+
.instance(101, coordinator, context: { limit: 100 })
|
|
73
|
+
.validate { |value| Refute(:not_an_integer) unless value.is_a?(Integer) }
|
|
74
|
+
.validate { |value, context| Dispute(:excessive) if value > context[:limit] }
|
|
75
|
+
.to_result
|
|
76
|
+
|
|
77
|
+
expect(result.failure).to match({ errors: [have_attributes(code: :excessive)] })
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The core of validation is the `validate` method and its counterpart `validate?`.
|
|
81
|
+
These methods expose the current value and context to your validation block,
|
|
82
|
+
expecting a ruling in return — a decision about the value's validity.
|
|
83
|
+
There are four types of ruling available in validate blocks:
|
|
84
|
+
- `Pass()` — Indicates the value is valid. Rarely used since returning
|
|
85
|
+
`nil` has the same effect.
|
|
86
|
+
- `Dispute(code, message: nil, data: nil)` — Marks the value as invalid but allows validation
|
|
87
|
+
to continue on this node. All ancestor nodes also become disputed. You can also pass
|
|
88
|
+
a structured error object: `Dispute(structured_error)`
|
|
89
|
+
- `Refute(code, message: nil, data: nil)` — Marks the value as invalid with a fatal error
|
|
90
|
+
that stops further validation on this node. Parent nodes become disputed unless this
|
|
91
|
+
occurs in a [critical section](#critical-section). Also accepts structured errors: `Refute(structured_error)`.
|
|
92
|
+
- `Commit(value)` — Transforms the input data into a new structure. This enables validation
|
|
93
|
+
with simultaneous data transformation — we'll cover this [later](#transforming-the-validated-object).
|
|
94
|
+
Commited node can't be reopened for validation again, such attempt will trigger runtime error.
|
|
95
|
+
|
|
96
|
+
The distinction between `Dispute` and `Refute` gives you some control over validation flow:
|
|
97
|
+
use `Dispute` for errors where you want validation to continue, and `Refute` for errors serious
|
|
98
|
+
enough to halt processing.
|
|
99
|
+
|
|
100
|
+
### Validating structured data
|
|
101
|
+
The library's capabilities become more apparent with hierarchical data.
|
|
102
|
+
Pass a path as the first argument to `validate` — validator
|
|
103
|
+
will navigate to that value and yield it to the validation block:
|
|
104
|
+
|
|
105
|
+
```ruby rspec validation_hash_aligned
|
|
106
|
+
result = Validator
|
|
107
|
+
.instance({ foo: -1 }, coordinator)
|
|
108
|
+
.validate(:foo) { |foo, _ctx| Dispute(:negative) if foo < 0 }
|
|
109
|
+
.to_result
|
|
110
|
+
|
|
111
|
+
expect(result.failure).to match({ children: { foo: { errors: [have_attributes(code: :negative)] } } })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Separating data location from error location:**
|
|
115
|
+
Use the `from` parameter to validate data from one path but store errors at a different location:
|
|
116
|
+
|
|
117
|
+
```ruby rspec validation_hash_unaligned
|
|
118
|
+
result = Validator
|
|
119
|
+
.instance({ foo: -1 }, coordinator)
|
|
120
|
+
.validate(:bar, from: [:foo]) { |bar, _ctx| Dispute(:negative) if bar < 0 }
|
|
121
|
+
.to_result
|
|
122
|
+
|
|
123
|
+
expect(result.failure).to match({ children: { bar: { errors: [have_attributes(code: :negative)] } } })
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This separation enables two powerful patterns:
|
|
127
|
+
|
|
128
|
+
**1. Meaningful error keys** — Store errors under descriptive names rather than raw data keys:
|
|
129
|
+
|
|
130
|
+
```ruby rspec validation_hash_tuple_unaligned
|
|
131
|
+
result = Validator
|
|
132
|
+
.instance({ subtotal: 80, charges: 21 }, coordinator, context: { limit: 100 })
|
|
133
|
+
.validate(:total, from: [[:subtotal, :charges]]) do |(subtotal, charges), context|
|
|
134
|
+
Dispute(:excessive) if subtotal + charges > context[:limit]
|
|
135
|
+
end.to_result
|
|
136
|
+
|
|
137
|
+
expect(result.failure).to match({ children: { total: { errors: [have_attributes(code: :excessive)] } } })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Note how the `from` parameter accepts an array of paths — this creates a tuple from multiple values,
|
|
141
|
+
perfect for cross-field validations.
|
|
142
|
+
|
|
143
|
+
**2. Data transformation** — Remap input data into new structures using `Commit` rulings.
|
|
144
|
+
The `from` parameter lets you source data from one location while building transformed
|
|
145
|
+
output at another. We'll explore this pattern
|
|
146
|
+
in detail [later](#transforming-the-validated-object).
|
|
147
|
+
|
|
148
|
+
### Alternative syntax
|
|
149
|
+
You can also apply rulings directly to validator nodes rather
|
|
150
|
+
of returning them from validation blocks. This permits
|
|
151
|
+
more concise phrasing in certain cases — for example when passing validator
|
|
152
|
+
into functions:
|
|
153
|
+
|
|
154
|
+
```ruby rspec node_disputed
|
|
155
|
+
def self.validate_total(validator)
|
|
156
|
+
validator.dispute(:excessive, at: [:total]) if validator.value > validator.context[:max]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
validator = Validator.instance(201, coordinator, context: { max: 200 })
|
|
160
|
+
disputed = validate_total(validator)
|
|
161
|
+
|
|
162
|
+
expect(disputed.to_result.failure)
|
|
163
|
+
.to match({ children: { total: { errors: [have_attributes(code: :excessive)] } } })
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Remember that validators are immutable — methods like `dispute`, `refute`, and `commit`
|
|
167
|
+
return new validator instance with updated state.
|
|
168
|
+
|
|
169
|
+
### Handling missing values
|
|
170
|
+
The `validate?` method provides flexible handling of missing values.
|
|
171
|
+
While `validate` immediately refutes nodes when values aren't found,
|
|
172
|
+
`validate?` offers more nuanced options:
|
|
173
|
+
|
|
174
|
+
**Default behavior:** Skip validation entirely if the value is missing — the validator state
|
|
175
|
+
remains unchanged.
|
|
176
|
+
|
|
177
|
+
**With missing value strategies:** Call `validate?` without a block, then chain `.some_or_nil` or `.option`
|
|
178
|
+
to control how missing values are handled:
|
|
179
|
+
|
|
180
|
+
- **`some_or_nil`** — Passes `nil` for missing values. In tuples, only missing fields become `nil`,
|
|
181
|
+
not the entire tuple.
|
|
182
|
+
- **`option`** — Passes an option type (like `Dry::Result::Failure(Unit)` when using the Dry interface).
|
|
183
|
+
Again, in tuples only missing fields become *none* values.
|
|
184
|
+
|
|
185
|
+
The `option` strategy enables validations where fields have disjunctive relationships —
|
|
186
|
+
like "either `:foo` or `:bar` must be set, but not both":
|
|
187
|
+
|
|
188
|
+
```ruby rspec validation_option
|
|
189
|
+
result = Validator
|
|
190
|
+
.instance({ foo: 'FOO', bar: 'BAR' }, coordinator)
|
|
191
|
+
.validate?([:foo, :bar]).option { |(foo, bar), _ctx| Dispute(:xor_violation) unless foo.failure? ^ bar.failure? }
|
|
192
|
+
.to_result
|
|
193
|
+
|
|
194
|
+
expect(result.failure)
|
|
195
|
+
.to match({ children: { [:foo, :bar] => { errors: [have_attributes(code: :xor_violation)] } } })
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Validating objects
|
|
199
|
+
Beyond hashes and arrays, the validator works seamlessly with any Ruby object.
|
|
200
|
+
When navigating to a path, it calls the corresponding reader method on the object:
|
|
201
|
+
|
|
202
|
+
```ruby rspec validation_object
|
|
203
|
+
result = Validator
|
|
204
|
+
.instance(OpenStruct.new(foo: 5), coordinator)
|
|
205
|
+
.validate(:foo) { |foo| Dispute(:not_three) if foo != 3 }
|
|
206
|
+
.to_result
|
|
207
|
+
|
|
208
|
+
expect(result.failure)
|
|
209
|
+
.to match({ children: { foo: { errors: [have_attributes(code: :not_three)] } } })
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Graceful error handling:** If the object doesn't respond to a reader method or raises an exception,
|
|
213
|
+
the validator automatically converts this into a validation error:
|
|
214
|
+
|
|
215
|
+
```ruby rspec validation_object_reader_unimplemented
|
|
216
|
+
result = Validator
|
|
217
|
+
.instance(Object.new, coordinator)
|
|
218
|
+
.validate(:foo) { |foo| Dispute(:not_three) if foo != 3 }
|
|
219
|
+
.to_result
|
|
220
|
+
|
|
221
|
+
expect(result.failure)
|
|
222
|
+
.to match({ children: { foo: { errors: [have_attributes(code: :invalid_access)] } } })
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This means you can validate any object without worrying about method availability — missing methods
|
|
226
|
+
become validation errors rather than runtime exceptions.
|
|
227
|
+
|
|
228
|
+
## Predicates
|
|
229
|
+
Common validation logic can be extracted into reusable **predicates** that you invoke
|
|
230
|
+
by name using the `satisfy` method. This promotes consistency and reduces duplication
|
|
231
|
+
across your validation code.
|
|
232
|
+
|
|
233
|
+
Define predicates using a builder pattern:
|
|
234
|
+
|
|
235
|
+
```ruby rspec predication_define_native
|
|
236
|
+
Predicate.define(:presence) do
|
|
237
|
+
validate_value do |value, _context|
|
|
238
|
+
next Ruling::Invalidate(:blank, message: 'must not be nil') if value.nil?
|
|
239
|
+
|
|
240
|
+
Ruling::Invalidate(:blank, message: 'must not be empty') if value.respond_to?(:empty?) && value.empty?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
validate_option do |option, _context|
|
|
244
|
+
next Ruling::Invalidate(:blank, message: 'must be given') if option.failure?
|
|
245
|
+
|
|
246
|
+
validate_value(option.success)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Key concepts:**
|
|
252
|
+
- **`Ruling::Invalidate`** — A suspended ruling that doesn't specify severity (`dispute` vs `refute`).
|
|
253
|
+
The caller determines severity when using the predicate via `satisfy`.
|
|
254
|
+
- **`validate_value`** — Handles definite values (the common case)
|
|
255
|
+
- **`validate_option`** — Handles optional values from `satisfy?` with the option strategy.
|
|
256
|
+
This is not required — omit if your predicate doesn't need to handle missing values.
|
|
257
|
+
|
|
258
|
+
This separation lets predicates work with both definite and optional values
|
|
259
|
+
while leaving severity decisions to the validation context where they're used.
|
|
260
|
+
|
|
261
|
+
### Declarative predicates
|
|
262
|
+
You can integrate existing predicate libraries through adapters.
|
|
263
|
+
The library ships with a `Dry::Logic` adapter that lets you define
|
|
264
|
+
predicates using Dry's declarative syntax.
|
|
265
|
+
|
|
266
|
+
**Setup:** Require the adapter and configure error handling:
|
|
267
|
+
|
|
268
|
+
```ruby rspec predication_foreign_configuration
|
|
269
|
+
require 'lite/validation/validator/adapters/predicates/dry'
|
|
270
|
+
|
|
271
|
+
error_adapter = proc { |rule, value| StructuredError::Record.instance(:"failed: #{rule}", data: value) }
|
|
272
|
+
|
|
273
|
+
Predicate::Registry.register_adapter :dry, Adapters::Predicates::Dry::Engine.instance(error_adapter)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The error adapter proc converts `Dry::Logic` failures into structured errors.
|
|
277
|
+
It receives the failed rule and the value that caused the failure.
|
|
278
|
+
|
|
279
|
+
**Define predicates:** With the adapter registered, you can create named predicates using
|
|
280
|
+
`Dry::Logic` syntax:
|
|
281
|
+
```ruby rspec predication_define_foreign
|
|
282
|
+
positive_number = Predicate::Registry.engine(:dry).build([:val]) { number? & gt?(0) }
|
|
283
|
+
Predicate::Registry.register_predicate :positive_number, positive_number
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Using predicates with `satisfy`
|
|
287
|
+
The `satisfy` method invokes predefined predicates on validator nodes.
|
|
288
|
+
For named predicates (whether native or adapter-based), simply return the predicate
|
|
289
|
+
name from the block:
|
|
290
|
+
|
|
291
|
+
```ruby rspec predication_satisfy_declared
|
|
292
|
+
result = Validator
|
|
293
|
+
.instance({ foo: -1 }, coordinator)
|
|
294
|
+
.satisfy(:foo, severity: :refute) { :presence }
|
|
295
|
+
.satisfy(:foo, severity: :dispute) { :positive_number }
|
|
296
|
+
.to_result
|
|
297
|
+
|
|
298
|
+
expect(result.failure)
|
|
299
|
+
.to match({ children: { foo: { errors: [have_attributes(code: :'failed: number? AND gt?(0)')] } } })
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Context-dependent predicates:** For predicates that need context data, use the builder pattern:
|
|
303
|
+
|
|
304
|
+
```ruby rspec predication_satisfy_contextual
|
|
305
|
+
result = Validator
|
|
306
|
+
.instance({ foo: 101 }, coordinator, context: { max: 100 })
|
|
307
|
+
.satisfy(:foo, using: :dry, severity: :dispute) do |builder, context|
|
|
308
|
+
builder.call { lteq?(context[:max]) }
|
|
309
|
+
end.to_result
|
|
310
|
+
|
|
311
|
+
expect(result.failure)
|
|
312
|
+
.to match(children: { foo: { errors: [have_attributes(code: :'failed: lteq?(100)')]}})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Severity control:** The `severity` parameter determines whether predicate failures become
|
|
316
|
+
disputes or refutations, giving you control over validation flow.
|
|
317
|
+
|
|
318
|
+
**Missing values:** Like `validate?`, the `satisfy?` method handles missing values
|
|
319
|
+
gracefully — skipping validation by default, or using `some_or_nil`/`option` strategies
|
|
320
|
+
when chained.
|
|
321
|
+
|
|
322
|
+
## Navigation
|
|
323
|
+
Navigate through data structures using `at` and `each_at` methods.
|
|
324
|
+
These methods look up values and create new validator nodes for deeper validation.
|
|
325
|
+
|
|
326
|
+
Like other validation methods, navigation supports the `from` parameter
|
|
327
|
+
to separate data location from validation location.
|
|
328
|
+
|
|
329
|
+
### Validating nested structures
|
|
330
|
+
Use `at` to navigate complex nested values and validate their internal structure.
|
|
331
|
+
If a node requires substantial processing, consider extracting the logic into
|
|
332
|
+
a separate function for clarity and reuse:
|
|
333
|
+
|
|
334
|
+
```ruby rspec navigation_nested_node
|
|
335
|
+
def self.foo(foo)
|
|
336
|
+
foo.validate(:bar) { |bar, _ctx| Dispute(:excessive) if bar > 10 }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
result = Validator
|
|
340
|
+
.instance({ foo: { bar: 11 } }, coordinator).at(:foo) { |foo| foo(foo) }
|
|
341
|
+
.to_result
|
|
342
|
+
|
|
343
|
+
expect(result.failure)
|
|
344
|
+
.to match({ children: { foo: { children: { bar: { errors: [have_attributes(code: :excessive)] } } } } })
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The `at` method passes a new validator node (positioned at the nested location)
|
|
348
|
+
to your block. This lets you apply the full range of validation tools
|
|
349
|
+
to nested data structures.
|
|
350
|
+
|
|
351
|
+
**Performance consideration:** Creating new validator nodes has overhead.
|
|
352
|
+
Use `at` when you need to validate multiple aspects of a nested structure,
|
|
353
|
+
but consider direct path validation (`validate(:foo, :bar)`) for simple cases.
|
|
354
|
+
|
|
355
|
+
### Validating collections
|
|
356
|
+
For arrays of complex objects, use `each_at` to validate each element:
|
|
357
|
+
|
|
358
|
+
```ruby rspec navigation_nested_node_each
|
|
359
|
+
result = Validator
|
|
360
|
+
.instance({ foos: [{ bar: 10 }, { bar: 11 }] }, coordinator)
|
|
361
|
+
.each_at(:foos) { |foo| foo.validate(:bar) { |bar, _ctx| Dispute(:excessive) if bar > 10 } }
|
|
362
|
+
.to_result
|
|
363
|
+
|
|
364
|
+
expected_errors = {
|
|
365
|
+
children: {
|
|
366
|
+
foos: {
|
|
367
|
+
children: {
|
|
368
|
+
1 => {
|
|
369
|
+
children: {
|
|
370
|
+
bar: { errors: [have_attributes(code: :excessive)] }
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
expect(result.failure).to match(expected_errors)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The `each_at` method creates a new validator node for each collection element,
|
|
382
|
+
enabling full validation of nested structures. Note how errors are indexed
|
|
383
|
+
by position (the second element gets index `1`).
|
|
384
|
+
|
|
385
|
+
**Performance optimization for scalars:**
|
|
386
|
+
When validating arrays of simple values, avoid the node creation overhead by chaining `validate`
|
|
387
|
+
directly after `each_at`:
|
|
388
|
+
|
|
389
|
+
```ruby rspec navigation_nested_node_each_validate
|
|
390
|
+
result = Validator.instance({ foos: [10, 11] }, coordinator)
|
|
391
|
+
.each_at(:foos)
|
|
392
|
+
.validate { |foo, _ctx| Dispute(:excessive) if foo > 10 }
|
|
393
|
+
.to_result
|
|
394
|
+
|
|
395
|
+
expected_errors = {
|
|
396
|
+
children: {
|
|
397
|
+
foos: {
|
|
398
|
+
children: {
|
|
399
|
+
1 => {
|
|
400
|
+
errors: [have_attributes(code: :excessive)]
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
expect(result.failure).to match(expected_errors)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
This pattern skips node creation and validates each scalar value directly, significantly
|
|
411
|
+
improving performance for large collections of simple values.
|
|
412
|
+
|
|
413
|
+
**Using predicates with collections:**
|
|
414
|
+
You can also chain `satisfy` after `each_at` for declarative validation:
|
|
415
|
+
```ruby rspec navigation_nested_node_each_satisfy
|
|
416
|
+
result = Validator
|
|
417
|
+
.instance({ foos: [10, 11] }, coordinator, context: { max: 10 })
|
|
418
|
+
.each_at(:foos).satisfy(using: :dry, severity: :dispute) do |builder, context|
|
|
419
|
+
builder.call { lteq?(context[:max]) }
|
|
420
|
+
end.to_result
|
|
421
|
+
|
|
422
|
+
expected_errors = {
|
|
423
|
+
children: {
|
|
424
|
+
foos: {
|
|
425
|
+
children: {
|
|
426
|
+
1 => {
|
|
427
|
+
errors: [have_attributes(code: :'failed: lteq?(10)')]
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
expect(result.failure).to match(expected_errors)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Important limitation:** Context-dependent predicates with `satisfy` are built only once before
|
|
438
|
+
iteration begins. If your predication logic is based on per-element context, use `validate` instead.
|
|
439
|
+
|
|
440
|
+
**Missing value handling:** Like other validation methods, `at` and `each_at` have `?`
|
|
441
|
+
variants (`at?`, `each_at?`) that handle missing values gracefully. Note that
|
|
442
|
+
`some_or_nil` and `option` strategies don't apply to `each_at?` since they don't
|
|
443
|
+
make sense for collection elements.
|
|
444
|
+
|
|
445
|
+
**Supported collections:** Currently `each_at` works with `Array` and `Hash`. You can add support
|
|
446
|
+
for other collection types (like `Set` or `ActiveRecord::Relation`) using [custom wrappers](#custom-wrappers).
|
|
447
|
+
|
|
448
|
+
## Flow control
|
|
449
|
+
Basic flow control comes from the `Dispute`/`Refute` distinction—`Refute` rulings skip
|
|
450
|
+
all subsequent validations on that node.
|
|
451
|
+
|
|
452
|
+
For more sophisticated control, use `with_valid` to conditionally execute validation
|
|
453
|
+
logic based on node state.
|
|
454
|
+
|
|
455
|
+
### Conditional validation
|
|
456
|
+
Execute validation only when the current node is valid (neither disputed nor refuted):
|
|
457
|
+
|
|
458
|
+
```ruby rspec scoping_with_valid_node
|
|
459
|
+
expect do |yield_probe|
|
|
460
|
+
Validator.instance({ foo: 'FOO', bar: 'BAR' }, coordinator).with_valid do |valid|
|
|
461
|
+
yield_probe.to_proc.call
|
|
462
|
+
valid
|
|
463
|
+
end
|
|
464
|
+
end.to yield_control
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Multi-clause conditions
|
|
468
|
+
Validate nodes together only when all dependencies are valid:
|
|
469
|
+
|
|
470
|
+
```ruby rspec scoping_with_valid_children
|
|
471
|
+
expect do |yield_probe|
|
|
472
|
+
Validator.instance({ foo: 'FOO', bar: 'BAR' }, coordinator)
|
|
473
|
+
.dispute(:invalid, at: [:foo])
|
|
474
|
+
.with_valid(:foo).and(:bar, &yield_probe)
|
|
475
|
+
end.not_to yield_control
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
This example validates `foo` and `bar` as a tuple, but only if both nodes are individually valid.
|
|
479
|
+
Since `foo` is disputed, the validation block never executes.
|
|
480
|
+
|
|
481
|
+
The `with_valid` method enables complex validation dependencies while maintaining clean,
|
|
482
|
+
readable validation logic.
|
|
483
|
+
|
|
484
|
+
## Critical section
|
|
485
|
+
Sometimes child node failures are so significant they should fail
|
|
486
|
+
the entire parent validation. The `critical` block propagates any `Refute`
|
|
487
|
+
ruling from within the block up to the parent node.
|
|
488
|
+
|
|
489
|
+
The `critical` method requires an error transformer lambda to adapt
|
|
490
|
+
child errors for the parent context. Without transformation, propagated
|
|
491
|
+
errors often don't make sense at the parent level.
|
|
492
|
+
|
|
493
|
+
### Error propagation
|
|
494
|
+
Here's a critical section with minimal transformation (just passing the error through):
|
|
495
|
+
|
|
496
|
+
```ruby rspec scoping_critical_refute_nested
|
|
497
|
+
result = Validator.instance({ user: { age: 'eleven' } }, coordinator).at(:user) do |user|
|
|
498
|
+
user.critical(->(error, _path) { error }) do |critical|
|
|
499
|
+
critical.validate(:age) do |age|
|
|
500
|
+
Refute(:not_integer) unless age.is_a?(Integer)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end.to_result
|
|
504
|
+
|
|
505
|
+
expect(result.failure)
|
|
506
|
+
.to match({ children: { user: { errors: [have_attributes(code: :not_integer)] } } })
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
The error "user is not_integer" is confusing because the problem is actually with the age field.
|
|
510
|
+
|
|
511
|
+
Use the transformer to create meaningful parent-level error messages:
|
|
512
|
+
|
|
513
|
+
```ruby rspec scoping_critical_rewrap_error
|
|
514
|
+
REWRAP_CRITICAL = lambda { |error, path|
|
|
515
|
+
StructuredError::Record.instance(
|
|
516
|
+
:invalid,
|
|
517
|
+
message: "#{error.code} at #{path.join('.')}",
|
|
518
|
+
data: { original_error: error, path: path }
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
result = Validator.instance({ user: { age: 'eleven' } }, coordinator).at(:user) do |user|
|
|
523
|
+
user.critical(REWRAP_CRITICAL) do |critical|
|
|
524
|
+
critical.validate(:age) do |age|
|
|
525
|
+
Refute(:not_integer) unless age.is_a?(Integer)
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end.to_result
|
|
529
|
+
|
|
530
|
+
expect(result.failure)
|
|
531
|
+
.to match({ children: { user: { errors: [have_attributes(code: :invalid, message: 'not_integer at age')] } } })
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
The transformer receives the original error and the path from the critical section start
|
|
535
|
+
to the failure point, enabling contextual error messages that make sense at the parent level.
|
|
536
|
+
|
|
537
|
+
## Transforming the validated object
|
|
538
|
+
The `Commit` ruling enables validation with simultaneous data transformation,
|
|
539
|
+
letting you reshape data while validating it.
|
|
540
|
+
|
|
541
|
+
### Ways to commit values
|
|
542
|
+
You can commit values through several mechanisms:
|
|
543
|
+
- Return `Commit(value)` from a `validate` block
|
|
544
|
+
- Call the `commit(value)` method on a validator node
|
|
545
|
+
- Pass `commit: true` to the `validate` or `satisfy` method (commits the original value if validation passes)
|
|
546
|
+
- Pass `commit: <collection_type>` to the `each_at` - gathers values of all committed nodes
|
|
547
|
+
into the specified collection — either `array` or `hash` and commits them to the node
|
|
548
|
+
after the iteration.
|
|
549
|
+
|
|
550
|
+
Individual value commits aren't enough — you must also commit the containing structure.
|
|
551
|
+
The validator can't automatically determine the desired output format,
|
|
552
|
+
so you need to explicitly commit each level.
|
|
553
|
+
|
|
554
|
+
Use `auto_commit(as: :hash)` to gather committed child values into a new container:
|
|
555
|
+
|
|
556
|
+
```ruby rspec ruling_commit_complex
|
|
557
|
+
def self.item(item)
|
|
558
|
+
item
|
|
559
|
+
.satisfy(:name, commit: true) { :presence }
|
|
560
|
+
.satisfy(:unit_price, from: [:price], commit: true ) { :presence }
|
|
561
|
+
.auto_commit(as: :hash)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
original_data = {
|
|
565
|
+
customer: { name: 'John Doe' },
|
|
566
|
+
items: [{ price: 100, name: 'Item 1' }],
|
|
567
|
+
price: 100
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
result = Validator
|
|
571
|
+
.instance(original_data, coordinator)
|
|
572
|
+
.satisfy(:customer_name, from: [:customer, :name], commit: true) { :presence }
|
|
573
|
+
.validate(:total, from: [:price]) { |price, _ctx| price <= 100 ? Commit(price) : Refute(:excessive) }
|
|
574
|
+
.each_at(:line_items, from: [:items], commit: :array) { |item| item(item) }
|
|
575
|
+
.auto_commit(as: :hash)
|
|
576
|
+
.to_result
|
|
577
|
+
|
|
578
|
+
transformed_data = {
|
|
579
|
+
customer_name: 'John Doe',
|
|
580
|
+
line_items: [{ name: 'Item 1', unit_price: 100 }],
|
|
581
|
+
total: 100
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
expect(result.success).to eq(transformed_data)
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
This example demonstrates the full transformation pipeline:
|
|
588
|
+
1. Extract and validate data from nested sources (`customer.name`)
|
|
589
|
+
2. Commit individual values under new keys (`customer_name`, `total`, `line_items`, `unit_price`)
|
|
590
|
+
3. Build the final transformed structure with `auto_commit`
|
|
591
|
+
|
|
592
|
+
The result is a validated and transformed structure entirely different
|
|
593
|
+
from the original data.
|
|
594
|
+
|
|
595
|
+
## Implementing custom wrappers
|
|
596
|
+
The validator supports `Hash` and `Array` out of the box,
|
|
597
|
+
but you can extend it to work with specialized collection types
|
|
598
|
+
like `ActiveRecord::Relation`.
|
|
599
|
+
|
|
600
|
+
### Registering compatible classes
|
|
601
|
+
If your class implements the same interface as an existing wrapper
|
|
602
|
+
but doesn't inherit from the expected base class, register it with an existing wrapper:
|
|
603
|
+
|
|
604
|
+
```ruby rspec implement_custom_wrapper
|
|
605
|
+
class NotArray
|
|
606
|
+
extend Forwardable
|
|
607
|
+
|
|
608
|
+
def initialize(array)
|
|
609
|
+
@array = array
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def_delegator :array, :length
|
|
613
|
+
def_delegator :array, :[]
|
|
614
|
+
def_delegator :array, :lazy
|
|
615
|
+
|
|
616
|
+
private
|
|
617
|
+
|
|
618
|
+
attr_reader :array
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
Complex::Registry.register(NotArray, Complex::Wrappers::Array)
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
This tells the validator to treat `NotArray` instances like arrays for navigation and iteration.
|
|
625
|
+
|
|
626
|
+
### Creating new wrappers
|
|
627
|
+
For containers that don't match existing patterns, create a custom wrapper by inheriting from:
|
|
628
|
+
- **`Wrappers::Abstract::NonIterable`** - For containers that support key-based access but not iteration
|
|
629
|
+
- **`Wrappers::Abstract::Iterable`** - For containers that support both access and iteration (enabling `each_at`)
|
|
630
|
+
|
|
631
|
+
**Required methods:**
|
|
632
|
+
|
|
633
|
+
For any wrapper:
|
|
634
|
+
- `fetch(key)` - returns `Option.some(value)` if the key exists, `Option.none` otherwise
|
|
635
|
+
|
|
636
|
+
For iterable wrappers, also implement:
|
|
637
|
+
- `reduce(initial_state, &block)` - yields `(accumulator, [value, key])` for each element
|
|
638
|
+
|
|
639
|
+
The abstract base classes handle all other functionality.
|
|
640
|
+
Register your custom wrapper the same way as shown above.
|
|
641
|
+
|
|
642
|
+
This extension system lets the validator work with any collection type
|
|
643
|
+
while maintaining consistent navigation and validation APIs.
|
|
644
|
+
|
|
645
|
+
## Configuration
|
|
646
|
+
The library's extensive configurability enables smooth integration
|
|
647
|
+
with existing systems, but requires upfront setup to become operational.
|
|
648
|
+
Two core areas need configuration:
|
|
649
|
+
|
|
650
|
+
1. **Error handling** - How validation errors are created, structured,
|
|
651
|
+
and presented to your application
|
|
652
|
+
2. **Interface types** - What result and option types the library uses
|
|
653
|
+
to communicate with your code
|
|
654
|
+
|
|
655
|
+
This flexibility lets you adapt the library to work with your existing error
|
|
656
|
+
handling patterns and result types, whether you're using a proprietary solution,
|
|
657
|
+
`Dry::Monads`, or some more exotic library.
|
|
658
|
+
|
|
659
|
+
### Validation errors
|
|
660
|
+
Validation errors must include the `StructuredError` marker module. This module
|
|
661
|
+
defines abstract methods as suggestions rather than requirements — the library
|
|
662
|
+
works with any type that includes the module.
|
|
663
|
+
|
|
664
|
+
For simple cases, use the built-in `StructuredError::Record` class, which accepts:
|
|
665
|
+
- `code` (required `Symbol`) - The error identifier
|
|
666
|
+
- `message` (optional `String`) - Human-readable description
|
|
667
|
+
- `data` (optional, any type) - Additional error context
|
|
668
|
+
|
|
669
|
+
### Error factory methods
|
|
670
|
+
The configuration needs to provide factories to create structured errors
|
|
671
|
+
from these three parameters:
|
|
672
|
+
|
|
673
|
+
**`structured_error(code, message: nil, data: nil)`**
|
|
674
|
+
Creates validation errors when your code explicitly disputes or refutes nodes.
|
|
675
|
+
|
|
676
|
+
**`internal_error(id, message: nil, data: nil)`**
|
|
677
|
+
Translates internal framework errors into structured errors. Current internal error codes:
|
|
678
|
+
|
|
679
|
+
- `:execution_error` - Exception caught when calling foreign code
|
|
680
|
+
- `:invalid_access` - Object accessor method raised an exception
|
|
681
|
+
- `:not_iterable` - Attempted iteration on unsupported collection type
|
|
682
|
+
- `:value_missing` - Requested value not found in data structure
|
|
683
|
+
|
|
684
|
+
If you don't need to transform internal errors into something more meaningful
|
|
685
|
+
in your system, this method can simply delegate to `structured_error`.
|
|
686
|
+
|
|
687
|
+
### Error building strategies
|
|
688
|
+
The coordinator's `build_final_error` method determines how the validation
|
|
689
|
+
tree gets transformed into the final error structure returned by `to_result`.
|
|
690
|
+
|
|
691
|
+
The validator maintains errors as a tree where each node holds its own errors
|
|
692
|
+
plus references to invalid child nodes. The coordinator's `build_final_error`
|
|
693
|
+
method determines how the tree gets transformed into the final error
|
|
694
|
+
structure returned by `to_result`. Different applications need different final formats.
|
|
695
|
+
|
|
696
|
+
**Hierarchical Strategy** (`Coordinator::Errors::Hierarchical`)
|
|
697
|
+
Preserves the tree structure as nested hashes — most natural for debugging:
|
|
698
|
+
|
|
699
|
+
```ruby rspec with_hierarchical_adapter
|
|
700
|
+
expected_failure = {
|
|
701
|
+
errors: [root_error],
|
|
702
|
+
children: {
|
|
703
|
+
foo: {
|
|
704
|
+
children: {
|
|
705
|
+
bar: { errors: [bar_error] }
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
expect(result.to_result.failure).to eq(expected_failure)
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Flat Strategy** (`Coordinator::Errors::Flat`)
|
|
714
|
+
Flattens errors into path-value tuples — useful for processing or storage:
|
|
715
|
+
|
|
716
|
+
```ruby rspec with_flat_adapter
|
|
717
|
+
expected_failure = [
|
|
718
|
+
['', [root_error]],
|
|
719
|
+
['foo.bar', [bar_error]]
|
|
720
|
+
]
|
|
721
|
+
expect(result.to_result.failure).to eq(expected_failure)
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Dry Strategy** (`Coordinator::Errors::Dry`)
|
|
725
|
+
Mimics `Dry::Validation` error format for compatibility:
|
|
726
|
+
|
|
727
|
+
```ruby rspec with_dry_adapter
|
|
728
|
+
expected_failure = [
|
|
729
|
+
[root_error],
|
|
730
|
+
foo: {
|
|
731
|
+
bar: [bar_error]
|
|
732
|
+
}
|
|
733
|
+
]
|
|
734
|
+
expect(result.to_result.failure).to eq(expected_failure)
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
Choose the strategy that best fits your application's error handling patterns,
|
|
738
|
+
or implement custom strategies for specialized formats.
|
|
739
|
+
|
|
740
|
+
### Interfaces
|
|
741
|
+
The library communicates with your application through two key types:
|
|
742
|
+
|
|
743
|
+
- **Result** - Wraps the final validation outcome (success or failure)
|
|
744
|
+
- **Option** - Represents values that may or may not be present
|
|
745
|
+
(used with `validate?` and missing value strategies)
|
|
746
|
+
|
|
747
|
+
Both types are configurable to match your existing codebase's patterns.
|
|
748
|
+
|
|
749
|
+
**Default Interface**
|
|
750
|
+
The library includes basic implementations at `Lite::Validation::Validator::Adapters::Interfaces::Default`.
|
|
751
|
+
These are primarily intended for internal use but can be configured as external interfaces too.
|
|
752
|
+
They may provide a good enough solution when you want to avoid dependencies
|
|
753
|
+
but lack monadic functionality and may feel awkward compared to more advanced
|
|
754
|
+
alternatives.
|
|
755
|
+
|
|
756
|
+
**Dry::Monads Integration**
|
|
757
|
+
The recommended approach uses `Dry::Monads`:
|
|
758
|
+
- **Result**: Uses `Dry::Result` for success/failure outcomes
|
|
759
|
+
- **Option**: Uses `Dry::Result::Failure(Unit)` to represent missing values
|
|
760
|
+
(rather than `Dry::Maybe`, since `Maybe::Some` cannot hold `nil` values)
|
|
761
|
+
|
|
762
|
+
**Custom Interfaces**
|
|
763
|
+
Build custom interface adapters to integrate with your preferred flow control libraries.
|
|
764
|
+
This lets the validation library work seamlessly within your existing error
|
|
765
|
+
handling and optional value patterns. The interface configuration ensures the library adapts
|
|
766
|
+
to your codebase rather than forcing architectural decisions on your application.
|
|
767
|
+
|
|
768
|
+
# License
|
|
769
|
+
This library is published under MIT license
|
|
770
|
+
|