lite-validation 0.0.1 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c5842168d5b978d852741d1d8b644af018155532b0fd82ff1b001f8e385a953
4
- data.tar.gz: cab9dc80df653dce79ebbe0c78785bea1e2a7faa13f58f9025a040f5a04b5b69
3
+ metadata.gz: 72bde3b3c35381247baebe84b4b4d4394bf33769c4b5187e4d7a1124ca64464a
4
+ data.tar.gz: 4e97554d20e8bb5986a2d79b35483ad2b22ffa4c52ec874902108d1d37c77d0b
5
5
  SHA512:
6
- metadata.gz: af912dd40521360adfe2da60cb920700f7d1d898ab9efd6062c1a822da6e33ee96ff0211481d00d3513448fbf781bb92d990e28c11d16ff8dc10b09183f318f3
7
- data.tar.gz: 387a65a01ea8db1ea2786edb153ea492a52740a3bfe6fac882d2ffa4c777b197a47862b18ebc9c1e2c5d3e01d9200ed39f3ea7c52aa0da5a317bd400ceaf26ce
6
+ metadata.gz: 5f80e113ea3e07a76dd9b23a61c5e85a6302054f00c105d8efabbab4acf2077dbdd622f61988d78748374fa8d31d24a0ebdd158092b9dde824f4af359d634ee0
7
+ data.tar.gz: 332a9722829258a3631d84b12b432599b5d1722884273f81c33c40d8fb146d69e95adfabf10d468bfd29e53b3adda211270bfad49e29d4cc2886f08e34f2beea
data/.rubocop.yml CHANGED
@@ -90,5 +90,8 @@ Style/MethodCallWithoutArgsParentheses:
90
90
  Style/SingleArgumentDig:
91
91
  Enabled: false
92
92
 
93
+ Style/SymbolProc:
94
+ Enabled: false
95
+
93
96
  Style/TrailingUnderscoreVariable:
94
97
  Enabled: false
data/README.md CHANGED
@@ -5,20 +5,20 @@ transform input into new shape through an immutable, composable interface
5
5
  that treats validation as a general-purpose computational tool.
6
6
 
7
7
  - Extensible wrapper system supports custom collections (like `ActiveRecord::Relation`)
8
- - Pluggable predicate engines ships with `Dry::Logic` adapter for declarative validation
8
+ - Pluggable predicate engines ships with `Dry::Logic` adapter for declarative validation
9
9
  - Configurable result types and error formats
10
10
  - Transform data while validating through integrated commit/transformation mechanics
11
11
 
12
12
  Engineered for consistent performance regardless of validation outcome.
13
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.
14
+ as filtering, decision-making, or data processing logic not just input sanitization.
15
15
  Whether validating inputs that mostly pass or mostly fail, performance remains predictable.
16
16
  Perfect for applications that need validation throughout the system:
17
17
  API endpoints, background jobs, data pipelines, and anywhere you need reliable
18
18
  validation with transformation capabilities.
19
19
 
20
20
  ## Getting started
21
- Before validating data, you'll need to create a **coordinator**
21
+ Before validating data, you'll need to create a **coordinator**
22
22
  a configuration object that defines how the validator integrates
23
23
  with your application. The coordinator specifies what types
24
24
  to use for results, options, and errors, making the library adaptable
@@ -79,18 +79,18 @@ expect(result.failure).to match({ errors: [have_attributes(code: :excessive)] })
79
79
 
80
80
  The core of validation is the `validate` method and its counterpart `validate?`.
81
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.
82
+ expecting a ruling in return a decision about the value's validity.
83
83
  There are four types of ruling available in validate blocks:
84
- - `Pass()` Indicates the value is valid. Rarely used since returning
84
+ - `Pass()` Indicates the value is valid. Rarely used since returning
85
85
  `nil` has the same effect.
86
- - `Dispute(code, message: nil, data: nil)` Marks the value as invalid but allows validation
86
+ - `Dispute(code, message: nil, data: nil)` Marks the value as invalid but allows validation
87
87
  to continue on this node. All ancestor nodes also become disputed. You can also pass
88
88
  a structured error object: `Dispute(structured_error)`
89
- - `Refute(code, message: nil, data: nil)` Marks the value as invalid with a fatal error
89
+ - `Refute(code, message: nil, data: nil)` Marks the value as invalid with a fatal error
90
90
  that stops further validation on this node. Parent nodes become disputed unless this
91
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).
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
94
  Commited node can't be reopened for validation again, such attempt will trigger runtime error.
95
95
 
96
96
  The distinction between `Dispute` and `Refute` gives you some control over validation flow:
@@ -99,7 +99,7 @@ enough to halt processing.
99
99
 
100
100
  ### Validating structured data
101
101
  The library's capabilities become more apparent with hierarchical data.
102
- Pass a path as the first argument to `validate` validator
102
+ Pass a path as the first argument to `validate` validator
103
103
  will navigate to that value and yield it to the validation block:
104
104
 
105
105
  ```ruby rspec validation_hash_aligned
@@ -125,7 +125,7 @@ expect(result.failure).to match({ children: { bar: { errors: [have_attributes(co
125
125
 
126
126
  This separation enables two powerful patterns:
127
127
 
128
- **1. Meaningful error keys** Store errors under descriptive names rather than raw data keys:
128
+ **1. Meaningful error keys** Store errors under descriptive names rather than raw data keys:
129
129
 
130
130
  ```ruby rspec validation_hash_tuple_unaligned
131
131
  result = Validator
@@ -137,10 +137,10 @@ result = Validator
137
137
  expect(result.failure).to match({ children: { total: { errors: [have_attributes(code: :excessive)] } } })
138
138
  ```
139
139
 
140
- Note how the `from` parameter accepts an array of paths this creates a tuple from multiple values,
140
+ Note how the `from` parameter accepts an array of paths this creates a tuple from multiple values,
141
141
  perfect for cross-field validations.
142
142
 
143
- **2. Data transformation** Remap input data into new structures using `Commit` rulings.
143
+ **2. Data transformation** Remap input data into new structures using `Commit` rulings.
144
144
  The `from` parameter lets you source data from one location while building transformed
145
145
  output at another. We'll explore this pattern
146
146
  in detail [later](#transforming-the-validated-object).
@@ -148,7 +148,7 @@ in detail [later](#transforming-the-validated-object).
148
148
  ### Alternative syntax
149
149
  You can also apply rulings directly to validator nodes rather
150
150
  of returning them from validation blocks. This permits
151
- more concise phrasing in certain cases for example when passing validator
151
+ more concise phrasing in certain cases for example when passing validator
152
152
  into functions:
153
153
 
154
154
  ```ruby rspec node_disputed
@@ -163,7 +163,7 @@ expect(disputed.to_result.failure)
163
163
  .to match({ children: { total: { errors: [have_attributes(code: :excessive)] } } })
164
164
  ```
165
165
 
166
- Remember that validators are immutable methods like `dispute`, `refute`, and `commit`
166
+ Remember that validators are immutable methods like `dispute`, `refute`, and `commit`
167
167
  return new validator instance with updated state.
168
168
 
169
169
  ### Handling missing values
@@ -171,18 +171,18 @@ The `validate?` method provides flexible handling of missing values.
171
171
  While `validate` immediately refutes nodes when values aren't found,
172
172
  `validate?` offers more nuanced options:
173
173
 
174
- **Default behavior:** Skip validation entirely if the value is missing the validator state
174
+ **Default behavior:** Skip validation entirely if the value is missing the validator state
175
175
  remains unchanged.
176
176
 
177
177
  **With missing value strategies:** Call `validate?` without a block, then chain `.some_or_nil` or `.option`
178
178
  to control how missing values are handled:
179
179
 
180
- - **`some_or_nil`** Passes `nil` for missing values. In tuples, only missing fields become `nil`,
180
+ - **`some_or_nil`** Passes `nil` for missing values. In tuples, only missing fields become `nil`,
181
181
  not the entire tuple.
182
- - **`option`** Passes an option type (like `Dry::Result::Failure(Unit)` when using the Dry interface).
182
+ - **`option`** Passes an option type (like `Dry::Result::Failure(Unit)` when using the Dry interface).
183
183
  Again, in tuples only missing fields become *none* values.
184
184
 
185
- The `option` strategy enables validations where fields have disjunctive relationships
185
+ The `option` strategy enables validations where fields have disjunctive relationships
186
186
  like "either `:foo` or `:bar` must be set, but not both":
187
187
 
188
188
  ```ruby rspec validation_option
@@ -222,7 +222,7 @@ expect(result.failure)
222
222
  .to match({ children: { foo: { errors: [have_attributes(code: :invalid_access)] } } })
223
223
  ```
224
224
 
225
- This means you can validate any object without worrying about method availability missing methods
225
+ This means you can validate any object without worrying about method availability missing methods
226
226
  become validation errors rather than runtime exceptions.
227
227
 
228
228
  ## Predicates
@@ -249,11 +249,11 @@ end
249
249
  ```
250
250
 
251
251
  **Key concepts:**
252
- - **`Ruling::Invalidate`** A suspended ruling that doesn't specify severity (`dispute` vs `refute`).
252
+ - **`Ruling::Invalidate`** A suspended ruling that doesn't specify severity (`dispute` vs `refute`).
253
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.
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
257
 
258
258
  This separation lets predicates work with both definite and optional values
259
259
  while leaving severity decisions to the validation context where they're used.
@@ -316,7 +316,7 @@ expect(result.failure)
316
316
  disputes or refutations, giving you control over validation flow.
317
317
 
318
318
  **Missing values:** Like `validate?`, the `satisfy?` method handles missing values
319
- gracefully skipping validation by default, or using `some_or_nil`/`option` strategies
319
+ gracefully skipping validation by default, or using `some_or_nil`/`option` strategies
320
320
  when chained.
321
321
 
322
322
  ## Navigation
@@ -443,10 +443,10 @@ variants (`at?`, `each_at?`) that handle missing values gracefully. Note that
443
443
  make sense for collection elements.
444
444
 
445
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).
446
+ for other collection types (like `Set` or `ActiveRecord::Relation`) using [custom wrappers](#implementing-custom-wrappers).
447
447
 
448
448
  ## Flow control
449
- Basic flow control comes from the `Dispute`/`Refute` distinction—`Refute` rulings skip
449
+ Basic flow control comes from the `Dispute`/`Refute` distinction – `Refute` rulings skip
450
450
  all subsequent validations on that node.
451
451
 
452
452
  For more sophisticated control, use `with_valid` to conditionally execute validation
@@ -542,12 +542,37 @@ letting you reshape data while validating it.
542
542
  You can commit values through several mechanisms:
543
543
  - Return `Commit(value)` from a `validate` block
544
544
  - Call the `commit(value)` method on a validator node
545
+ - Use `transform` / `transform?` - extract and commit values with optional transformation
545
546
  - Pass `commit: true` to the `validate` or `satisfy` method (commits the original value if validation passes)
546
547
  - 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
+ into the specified collection either `array` or `hash` and commits them to the node
548
549
  after the iteration.
549
550
 
550
- Individual value commits aren't enough you must also commit the containing structure.
551
+ The `transform`/`transform?` methods mirror the semantics of the `validate`/`validate?` pair,
552
+ only they don't expect a ruling to be returned from the block, just the bare value.
553
+ The `transform?` variant supports suspended execution with `.option` and `.some_or_nil` strategies.
554
+
555
+
556
+ ```ruby rspec transformation_hash
557
+ result = Validator.instance({ bar: 'bar' }, coordinator)
558
+ .transform(from: [:bar]) { _1.upcase }
559
+ .to_result
560
+ .value!
561
+
562
+ expect(result).to eq('BAR')
563
+
564
+ result = Validator.instance({}, coordinator)
565
+ .transform?(from: [:bar])
566
+ .option { _1.value_or { 'default' } }
567
+ .to_result
568
+ .value!
569
+
570
+ expect(result).to eq('default')
571
+ ```
572
+
573
+ ### Structural commitment
574
+
575
+ Individual value commits aren't enough – you must also commit the containing structure.
551
576
  The validator can't automatically determine the desired output format,
552
577
  so you need to explicitly commit each level.
553
578
 
@@ -558,12 +583,14 @@ def self.item(item)
558
583
  item
559
584
  .satisfy(:name, commit: true) { :presence }
560
585
  .satisfy(:unit_price, from: [:price], commit: true ) { :presence }
586
+ .transform(:note, from: [:meta, :note]) { _1 }
561
587
  .auto_commit(as: :hash)
562
588
  end
563
589
 
590
+
564
591
  original_data = {
565
592
  customer: { name: 'John Doe' },
566
- items: [{ price: 100, name: 'Item 1' }],
593
+ items: [{ price: 100, name: 'Item 1', meta: { note: 'A note' } }],
567
594
  price: 100
568
595
  }
569
596
 
@@ -577,7 +604,7 @@ result = Validator
577
604
 
578
605
  transformed_data = {
579
606
  customer_name: 'John Doe',
580
- line_items: [{ name: 'Item 1', unit_price: 100 }],
607
+ line_items: [{ name: 'Item 1', unit_price: 100, note: 'A note' }],
581
608
  total: 100
582
609
  }
583
610
 
@@ -585,13 +612,19 @@ expect(result.success).to eq(transformed_data)
585
612
  ```
586
613
 
587
614
  This example demonstrates the full transformation pipeline:
588
- 1. Extract and validate data from nested sources (`customer.name`)
615
+ 1. Extract and validate data from nested sources (`customer.name`, `items.meta.note`)
589
616
  2. Commit individual values under new keys (`customer_name`, `total`, `line_items`, `unit_price`)
590
617
  3. Build the final transformed structure with `auto_commit`
591
618
 
592
619
  The result is a validated and transformed structure entirely different
593
620
  from the original data.
594
621
 
622
+ **Performance consideration:** This library is built primarily for validation workflows
623
+ with transformation capabilities as a convenience feature.
624
+ It is not a dedicated transformation tool. For transformation-heavy workflows, traditional explicit
625
+ Ruby transformations or specialized tools will provide significantly better performance. Use the
626
+ validator's transformation features when validation is the primary goal and transformation is incidental.
627
+
595
628
  ## Implementing custom wrappers
596
629
  The validator supports `Hash` and `Array` out of the box,
597
630
  but you can extend it to work with specialized collection types
@@ -658,7 +691,7 @@ handling patterns and result types, whether you're using a proprietary solution,
658
691
 
659
692
  ### Validation errors
660
693
  Validation errors must include the `StructuredError` marker module. This module
661
- defines abstract methods as suggestions rather than requirements the library
694
+ defines abstract methods as suggestions rather than requirements the library
662
695
  works with any type that includes the module.
663
696
 
664
697
  For simple cases, use the built-in `StructuredError::Record` class, which accepts:
@@ -694,7 +727,7 @@ method determines how the tree gets transformed into the final error
694
727
  structure returned by `to_result`. Different applications need different final formats.
695
728
 
696
729
  **Hierarchical Strategy** (`Coordinator::Errors::Hierarchical`)
697
- Preserves the tree structure as nested hashes most natural for debugging:
730
+ Preserves the tree structure as nested hashes most natural for debugging:
698
731
 
699
732
  ```ruby rspec with_hierarchical_adapter
700
733
  expected_failure = {
@@ -711,7 +744,7 @@ expect(result.to_result.failure).to eq(expected_failure)
711
744
  ```
712
745
 
713
746
  **Flat Strategy** (`Coordinator::Errors::Flat`)
714
- Flattens errors into path-value tuples useful for processing or storage:
747
+ Flattens errors into path-value tuples useful for processing or storage:
715
748
 
716
749
  ```ruby rspec with_flat_adapter
717
750
  expected_failure = [
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'active_model'
5
+ require 'dry/validation'
6
+ require 'byebug'
7
+
8
+ require_relative '../../lib/lite/validation/validator'
9
+ require_relative '../../spec/validation/validator/support/functional/coordinators/dry'
10
+
11
+ module Lite
12
+ module Validation
13
+ module Validator
14
+ module Benchmark
15
+ module Comparative
16
+ module Transformation
17
+ INPUT = {
18
+ customer: { name: 'John Doe' },
19
+ items: [
20
+ { price: '100', name: 'Item 1' },
21
+ { price: '200', name: 'Item 2' }
22
+ ],
23
+ charges: [
24
+ { price: '50', name: 'Charge 1' }
25
+ ],
26
+ item_total: '300',
27
+ charge_total: '50'
28
+ }.freeze
29
+
30
+ module HashBench
31
+ def self.call(input)
32
+ {
33
+ customer_name: input.dig(:customer, :name),
34
+ line_items: line_items(input[:items]),
35
+ charges: charges(input[:charges]),
36
+ total: input[:item_total].to_i + input[:charge_total].to_i
37
+ }
38
+ end
39
+
40
+ def self.line_items(items)
41
+ items.map { { unit_price: _1[:price].to_i, name: _1[:name] } }
42
+ end
43
+
44
+ def self.charges(charges)
45
+ charges.map { { amount: _1[:price].to_i, name: _1[:name] } }
46
+ end
47
+ end
48
+
49
+ module LiteBench
50
+ def self.call(input)
51
+ Validator
52
+ .instance(input, Support::Functional::Coordinators::Dry::Flat)
53
+ .transform(:customer_name, from: %i[customer name]) { _1 }
54
+ .each_at(:line_items, from: [:items], commit: :array) { line_item(_1) }
55
+ .each_at(:charges, commit: :array) { charge(_1) }
56
+ .transform(:total, from: [%i[item_total charge_total]]) { _1.map(&:to_i).sum }
57
+ .auto_commit(as: :hash)
58
+ end
59
+
60
+ def self.line_item(item)
61
+ item
62
+ .transform(:unit_price, from: %i[price]) { _1.to_i }
63
+ .transform(:name) { _1 }
64
+ .auto_commit(as: :hash)
65
+ end
66
+
67
+ def self.charge(charge)
68
+ charge
69
+ .transform(:amount, from: %i[price]) { _1.to_i }
70
+ .transform(:name) { _1 }
71
+ .auto_commit(as: :hash)
72
+ end
73
+ end
74
+
75
+ def self.run(n) # rubocop:disable Naming/MethodParameterName
76
+ runs = {}
77
+
78
+ runs[:Hash] = proc do
79
+ HashBench.call(INPUT)
80
+ end
81
+
82
+ runs[:Lite] = proc do
83
+ LiteBench.call(INPUT).to_result.success
84
+ end
85
+
86
+ runs.to_a.shuffle.each do |key, proc|
87
+ result = ::Benchmark.measure { n.times { proc.call } }
88
+ puts "#{key}: #{result}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ Lite::Validation::Validator::Benchmark::Comparative::Transformation.run(10_000)
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'active_model'
5
+ require 'dry/validation'
6
+ require 'byebug'
7
+
8
+ require_relative '../../spec/validation/validator/support/functional/contracts/hash'
9
+ require_relative '../../spec/validation/validator/support/functional/coordinators/dry'
10
+ require_relative '../../spec/validation/validator/support/shared/predicates/dry'
11
+
12
+ module Lite
13
+ module Validation
14
+ module Validator
15
+ module Benchmark
16
+ module Comparative
17
+ module Validation
18
+ class DryBench < Dry::Validation::Contract
19
+ option(:limit)
20
+
21
+ json do
22
+ required(:id).filled(:string)
23
+ required(:price).value(:integer, gteq?: 0)
24
+ required(:dates).schema do
25
+ required(:issued).value(:date_time)
26
+ required(:due).value(:date_time)
27
+ end
28
+ required(:payments).array do
29
+ schema do
30
+ required(:amount).value(:integer, gteq?: 0)
31
+ required(:type).value(:string, included_in?: %w[card cash])
32
+ end
33
+ end
34
+ required(:items).array do
35
+ schema do
36
+ required(:id).filled(:string)
37
+ required(:name).filled(:string)
38
+ required(:price).value(:integer, gteq?: 0)
39
+ required(:qty).value(:integer, gteq?: 0)
40
+ end
41
+ end
42
+ end
43
+
44
+ rule(dates: %i[issued due]) do
45
+ next if value[0] <= value[1]
46
+
47
+ key.failure('first must be less than or equal to the second')
48
+ end
49
+
50
+ rule(:price) do
51
+ next if value <= limit
52
+
53
+ key.failure('is excessive')
54
+ end
55
+ end
56
+
57
+ module ActiveModelBench
58
+ def self.call(data, limit:)
59
+ model ||= BenchModel.instance(data, limit: limit)
60
+ model.valid? ? nil : model.errors
61
+ end
62
+
63
+ class BenchModel
64
+ def self.instance(data, limit:)
65
+ dates = Dates.new(**data[:dates])
66
+ items = data[:items].map { Item.new(**data.slice(:id, :name, :price, :qty)) }
67
+ payments = data[:payments].map { Payment.new(**data.slice(:amount, :type)) }
68
+
69
+ BenchModel.new(
70
+ dates: dates,
71
+ items: items,
72
+ payments: payments,
73
+ limit: limit,
74
+ **data.slice(:id, :price)
75
+ )
76
+ end
77
+
78
+ class Dates
79
+ include ActiveModel::Model
80
+ include ActiveModel::Attributes
81
+ include ActiveModel::Validations
82
+
83
+ attribute :issued, :datetime
84
+ attribute :due, :datetime
85
+
86
+ validates :issued, presence: true
87
+ validates :due, presence: true
88
+
89
+ validate do
90
+ next if issued < due
91
+
92
+ errors.add('(issued,due)', 'first must be less than or equal to the second')
93
+ end
94
+ end
95
+
96
+ class Payment
97
+ include ActiveModel::Model
98
+ include ActiveModel::Attributes
99
+ include ActiveModel::Validations
100
+
101
+ attribute :amount, :integer
102
+ attribute :type, :string
103
+
104
+ validates :amount, presence: true, numericality: { greater_than: 0 }
105
+ validates :type, presence: true, inclusion: %w[cash card]
106
+ end
107
+
108
+ class Item
109
+ include ActiveModel::Model
110
+ include ActiveModel::Attributes
111
+ include ActiveModel::Validations
112
+
113
+ attribute :id, :string
114
+ attribute :name, :string
115
+ attribute :price, :integer
116
+ attribute :qty, :integer
117
+
118
+ validates :price, presence: true, numericality: { greater_than: 0 }
119
+ validates :qty, presence: true, numericality: { greater_than: 0 }
120
+ end
121
+
122
+ include ActiveModel::Model
123
+ include ActiveModel::Attributes
124
+ include ActiveModel::Validations
125
+
126
+ attribute :id, :string
127
+ attribute :price, :integer
128
+ attribute :limit, :integer
129
+ attribute :dates
130
+ attribute :items
131
+ attribute :payments
132
+
133
+ validates :id, presence: true
134
+ validates :price, presence: true
135
+
136
+ validate do
137
+ next if dates.valid?
138
+
139
+ dates.errors.messages.each { |attr, msg| errors.add("dates.#{attr}", msg) }
140
+ end
141
+
142
+ validate do
143
+ items.each_with_index do |item, idx|
144
+ next if item.valid?
145
+
146
+ item.errors.messages.each { |attr, msg| errors.add("items.#{idx}.#{attr}", msg) }
147
+ end
148
+ end
149
+
150
+ validate do
151
+ payments.each_with_index do |payment, idx|
152
+ next if payment.valid?
153
+
154
+ payment.errors.messages.each { |attr, msg| errors.add("payments.#{idx}.#{attr}", msg) }
155
+ end
156
+ end
157
+
158
+ validate do
159
+ errors.add(:price, 'is excessive') if price > limit
160
+ end
161
+ end
162
+ end
163
+
164
+ LiteBench = Support::Functional::Contracts::Hash
165
+
166
+ VALID = LiteBench::VALID
167
+ INVALID = LiteBench::INVALID
168
+ CONTEXT = LiteBench::CONTEXT
169
+
170
+ def self.run(n) # rubocop:disable Naming/MethodParameterName, Metrics/AbcSize
171
+ runs = {}
172
+
173
+ runs[:ActiveModel] = proc do |idx|
174
+ ActiveModelBench.call(data(idx), **CONTEXT)
175
+ end
176
+
177
+ runs[:Dry] = proc do |idx|
178
+ DryBench.new(**CONTEXT).call(data(idx))
179
+ end
180
+
181
+ runs[:Lite] = proc do |idx|
182
+ LiteBench.call(
183
+ data(idx),
184
+ Support::Functional::Coordinators::Dry::Flat,
185
+ CONTEXT
186
+ ).to_result
187
+ end
188
+
189
+ runs.to_a.shuffle.each do |key, proc|
190
+ result = ::Benchmark.measure { n.times { |idx| proc.call(idx) } }
191
+ puts "#{key}: #{result}"
192
+ end
193
+ end
194
+
195
+ def self.data(idx)
196
+ (idx % 5).zero? ? INVALID : VALID
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ Lite::Validation::Validator::Benchmark::Comparative::Validation.run(1000)
@@ -6,6 +6,7 @@ require_relative 'implementation/identity'
6
6
  require_relative 'implementation/iteration'
7
7
  require_relative 'implementation/predication'
8
8
  require_relative 'implementation/scoping'
9
+ require_relative 'implementation/transformation'
9
10
 
10
11
  module Lite
11
12
  module Validation
@@ -18,6 +19,7 @@ module Lite
18
19
  include Implementation::Iteration
19
20
  include Implementation::Predication
20
21
  include Implementation::Scoping
22
+ include Implementation::Transformation
21
23
  end
22
24
  end
23
25
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../ruling'
3
+ require_relative '../../../error'
4
4
  require_relative 'helpers/with_result'
5
5
 
6
6
  module Lite
@@ -9,10 +9,24 @@ module Lite
9
9
  module Node
10
10
  module Implementation
11
11
  module ApplyRuling
12
- include Ruling::Constructors
12
+ def self.apply_ruling(validator, path: nil)
13
+ updated, _meta = validator.result.navigate(*path) do |result|
14
+ applied = yield result
15
+ validator.merge_strategy.transform_result(applied, validator, path)
16
+ end
17
+ Helpers::WithResult.with_result(validator, updated)
18
+ end
19
+
20
+ def self.structured_error(coordinator, error, **opts)
21
+ case [error, opts]
22
+ in [StructuredError, {}] then error
23
+ in [Symbol, { ** }] then coordinator.structured_error(error, **opts)
24
+ else raise Error::Fatal, "Unexpected first argument: #{error.inspect}"
25
+ end
26
+ end
13
27
 
14
- def commit(value)
15
- ApplyRuling.apply_ruling(self, Commit(value))
28
+ def commit(value, at: nil)
29
+ ApplyRuling.apply_ruling(self, path: at) { _1.commit(value) }
16
30
  end
17
31
 
18
32
  def auto_commit(as:)
@@ -20,21 +34,15 @@ module Lite
20
34
  end
21
35
 
22
36
  def dispute(error, at: nil, **opts)
23
- ApplyRuling.apply_ruling(self, Dispute(error, **opts), path: at)
37
+ ApplyRuling.apply_ruling(self, path: at) do |result|
38
+ result.dispute(ApplyRuling.structured_error(coordinator, error, **opts))
39
+ end
24
40
  end
25
41
 
26
42
  def refute(error, at: nil, **opts)
27
- ApplyRuling.apply_ruling(self, Refute(error, **opts), path: at)
28
- end
29
-
30
- def self.apply_ruling(validator, ruling, path: nil)
31
- return validator if ruling.is_a?(Ruling::Pass)
32
-
33
- updated, _meta = validator.result.navigate(*path) do |result|
34
- applied = Ruling.apply(ruling, result, validator.coordinator)
35
- validator.merge_strategy.transform_result(applied, validator, path)
43
+ ApplyRuling.apply_ruling(self, path: at) do |result|
44
+ result.refute(ApplyRuling.structured_error(coordinator, error, **opts))
36
45
  end
37
- Helpers::WithResult.with_result(validator, updated)
38
46
  end
39
47
  end
40
48
  end
@@ -24,8 +24,8 @@ module Lite
24
24
  end
25
25
 
26
26
  module Nullify
27
- def self.child_parameters(_validator, option, _result, &block)
28
- block.call(option.some_or_nil)
27
+ def self.child_parameters(validator, option, _result, &block)
28
+ block.call(option.some_or_nil, validator.send(:state).value_definite)
29
29
  end
30
30
 
31
31
  def self.block_parameters(_validator, option, _result, &block)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'dig'
4
- require_relative 'helpers/yield_validator'
5
4
  require_relative '../suspended'
5
+ require_relative 'helpers/yield_validator'
6
6
  require_relative 'helpers/yield_strategy'
7
7
 
8
8
  module Lite
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../ruling'
4
+ require_relative '../suspended'
5
+ require_relative 'dig'
6
+ require_relative 'helpers/call_foreign'
7
+ require_relative 'helpers/yield_strategy'
8
+
9
+ module Lite
10
+ module Validation
11
+ module Validator
12
+ module Node
13
+ module Implementation
14
+ module Transformation
15
+ include Ruling::Constructors
16
+ include Dig
17
+
18
+ def transform?(*path, from: nil, &block)
19
+ return Suspended.new(:transform!, self, path, from) if block.nil?
20
+
21
+ transform!(path, from, :skip, block)
22
+ end
23
+
24
+ def transform(*path, from: nil, &block)
25
+ transform!(path, from, :refute, block)
26
+ end
27
+
28
+ private
29
+
30
+ def transform!(path, from, strategy, block)
31
+ return self unless result.success?
32
+
33
+ dig(*path, from: from) do |option, result|
34
+ strategy = Helpers::YieldStrategy.to_yield(strategy)
35
+ strategy.block_parameters(self, option, result) do |to_yield|
36
+ Helpers::CallForeign.call_foreign(result, coordinator) do
37
+ value = block.call(to_yield, context)
38
+ committed = result.commit(value)
39
+ merge_strategy.transform_result(committed, self, path)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'dig'
4
3
  require_relative '../suspended'
4
+ require_relative 'dig'
5
5
  require_relative 'helpers/call_foreign'
6
6
  require_relative 'helpers/yield_strategy'
7
7
 
@@ -3,7 +3,7 @@
3
3
  module Lite
4
4
  module Validation
5
5
  module Version
6
- VERSION = '0.0.1'
6
+ VERSION = '0.0.3'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lite-validation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomas Milsimer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-06 00:00:00.000000000 Z
11
+ date: 2025-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lite-data
@@ -37,7 +37,8 @@ files:
37
37
  - Gemfile
38
38
  - README.md
39
39
  - bench/calibrational.rb
40
- - bench/comparative.rb
40
+ - bench/comparative/transformation.rb
41
+ - bench/comparative/validation.rb
41
42
  - bench/functional.rb
42
43
  - bench/profile.rb
43
44
  - lib/lite/validation.rb
@@ -86,6 +87,7 @@ files:
86
87
  - lib/lite/validation/validator/node/implementation/predication.rb
87
88
  - lib/lite/validation/validator/node/implementation/scoping.rb
88
89
  - lib/lite/validation/validator/node/implementation/scoping/evaluator.rb
90
+ - lib/lite/validation/validator/node/implementation/transformation.rb
89
91
  - lib/lite/validation/validator/node/implementation/validation.rb
90
92
  - lib/lite/validation/validator/node/implementation/wrap.rb
91
93
  - lib/lite/validation/validator/node/root.rb
data/bench/comparative.rb DELETED
@@ -1,197 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'benchmark'
4
- require 'active_model'
5
- require 'dry/validation'
6
- require 'byebug'
7
-
8
- require_relative '../spec/validation/validator/support/functional/contracts/hash'
9
- require_relative '../spec/validation/validator/support/functional/coordinators/dry'
10
- require_relative '../spec/validation/validator/support/shared/predicates/dry'
11
-
12
- module Lite
13
- module Validation
14
- module Validator
15
- module Benchmark
16
- module Comparative
17
- class DryBench < Dry::Validation::Contract
18
- option(:limit)
19
-
20
- json do
21
- required(:id).filled(:string)
22
- required(:price).value(:integer, gteq?: 0)
23
- required(:dates).schema do
24
- required(:issued).value(:date_time)
25
- required(:due).value(:date_time)
26
- end
27
- required(:payments).array do
28
- schema do
29
- required(:amount).value(:integer, gteq?: 0)
30
- required(:type).value(:string, included_in?: %w[card cash])
31
- end
32
- end
33
- required(:items).array do
34
- schema do
35
- required(:id).filled(:string)
36
- required(:name).filled(:string)
37
- required(:price).value(:integer, gteq?: 0)
38
- required(:qty).value(:integer, gteq?: 0)
39
- end
40
- end
41
- end
42
-
43
- rule(dates: %i[issued due]) do
44
- next if value[0] <= value[1]
45
-
46
- key.failure('first must be less than or equal to the second')
47
- end
48
-
49
- rule(:price) do
50
- next if value <= limit
51
-
52
- key.failure('is excessive')
53
- end
54
- end
55
-
56
- class ActiveModelBench < Dry::Validation::Contract
57
- def self.call(data, limit:)
58
- model ||= BenchModel.instance(data, limit: limit)
59
- model.valid? ? nil : model.errors
60
- end
61
-
62
- class BenchModel
63
- def self.instance(data, limit:)
64
- dates = Dates.new(**data[:dates])
65
- items = data[:items].map { Item.new(**data.slice(:id, :name, :price, :qty)) }
66
- payments = data[:payments].map { Payment.new(**data.slice(:amount, :type)) }
67
-
68
- BenchModel.new(dates: dates, items: items, payments: payments, limit: limit, **data.slice(:id, :price))
69
- end
70
-
71
- class Dates
72
- include ActiveModel::Model
73
- include ActiveModel::Attributes
74
- include ActiveModel::Validations
75
-
76
- attribute :issued, :datetime
77
- attribute :due, :datetime
78
-
79
- validates :issued, presence: true
80
- validates :due, presence: true
81
-
82
- validate do
83
- next if issued < due
84
-
85
- errors.add('(issued,due)', 'first must be less than or equal to the second')
86
- end
87
- end
88
-
89
- class Payment
90
- include ActiveModel::Model
91
- include ActiveModel::Attributes
92
- include ActiveModel::Validations
93
-
94
- attribute :amount, :integer
95
- attribute :type, :string
96
-
97
- validates :amount, presence: true, numericality: { greater_than: 0 }
98
- validates :type, presence: true, inclusion: %w[cash card]
99
- end
100
-
101
- class Item
102
- include ActiveModel::Model
103
- include ActiveModel::Attributes
104
- include ActiveModel::Validations
105
-
106
- attribute :id, :string
107
- attribute :name, :string
108
- attribute :price, :integer
109
- attribute :qty, :integer
110
-
111
- validates :price, presence: true, numericality: { greater_than: 0 }
112
- validates :qty, presence: true, numericality: { greater_than: 0 }
113
- end
114
-
115
- include ActiveModel::Model
116
- include ActiveModel::Attributes
117
- include ActiveModel::Validations
118
-
119
- attribute :id, :string
120
- attribute :price, :integer
121
- attribute :limit, :integer
122
- attribute :dates
123
- attribute :items
124
- attribute :payments
125
-
126
- validates :id, presence: true
127
- validates :price, presence: true
128
-
129
- validate do
130
- next if dates.valid?
131
-
132
- dates.errors.messages.each { |attr, msg| errors.add("dates.#{attr}", msg) }
133
- end
134
-
135
- validate do
136
- items.each_with_index do |item, idx|
137
- next if item.valid?
138
-
139
- item.errors.messages.each { |attr, msg| errors.add("items.#{idx}.#{attr}", msg) }
140
- end
141
- end
142
-
143
- validate do
144
- payments.each_with_index do |payment, idx|
145
- next if payment.valid?
146
-
147
- payment.errors.messages.each { |attr, msg| errors.add("payments.#{idx}.#{attr}", msg) }
148
- end
149
- end
150
-
151
- validate do
152
- errors.add(:price, 'is excessive') if price > limit
153
- end
154
- end
155
- end
156
-
157
- LiteBench = Support::Functional::Contracts::Hash
158
-
159
- VALID = LiteBench::VALID
160
- INVALID = LiteBench::INVALID
161
- CONTEXT = LiteBench::CONTEXT
162
-
163
- def self.run(n) # rubocop:disable Naming/MethodParameterName, Metrics/AbcSize
164
- runs = {}
165
-
166
- runs[:ActiveModel] = proc do |idx|
167
- ActiveModelBench.call(data(idx), **CONTEXT)
168
- end
169
-
170
- runs[:Dry] = proc do |idx|
171
- DryBench.new(**CONTEXT).call(data(idx))
172
- end
173
-
174
- runs[:Lite] = proc do |idx|
175
- LiteBench.call(
176
- data(idx),
177
- Support::Functional::Coordinators::Dry::Flat,
178
- CONTEXT
179
- ).to_result
180
- end
181
-
182
- runs.to_a.shuffle.each do |key, proc|
183
- result = ::Benchmark.measure { n.times { |idx| proc.call(idx) } }
184
- puts "#{key}: #{result}"
185
- end
186
- end
187
-
188
- def self.data(idx)
189
- (idx % 5).zero? ? INVALID : VALID
190
- end
191
- end
192
- end
193
- end
194
- end
195
- end
196
-
197
- Lite::Validation::Validator::Benchmark::Comparative.run(1000)