treaty 0.14.0 → 0.16.0

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.
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module Attribute
5
5
  module Option
6
- # Central registry for all option processors (validators and modifiers).
6
+ # Central registry for all option processors (validators, modifiers, and conditionals).
7
7
  #
8
8
  # ## Purpose
9
9
  #
@@ -14,7 +14,7 @@ module Treaty
14
14
  #
15
15
  # 1. **Registration** - Stores option processor classes
16
16
  # 2. **Retrieval** - Provides access to registered processors
17
- # 3. **Categorization** - Organizes processors by category (validator/modifier)
17
+ # 3. **Categorization** - Organizes processors by category (validator/modifier/conditional)
18
18
  # 4. **Validation** - Checks if options are registered
19
19
  #
20
20
  # ## Registered Options
@@ -23,15 +23,23 @@ module Treaty
23
23
  # - `:required` → RequiredValidator
24
24
  # - `:type` → TypeValidator
25
25
  # - `:inclusion` → InclusionValidator
26
+ # - `:format` → FormatValidator
26
27
  #
27
28
  # ### Modifiers
28
29
  # - `:as` → AsModifier
29
30
  # - `:default` → DefaultModifier
31
+ # - `:transform` → TransformModifier
32
+ # - `:cast` → CastModifier
33
+ #
34
+ # ### Conditionals
35
+ # - `:if` → IfConditional
36
+ # - `:unless` → UnlessConditional
30
37
  #
31
38
  # ## Usage
32
39
  #
33
40
  # Registration (done in RegistryInitializer):
34
41
  # Registry.register(:required, RequiredValidator, category: :validator)
42
+ # Registry.register(:if, IfConditional, category: :conditional)
35
43
  #
36
44
  # Retrieval (done in OptionOrchestrator):
37
45
  # processor_class = Registry.processor_for(:required)
@@ -111,6 +119,14 @@ module Treaty
111
119
  .transform_values { |info| info.fetch(:processor_class) }
112
120
  end
113
121
 
122
+ # Get all conditionals
123
+ #
124
+ # @return [Hash] Hash of option_name => processor_class for conditionals
125
+ def conditionals
126
+ registry.select { |_, info| info.fetch(:category) == :conditional }
127
+ .transform_values { |info| info.fetch(:processor_class) }
128
+ end
129
+
114
130
  # Reset registry (mainly for testing)
115
131
  def reset!
116
132
  @registry = nil
@@ -7,14 +7,15 @@ module Treaty
7
7
  #
8
8
  # ## Purpose
9
9
  #
10
- # Centralized registration point for all option processors (validators and modifiers).
10
+ # Centralized registration point for all option processors (validators, modifiers, and conditionals).
11
11
  # Automatically registers all built-in options when loaded.
12
12
  #
13
13
  # ## Responsibilities
14
14
  #
15
15
  # 1. **Validator Registration** - Registers all built-in validators
16
16
  # 2. **Modifier Registration** - Registers all built-in modifiers
17
- # 3. **Auto-Loading** - Executes automatically when file is loaded
17
+ # 3. **Conditional Registration** - Registers all built-in conditionals
18
+ # 4. **Auto-Loading** - Executes automatically when file is loaded
18
19
  #
19
20
  # ## Built-in Validators
20
21
  #
@@ -25,10 +26,16 @@ module Treaty
25
26
  #
26
27
  # ## Built-in Modifiers
27
28
  #
28
- # - `:as` → AsModifier - Renames attributes
29
- # - `:default` → DefaultModifier - Provides default values
29
+ # - `:computed` → ComputedModifier - Computes values from all raw data (executes first)
30
30
  # - `:transform` → TransformModifier - Transforms values using custom lambdas
31
31
  # - `:cast` → CastModifier - Converts values between types automatically
32
+ # - `:default` → DefaultModifier - Provides default values
33
+ # - `:as` → AsModifier - Renames attributes
34
+ #
35
+ # ## Built-in Conditionals
36
+ #
37
+ # - `:if` → IfConditional - Conditionally includes attributes based on runtime data
38
+ # - `:unless` → UnlessConditional - Conditionally excludes attributes based on runtime data
32
39
  #
33
40
  # ## Auto-Registration
34
41
  #
@@ -63,6 +70,7 @@ module Treaty
63
70
  def register_all!
64
71
  register_validators!
65
72
  register_modifiers!
73
+ register_conditionals!
66
74
  end
67
75
 
68
76
  private
@@ -78,13 +86,23 @@ module Treaty
78
86
  end
79
87
 
80
88
  # Registers all built-in modifiers
89
+ # Order matters: computed runs first, then transform, cast, default, as
81
90
  #
82
91
  # @return [void]
83
92
  def register_modifiers!
84
- Registry.register(:as, Modifiers::AsModifier, category: :modifier)
85
- Registry.register(:default, Modifiers::DefaultModifier, category: :modifier)
93
+ Registry.register(:computed, Modifiers::ComputedModifier, category: :modifier)
86
94
  Registry.register(:transform, Modifiers::TransformModifier, category: :modifier)
87
95
  Registry.register(:cast, Modifiers::CastModifier, category: :modifier)
96
+ Registry.register(:default, Modifiers::DefaultModifier, category: :modifier)
97
+ Registry.register(:as, Modifiers::AsModifier, category: :modifier)
98
+ end
99
+
100
+ # Registers all built-in conditionals
101
+ #
102
+ # @return [void]
103
+ def register_conditionals!
104
+ Registry.register(:if, Conditionals::IfConditional, category: :conditional)
105
+ Registry.register(:unless, Conditionals::UnlessConditional, category: :conditional)
88
106
  end
89
107
  end
90
108
  end
@@ -103,10 +103,11 @@ module Treaty
103
103
  # Applies transformations like defaults, type coercion, etc.
104
104
  #
105
105
  # @param value [Object] The value to transform
106
+ # @param root_data [Hash] Full raw data from root level (used by computed modifier)
106
107
  # @return [Object] Transformed value
107
- def transform_value(value)
108
+ def transform_value(value, root_data = {})
108
109
  @processors.values.reduce(value) do |current_value, processor|
109
- processor.transform_value(current_value)
110
+ processor.transform_value(current_value, root_data)
110
111
  end
111
112
  end
112
113
 
@@ -68,9 +68,10 @@ module Treaty
68
68
  # Transforms attribute value through all modifiers
69
69
  #
70
70
  # @param value [Object] The value to transform
71
+ # @param root_data [Hash] Full raw data from root level (used by computed modifier)
71
72
  # @return [Object] Transformed value
72
- def transform_value(value)
73
- option_orchestrator.transform_value(value)
73
+ def transform_value(value, root_data = {})
74
+ option_orchestrator.transform_value(value, root_data)
74
75
  end
75
76
 
76
77
  # Checks if attribute name is transformed
@@ -22,15 +22,16 @@ module Treaty
22
22
  # Returns original value if nil or not nested
23
23
  #
24
24
  # @param value [Object] The value to transform
25
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
25
26
  # @return [Object] Transformed value
26
- def transform(value)
27
+ def transform(value, root_data = {})
27
28
  return value if value.nil?
28
29
 
29
30
  case attribute.type
30
31
  when :object
31
- transform_object(value)
32
+ transform_object(value, root_data)
32
33
  when :array
33
- transform_array(value)
34
+ transform_array(value, root_data)
34
35
  else
35
36
  value
36
37
  end
@@ -41,23 +42,25 @@ module Treaty
41
42
  # Transforms object (hash) value
42
43
  #
43
44
  # @param value [Hash] The hash to transform
45
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
44
46
  # @return [Hash] Transformed hash
45
- def transform_object(value)
47
+ def transform_object(value, root_data = {})
46
48
  return value unless attribute.nested?
47
49
 
48
50
  transformer = ObjectTransformer.new(attribute)
49
- transformer.transform(value)
51
+ transformer.transform(value, root_data)
50
52
  end
51
53
 
52
54
  # Transforms array value
53
55
  #
54
56
  # @param value [Array] The array to transform
57
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
55
58
  # @return [Array] Transformed array
56
- def transform_array(value)
59
+ def transform_array(value, root_data = {})
57
60
  return value unless attribute.nested?
58
61
 
59
62
  transformer = ArrayTransformer.new(attribute)
60
- transformer.transform(value)
63
+ transformer.transform(value, root_data)
61
64
  end
62
65
 
63
66
  # Transforms object (hash) with nested attributes
@@ -74,12 +77,16 @@ module Treaty
74
77
  # Transforms hash by processing all nested attributes
75
78
  #
76
79
  # @param value [Hash] The source hash
80
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
77
81
  # @return [Hash] Transformed hash with processed attributes
78
- def transform(value)
82
+ def transform(value, root_data = {})
79
83
  transformed = {}
80
84
 
81
85
  attribute.collection_of_attributes.each do |nested_attribute|
82
- process_attribute(nested_attribute, value, transformed)
86
+ # Check if conditional (if/unless option) - skip attribute if condition evaluates to skip
87
+ next unless should_process_attribute?(nested_attribute, value)
88
+
89
+ process_attribute(nested_attribute, value, transformed, root_data)
83
90
  end
84
91
 
85
92
  transformed
@@ -87,14 +94,96 @@ module Treaty
87
94
 
88
95
  private
89
96
 
97
+ # Returns the conditional option name if present (:if or :unless)
98
+ # Raises error if both are present (mutual exclusivity)
99
+ #
100
+ # @param nested_attribute [Attribute::Base] The attribute to check
101
+ # @raise [Treaty::Exceptions::Validation] If both :if and :unless are present
102
+ # @return [Symbol, nil] :if, :unless, or nil
103
+ def conditional_option_for(nested_attribute) # rubocop:disable Metrics/MethodLength
104
+ has_if = nested_attribute.options.key?(:if)
105
+ has_unless = nested_attribute.options.key?(:unless)
106
+
107
+ if has_if && has_unless
108
+ raise Treaty::Exceptions::Validation,
109
+ I18n.t(
110
+ "treaty.attributes.conditionals.mutual_exclusivity_error",
111
+ attribute: nested_attribute.name
112
+ )
113
+ end
114
+
115
+ return :if if has_if
116
+ return :unless if has_unless
117
+
118
+ nil
119
+ end
120
+
121
+ # Gets cached conditional processors for attributes or builds them
122
+ #
123
+ # @return [Hash] Hash of attribute => conditional processor
124
+ def conditionals_for_attributes
125
+ @conditionals_for_attributes ||= build_conditionals_for_attributes
126
+ end
127
+
128
+ # Builds conditional processors for attributes with :if or :unless option
129
+ # Validates schema at definition time for performance
130
+ #
131
+ # @return [Hash] Hash of attribute => conditional processor
132
+ def build_conditionals_for_attributes # rubocop:disable Metrics/MethodLength
133
+ attribute.collection_of_attributes.each_with_object({}) do |nested_attribute, cache|
134
+ # Get conditional option name (:if or :unless)
135
+ conditional_type = conditional_option_for(nested_attribute)
136
+ next if conditional_type.nil?
137
+
138
+ processor_class = Option::Registry.processor_for(conditional_type)
139
+ next if processor_class.nil?
140
+
141
+ # Create processor instance
142
+ conditional = processor_class.new(
143
+ attribute_name: nested_attribute.name,
144
+ attribute_type: nested_attribute.type,
145
+ option_schema: nested_attribute.options.fetch(conditional_type)
146
+ )
147
+
148
+ # Validate schema at definition time (not runtime)
149
+ conditional.validate_schema!
150
+
151
+ cache[nested_attribute] = conditional
152
+ end
153
+ end
154
+
155
+ # Checks if an attribute should be processed based on its conditional (if/unless option)
156
+ # Returns true if no conditional is defined or if conditional evaluates appropriately
157
+ #
158
+ # @param nested_attribute [Attribute::Base] The attribute to check
159
+ # @param source_hash [Hash] Source data to pass to conditional
160
+ # @return [Boolean] True if attribute should be processed, false to skip it
161
+ def should_process_attribute?(nested_attribute, source_hash)
162
+ # Check if attribute has a conditional option
163
+ conditional_type = conditional_option_for(nested_attribute)
164
+ return true if conditional_type.nil?
165
+
166
+ # Get cached conditional processor
167
+ conditional = conditionals_for_attributes[nested_attribute]
168
+ return true if conditional.nil?
169
+
170
+ # Evaluate condition with source hash data wrapped with parent object name
171
+ wrapped_data = { attribute.name => source_hash }
172
+ conditional.evaluate_condition(wrapped_data)
173
+ rescue StandardError
174
+ # If conditional evaluation fails, skip the attribute
175
+ false
176
+ end
177
+
90
178
  # Processes a single nested attribute
91
179
  # Validates, transforms, and adds to target hash
92
180
  #
93
181
  # @param nested_attribute [Attribute::Base] Attribute to process
94
182
  # @param source_hash [Hash] Source data
95
183
  # @param target_hash [Hash] Target hash to populate
184
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
96
185
  # @return [void]
97
- def process_attribute(nested_attribute, source_hash, target_hash) # rubocop:disable Metrics/MethodLength
186
+ def process_attribute(nested_attribute, source_hash, target_hash, root_data = {}) # rubocop:disable Metrics/MethodLength
98
187
  source_name = nested_attribute.name
99
188
  nested_value = source_hash.fetch(source_name, nil)
100
189
 
@@ -105,10 +194,10 @@ module Treaty
105
194
  nested_transformer = NestedTransformer.new(nested_attribute)
106
195
  validator.validate_type!(nested_value) unless nested_value.nil?
107
196
  validator.validate_required!(nested_value)
108
- nested_transformer.transform(nested_value)
197
+ nested_transformer.transform(nested_value, root_data)
109
198
  else
110
199
  validator.validate_value!(nested_value)
111
- validator.transform_value(nested_value)
200
+ validator.transform_value(nested_value, root_data)
112
201
  end
113
202
 
114
203
  target_name = validator.target_name
@@ -117,7 +206,7 @@ module Treaty
117
206
  end
118
207
 
119
208
  # Transforms array with nested attributes
120
- class ArrayTransformer
209
+ class ArrayTransformer # rubocop:disable Metrics/ClassLength
121
210
  SELF_OBJECT = :_self
122
211
  private_constant :SELF_OBJECT
123
212
 
@@ -134,19 +223,101 @@ module Treaty
134
223
  # Handles both simple arrays (:_self) and complex arrays (objects)
135
224
  #
136
225
  # @param value [Array] The source array
226
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
137
227
  # @return [Array] Transformed array
138
- def transform(value)
228
+ def transform(value, root_data = {})
139
229
  value.each_with_index.map do |item, index|
140
230
  if simple_array?
141
- transform_simple_element(item, index)
231
+ transform_simple_element(item, index, root_data)
142
232
  else
143
- transform_array_item(item, index)
233
+ transform_array_item(item, index, root_data)
144
234
  end
145
235
  end
146
236
  end
147
237
 
148
238
  private
149
239
 
240
+ # Returns the conditional option name if present (:if or :unless)
241
+ # Raises error if both are present (mutual exclusivity)
242
+ #
243
+ # @param nested_attribute [Attribute::Base] The attribute to check
244
+ # @raise [Treaty::Exceptions::Validation] If both :if and :unless are present
245
+ # @return [Symbol, nil] :if, :unless, or nil
246
+ def conditional_option_for(nested_attribute) # rubocop:disable Metrics/MethodLength
247
+ has_if = nested_attribute.options.key?(:if)
248
+ has_unless = nested_attribute.options.key?(:unless)
249
+
250
+ if has_if && has_unless
251
+ raise Treaty::Exceptions::Validation,
252
+ I18n.t(
253
+ "treaty.attributes.conditionals.mutual_exclusivity_error",
254
+ attribute: nested_attribute.name
255
+ )
256
+ end
257
+
258
+ return :if if has_if
259
+ return :unless if has_unless
260
+
261
+ nil
262
+ end
263
+
264
+ # Gets cached conditional processors for attributes or builds them
265
+ #
266
+ # @return [Hash] Hash of attribute => conditional processor
267
+ def conditionals_for_attributes
268
+ @conditionals_for_attributes ||= build_conditionals_for_attributes
269
+ end
270
+
271
+ # Builds conditional processors for attributes with :if or :unless option
272
+ # Validates schema at definition time for performance
273
+ #
274
+ # @return [Hash] Hash of attribute => conditional processor
275
+ def build_conditionals_for_attributes # rubocop:disable Metrics/MethodLength
276
+ attribute.collection_of_attributes.each_with_object({}) do |nested_attribute, cache|
277
+ # Get conditional option name (:if or :unless)
278
+ conditional_type = conditional_option_for(nested_attribute)
279
+ next if conditional_type.nil?
280
+
281
+ processor_class = Option::Registry.processor_for(conditional_type)
282
+ next if processor_class.nil?
283
+
284
+ # Create processor instance
285
+ conditional = processor_class.new(
286
+ attribute_name: nested_attribute.name,
287
+ attribute_type: nested_attribute.type,
288
+ option_schema: nested_attribute.options.fetch(conditional_type)
289
+ )
290
+
291
+ # Validate schema at definition time (not runtime)
292
+ conditional.validate_schema!
293
+
294
+ cache[nested_attribute] = conditional
295
+ end
296
+ end
297
+
298
+ # Checks if an attribute should be processed based on its conditional (if/unless option)
299
+ # Returns true if no conditional is defined or if conditional evaluates appropriately
300
+ #
301
+ # @param nested_attribute [Attribute::Base] The attribute to check
302
+ # @param source_hash [Hash] Source data to pass to conditional
303
+ # @return [Boolean] True if attribute should be processed, false to skip it
304
+ def should_process_attribute?(nested_attribute, source_hash)
305
+ # Check if attribute has a conditional option
306
+ conditional_type = conditional_option_for(nested_attribute)
307
+ return true if conditional_type.nil?
308
+
309
+ # Get cached conditional processor
310
+ conditional = conditionals_for_attributes[nested_attribute]
311
+ return true if conditional.nil?
312
+
313
+ # Evaluate condition with source hash data wrapped with parent array attribute name
314
+ wrapped_data = { attribute.name => source_hash }
315
+ conditional.evaluate_condition(wrapped_data)
316
+ rescue StandardError
317
+ # If conditional evaluation fails, skip the attribute
318
+ false
319
+ end
320
+
150
321
  # Checks if this is a simple array (primitive values)
151
322
  #
152
323
  # @return [Boolean] True if array contains primitive values with :_self attribute
@@ -160,16 +331,17 @@ module Treaty
160
331
  #
161
332
  # @param item [Object] Array element to transform
162
333
  # @param index [Integer] Element index for error messages
334
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
163
335
  # @raise [Treaty::Exceptions::Validation] If validation fails
164
336
  # @return [Object] Transformed element value
165
- def transform_simple_element(item, index) # rubocop:disable Metrics/MethodLength
166
- self_attr = attribute.collection_of_attributes.first
167
- validator = AttributeValidator.new(self_attr)
337
+ def transform_simple_element(item, index, root_data = {}) # rubocop:disable Metrics/MethodLength
338
+ self_attribute = attribute.collection_of_attributes.first
339
+ validator = AttributeValidator.new(self_attribute)
168
340
  validator.validate_schema!
169
341
 
170
342
  begin
171
343
  validator.validate_value!(item)
172
- validator.transform_value(item)
344
+ validator.transform_value(item, root_data)
173
345
  rescue Treaty::Exceptions::Validation => e
174
346
  raise Treaty::Exceptions::Validation,
175
347
  I18n.t(
@@ -185,9 +357,10 @@ module Treaty
185
357
  #
186
358
  # @param item [Hash] Array element to transform
187
359
  # @param index [Integer] Element index for error messages
360
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
188
361
  # @raise [Treaty::Exceptions::Validation] If item is not a Hash
189
362
  # @return [Hash] Transformed hash
190
- def transform_array_item(item, index) # rubocop:disable Metrics/MethodLength
363
+ def transform_array_item(item, index, root_data = {}) # rubocop:disable Metrics/MethodLength
191
364
  unless item.is_a?(Hash)
192
365
  raise Treaty::Exceptions::Validation,
193
366
  I18n.t(
@@ -201,7 +374,10 @@ module Treaty
201
374
  transformed = {}
202
375
 
203
376
  attribute.collection_of_attributes.each do |nested_attribute|
204
- process_attribute(nested_attribute, item, transformed, index)
377
+ # Check if conditional (if/unless option) - skip attribute if condition evaluates to skip
378
+ next unless should_process_attribute?(nested_attribute, item)
379
+
380
+ process_attribute(nested_attribute, item, transformed, index, root_data)
205
381
  end
206
382
 
207
383
  transformed
@@ -214,9 +390,10 @@ module Treaty
214
390
  # @param source_hash [Hash] Source data
215
391
  # @param target_hash [Hash] Target hash to populate
216
392
  # @param index [Integer] Array index for error messages
393
+ # @param root_data [Hash] Full raw data from root level (for computed modifier)
217
394
  # @raise [Treaty::Exceptions::Validation] If validation fails
218
395
  # @return [void]
219
- def process_attribute(nested_attribute, source_hash, target_hash, index) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
396
+ def process_attribute(nested_attribute, source_hash, target_hash, index, root_data = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
220
397
  source_name = nested_attribute.name
221
398
  nested_value = source_hash.fetch(source_name, nil)
222
399
 
@@ -228,10 +405,10 @@ module Treaty
228
405
  nested_transformer = NestedTransformer.new(nested_attribute)
229
406
  validator.validate_type!(nested_value) unless nested_value.nil?
230
407
  validator.validate_required!(nested_value)
231
- nested_transformer.transform(nested_value)
408
+ nested_transformer.transform(nested_value, root_data)
232
409
  else
233
410
  validator.validate_value!(nested_value)
234
- validator.transform_value(nested_value)
411
+ validator.transform_value(nested_value, root_data)
235
412
  end
236
413
  rescue Treaty::Exceptions::Validation => e
237
414
  raise Treaty::Exceptions::Validation,
@@ -70,12 +70,16 @@ module Treaty
70
70
 
71
71
  # Validates and transforms all attributes
72
72
  # Iterates through attributes, processes them, handles :_self objects
73
+ # Skips attributes with false conditional (if/unless option)
73
74
  #
74
75
  # @return [Hash] Transformed data with all attributes processed
75
- def validate!
76
+ def validate! # rubocop:disable Metrics/MethodLength
76
77
  transformed_data = {}
77
78
 
78
79
  collection_of_attributes.each do |attribute|
80
+ # Check if conditional (if/unless option) - skip attribute if condition evaluates to skip
81
+ next unless should_process_attribute?(attribute)
82
+
79
83
  transformed_value = validate_and_transform_attribute!(attribute)
80
84
 
81
85
  if attribute.name == SELF_OBJECT && attribute.type == :object
@@ -91,6 +95,49 @@ module Treaty
91
95
 
92
96
  private
93
97
 
98
+ # Returns the conditional option name if present (:if or :unless)
99
+ # Raises error if both are present (mutual exclusivity)
100
+ #
101
+ # @param attribute [Attribute::Base] The attribute to check
102
+ # @raise [Treaty::Exceptions::Validation] If both :if and :unless are present
103
+ # @return [Symbol, nil] :if, :unless, or nil
104
+ def conditional_option_for(attribute) # rubocop:disable Metrics/MethodLength
105
+ has_if = attribute.options.key?(:if)
106
+ has_unless = attribute.options.key?(:unless)
107
+
108
+ if has_if && has_unless
109
+ raise Treaty::Exceptions::Validation,
110
+ I18n.t(
111
+ "treaty.attributes.conditionals.mutual_exclusivity_error",
112
+ attribute: attribute.name
113
+ )
114
+ end
115
+
116
+ return :if if has_if
117
+ return :unless if has_unless
118
+
119
+ nil
120
+ end
121
+
122
+ # Checks if an attribute should be processed based on its conditional (if/unless option)
123
+ # Returns true if no conditional is defined or if conditional evaluates appropriately
124
+ #
125
+ # @param attribute [Attribute::Base] The attribute to check
126
+ # @return [Boolean] True if attribute should be processed, false to skip it
127
+ def should_process_attribute?(attribute)
128
+ # Check if attribute has a conditional option
129
+ conditional_type = conditional_option_for(attribute)
130
+ return true if conditional_type.nil?
131
+
132
+ # Get cached conditional processor
133
+ conditional = conditionals_for_attributes[attribute]
134
+ return true if conditional.nil?
135
+
136
+ # Evaluate condition with raw data
137
+ # The processor's evaluate_condition already handles if/unless logic
138
+ conditional.evaluate_condition(data)
139
+ end
140
+
94
141
  # Returns collection of attributes for this context
95
142
  # Must be implemented in subclasses
96
143
  #
@@ -119,6 +166,40 @@ module Treaty
119
166
  end
120
167
  end
121
168
 
169
+ # Gets cached conditional processors for attributes or builds them
170
+ #
171
+ # @return [Hash] Hash of attribute => conditional processor
172
+ def conditionals_for_attributes
173
+ @conditionals_for_attributes ||= build_conditionals_for_attributes
174
+ end
175
+
176
+ # Builds conditional processors for attributes with :if or :unless option
177
+ # Validates schema at definition time for performance
178
+ #
179
+ # @return [Hash] Hash of attribute => conditional processor
180
+ def build_conditionals_for_attributes # rubocop:disable Metrics/MethodLength
181
+ collection_of_attributes.each_with_object({}) do |attribute, cache|
182
+ # Get conditional option name (:if or :unless)
183
+ conditional_type = conditional_option_for(attribute)
184
+ next if conditional_type.nil?
185
+
186
+ processor_class = Option::Registry.processor_for(conditional_type)
187
+ next if processor_class.nil?
188
+
189
+ # Create processor instance
190
+ conditional = processor_class.new(
191
+ attribute_name: attribute.name,
192
+ attribute_type: attribute.type,
193
+ option_schema: attribute.options.fetch(conditional_type)
194
+ )
195
+
196
+ # Validate schema at definition time (not runtime)
197
+ conditional.validate_schema!
198
+
199
+ cache[attribute] = conditional
200
+ end
201
+ end
202
+
122
203
  # Validates and transforms a single attribute
123
204
  # Handles both nested and regular attributes
124
205
  #
@@ -138,7 +219,7 @@ module Treaty
138
219
  validate_and_transform_nested(attribute, value, validator)
139
220
  else
140
221
  validator.validate_value!(value)
141
- validator.transform_value(value)
222
+ validator.transform_value(value, data)
142
223
  end
143
224
  end
144
225
 
@@ -168,8 +249,9 @@ module Treaty
168
249
 
169
250
  # Step 4: Transform non-nil value
170
251
  # At this point, value is guaranteed to be non-nil
252
+ # Pass full root data as context for computed modifiers
171
253
  transformer = NestedTransformer.new(attribute)
172
- transformer.transform(value)
254
+ transformer.transform(value, data)
173
255
  end
174
256
  end
175
257
  end
data/lib/treaty/result.rb CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Treaty
4
4
  class Result
5
- attr_reader :data, :status
5
+ attr_reader :data, :status, :version
6
6
 
7
- def initialize(data:, status:)
7
+ def initialize(data:, status:, version:)
8
8
  @data = data
9
9
  @status = status
10
+ @version = version
10
11
  end
11
12
 
12
13
  def inspect
@@ -16,7 +17,7 @@ module Treaty
16
17
  private
17
18
 
18
19
  def draw_result
19
- "@data=#{@data.inspect}, @status=#{@status.inspect}"
20
+ "@data=#{@data.inspect}, @status=#{@status.inspect}, @version=#{@version.inspect}"
20
21
  end
21
22
  end
22
23
  end
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 14
6
+ MINOR = 16
7
7
  PATCH = 0
8
8
  PRE = nil
9
9