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.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +94 -0
  3. data/Gemfile +18 -0
  4. data/README.md +770 -0
  5. data/bench/calibrational.rb +89 -0
  6. data/bench/comparative.rb +197 -0
  7. data/bench/functional.rb +107 -0
  8. data/bench/profile.rb +45 -0
  9. data/lib/lite/validation/adapters/interfaces/dry.rb +29 -0
  10. data/lib/lite/validation/error.rb +9 -0
  11. data/lib/lite/validation/result/abstract/disputable.rb +17 -0
  12. data/lib/lite/validation/result/abstract/failure.rb +19 -0
  13. data/lib/lite/validation/result/abstract/refutable.rb +21 -0
  14. data/lib/lite/validation/result/abstract/success.rb +25 -0
  15. data/lib/lite/validation/result/abstract.rb +16 -0
  16. data/lib/lite/validation/structured_error/record.rb +36 -0
  17. data/lib/lite/validation/structured_error.rb +23 -0
  18. data/lib/lite/validation/validator/adapters/errors/default.rb +24 -0
  19. data/lib/lite/validation/validator/adapters/interfaces/default.rb +32 -0
  20. data/lib/lite/validation/validator/adapters/interfaces/dry.rb +17 -0
  21. data/lib/lite/validation/validator/adapters/predicates/dry/adapter.rb +50 -0
  22. data/lib/lite/validation/validator/adapters/predicates/dry/builder.rb +46 -0
  23. data/lib/lite/validation/validator/adapters/predicates/dry/engine.rb +32 -0
  24. data/lib/lite/validation/validator/adapters/predicates/dry.rb +3 -0
  25. data/lib/lite/validation/validator/coordinator/builder.rb +92 -0
  26. data/lib/lite/validation/validator/coordinator/default.rb +32 -0
  27. data/lib/lite/validation/validator/coordinator/errors/builder.rb +56 -0
  28. data/lib/lite/validation/validator/coordinator/errors/dry.rb +29 -0
  29. data/lib/lite/validation/validator/coordinator/errors/flat.rb +46 -0
  30. data/lib/lite/validation/validator/coordinator/errors/hierarchical.rb +29 -0
  31. data/lib/lite/validation/validator/coordinator/instance.rb +30 -0
  32. data/lib/lite/validation/validator/coordinator.rb +12 -0
  33. data/lib/lite/validation/validator/helpers/path.rb +49 -0
  34. data/lib/lite/validation/validator/node/abstract/branch.rb +21 -0
  35. data/lib/lite/validation/validator/node/abstract/instance.rb +43 -0
  36. data/lib/lite/validation/validator/node/abstract/leaf.rb +35 -0
  37. data/lib/lite/validation/validator/node/abstract.rb +25 -0
  38. data/lib/lite/validation/validator/node/child.rb +44 -0
  39. data/lib/lite/validation/validator/node/implementation/apply_ruling.rb +44 -0
  40. data/lib/lite/validation/validator/node/implementation/dig.rb +38 -0
  41. data/lib/lite/validation/validator/node/implementation/helpers/call_foreign.rb +31 -0
  42. data/lib/lite/validation/validator/node/implementation/helpers/with_result.rb +23 -0
  43. data/lib/lite/validation/validator/node/implementation/helpers/yield_strategy.rb +83 -0
  44. data/lib/lite/validation/validator/node/implementation/helpers/yield_validator.rb +49 -0
  45. data/lib/lite/validation/validator/node/implementation/identity.rb +90 -0
  46. data/lib/lite/validation/validator/node/implementation/iteration/iterator.rb +102 -0
  47. data/lib/lite/validation/validator/node/implementation/iteration.rb +46 -0
  48. data/lib/lite/validation/validator/node/implementation/navigation.rb +43 -0
  49. data/lib/lite/validation/validator/node/implementation/predication.rb +61 -0
  50. data/lib/lite/validation/validator/node/implementation/scoping/evaluator.rb +43 -0
  51. data/lib/lite/validation/validator/node/implementation/scoping.rb +43 -0
  52. data/lib/lite/validation/validator/node/implementation/validation.rb +64 -0
  53. data/lib/lite/validation/validator/node/implementation/wrap.rb +26 -0
  54. data/lib/lite/validation/validator/node/root.rb +60 -0
  55. data/lib/lite/validation/validator/node/suspended.rb +33 -0
  56. data/lib/lite/validation/validator/node.rb +12 -0
  57. data/lib/lite/validation/validator/option/none.rb +43 -0
  58. data/lib/lite/validation/validator/option/some/abstract.rb +29 -0
  59. data/lib/lite/validation/validator/option/some/complex/registry/abstract.rb +67 -0
  60. data/lib/lite/validation/validator/option/some/complex/registry/node.rb +47 -0
  61. data/lib/lite/validation/validator/option/some/complex/registry/root.rb +31 -0
  62. data/lib/lite/validation/validator/option/some/complex/registry.rb +32 -0
  63. data/lib/lite/validation/validator/option/some/complex/wrappers/abstract/iterable.rb +31 -0
  64. data/lib/lite/validation/validator/option/some/complex/wrappers/abstract/non_iterable.rb +27 -0
  65. data/lib/lite/validation/validator/option/some/complex/wrappers/abstract.rb +35 -0
  66. data/lib/lite/validation/validator/option/some/complex/wrappers/array.rb +41 -0
  67. data/lib/lite/validation/validator/option/some/complex/wrappers/hash.rb +40 -0
  68. data/lib/lite/validation/validator/option/some/complex/wrappers/object.rb +34 -0
  69. data/lib/lite/validation/validator/option/some/complex/wrappers/tuple.rb +47 -0
  70. data/lib/lite/validation/validator/option/some/complex/wrappers.rb +5 -0
  71. data/lib/lite/validation/validator/option/some/complex.rb +24 -0
  72. data/lib/lite/validation/validator/option/some/dig.rb +34 -0
  73. data/lib/lite/validation/validator/option/some/simple.rb +23 -0
  74. data/lib/lite/validation/validator/option/some/singular.rb +29 -0
  75. data/lib/lite/validation/validator/option/some.rb +20 -0
  76. data/lib/lite/validation/validator/option.rb +20 -0
  77. data/lib/lite/validation/validator/predicate/abstract/variants.rb +23 -0
  78. data/lib/lite/validation/validator/predicate/foreign/adapter/input/single.rb +21 -0
  79. data/lib/lite/validation/validator/predicate/foreign/adapter/input/tuple.rb +21 -0
  80. data/lib/lite/validation/validator/predicate/foreign/adapter/input.rb +28 -0
  81. data/lib/lite/validation/validator/predicate/foreign/adapter/ruling/instance.rb +37 -0
  82. data/lib/lite/validation/validator/predicate/foreign/adapter/ruling.rb +26 -0
  83. data/lib/lite/validation/validator/predicate/foreign/engine.rb +27 -0
  84. data/lib/lite/validation/validator/predicate/foreign/variant.rb +33 -0
  85. data/lib/lite/validation/validator/predicate/foreign/variants.rb +46 -0
  86. data/lib/lite/validation/validator/predicate/native/builder.rb +72 -0
  87. data/lib/lite/validation/validator/predicate/native/definite.rb +19 -0
  88. data/lib/lite/validation/validator/predicate/native/instance.rb +41 -0
  89. data/lib/lite/validation/validator/predicate/native/optional.rb +34 -0
  90. data/lib/lite/validation/validator/predicate/registry.rb +47 -0
  91. data/lib/lite/validation/validator/predicate.rb +17 -0
  92. data/lib/lite/validation/validator/result/abstract/failure.rb +21 -0
  93. data/lib/lite/validation/validator/result/abstract/instance.rb +18 -0
  94. data/lib/lite/validation/validator/result/abstract/success.rb +17 -0
  95. data/lib/lite/validation/validator/result/abstract.rb +29 -0
  96. data/lib/lite/validation/validator/result/committed.rb +75 -0
  97. data/lib/lite/validation/validator/result/disputable/hash.rb +17 -0
  98. data/lib/lite/validation/validator/result/disputable/instance.rb +43 -0
  99. data/lib/lite/validation/validator/result/disputable/iterable/array.rb +23 -0
  100. data/lib/lite/validation/validator/result/disputable/iterable.rb +17 -0
  101. data/lib/lite/validation/validator/result/disputable/navigable.rb +35 -0
  102. data/lib/lite/validation/validator/result/disputable.rb +14 -0
  103. data/lib/lite/validation/validator/result/disputed/abstract/hash.rb +32 -0
  104. data/lib/lite/validation/validator/result/disputed/abstract/instance.rb +26 -0
  105. data/lib/lite/validation/validator/result/disputed/iterable/array.rb +42 -0
  106. data/lib/lite/validation/validator/result/disputed/iterable/hash.rb +38 -0
  107. data/lib/lite/validation/validator/result/disputed/iterable.rb +20 -0
  108. data/lib/lite/validation/validator/result/disputed/navigable.rb +59 -0
  109. data/lib/lite/validation/validator/result/disputed.rb +17 -0
  110. data/lib/lite/validation/validator/result/refuted.rb +78 -0
  111. data/lib/lite/validation/validator/result/valid/abstract/collect.rb +42 -0
  112. data/lib/lite/validation/validator/result/valid/abstract/commit.rb +25 -0
  113. data/lib/lite/validation/validator/result/valid/abstract/instance.rb +23 -0
  114. data/lib/lite/validation/validator/result/valid/iterable/array/abstract.rb +24 -0
  115. data/lib/lite/validation/validator/result/valid/iterable/array/tuples.rb +64 -0
  116. data/lib/lite/validation/validator/result/valid/iterable/array/values.rb +42 -0
  117. data/lib/lite/validation/validator/result/valid/iterable/hash.rb +46 -0
  118. data/lib/lite/validation/validator/result/valid/iterable.rb +33 -0
  119. data/lib/lite/validation/validator/result/valid/navigable.rb +68 -0
  120. data/lib/lite/validation/validator/result/valid.rb +21 -0
  121. data/lib/lite/validation/validator/result.rb +15 -0
  122. data/lib/lite/validation/validator/ruling/abstract/invalid.rb +59 -0
  123. data/lib/lite/validation/validator/ruling/abstract/valid.rb +23 -0
  124. data/lib/lite/validation/validator/ruling/abstract.rb +12 -0
  125. data/lib/lite/validation/validator/ruling/commit.rb +17 -0
  126. data/lib/lite/validation/validator/ruling/dispute.rb +21 -0
  127. data/lib/lite/validation/validator/ruling/invalidate.rb +32 -0
  128. data/lib/lite/validation/validator/ruling/pass.rb +19 -0
  129. data/lib/lite/validation/validator/ruling/refute.rb +21 -0
  130. data/lib/lite/validation/validator/ruling.rb +53 -0
  131. data/lib/lite/validation/validator/state/instance.rb +46 -0
  132. data/lib/lite/validation/validator/state/merge_strategy.rb +50 -0
  133. data/lib/lite/validation/validator/state/unwrap_strategy.rb +31 -0
  134. data/lib/lite/validation/validator/state.rb +21 -0
  135. data/lib/lite/validation/validator.rb +15 -0
  136. data/lib/lite/validation/version.rb +9 -0
  137. data/lib/lite/validation.rb +8 -0
  138. 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
+