treaty 0.17.0 → 0.19.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/config/locales/en.yml +6 -2
  4. data/lib/treaty/engine.rb +1 -1
  5. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  6. data/lib/treaty/entity/attribute/base.rb +184 -0
  7. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  8. data/lib/treaty/entity/attribute/collection.rb +67 -0
  9. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  10. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  11. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  12. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  13. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  14. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  15. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  16. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  17. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  18. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  19. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  20. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  21. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  22. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  23. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  24. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  25. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  26. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  27. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  28. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  29. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  30. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  31. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  32. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  33. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  34. data/lib/treaty/entity/base.rb +90 -0
  35. data/lib/treaty/entity/builder.rb +44 -0
  36. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  37. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  38. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  39. data/lib/treaty/entity.rb +7 -75
  40. data/lib/treaty/request/attribute/attribute.rb +1 -1
  41. data/lib/treaty/request/attribute/builder.rb +24 -1
  42. data/lib/treaty/request/entity.rb +1 -1
  43. data/lib/treaty/request/factory.rb +6 -6
  44. data/lib/treaty/request/validator.rb +1 -1
  45. data/lib/treaty/response/attribute/attribute.rb +1 -1
  46. data/lib/treaty/response/attribute/builder.rb +24 -1
  47. data/lib/treaty/response/entity.rb +1 -1
  48. data/lib/treaty/response/factory.rb +6 -6
  49. data/lib/treaty/response/validator.rb +1 -1
  50. data/lib/treaty/version.rb +1 -1
  51. metadata +35 -34
  52. data/lib/treaty/attribute/base.rb +0 -182
  53. data/lib/treaty/attribute/builder/base.rb +0 -143
  54. data/lib/treaty/attribute/collection.rb +0 -65
  55. data/lib/treaty/attribute/dsl.rb +0 -90
  56. data/lib/treaty/attribute/entity/builder.rb +0 -23
  57. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  58. data/lib/treaty/attribute/option/base.rb +0 -188
  59. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  60. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  61. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  62. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  63. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  64. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  65. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  66. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  67. data/lib/treaty/attribute/option/registry.rb +0 -155
  68. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  69. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  70. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  71. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  72. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  73. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  74. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  75. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  76. data/lib/treaty/attribute/validation/base.rb +0 -74
  77. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  78. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  79. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  80. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
@@ -1,430 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Treaty
4
- module Attribute
5
- module Validation
6
- # Handles transformation of nested attributes (objects and arrays).
7
- # Extracted from Orchestrator::Base to reduce complexity.
8
- class NestedTransformer
9
- SELF_OBJECT = :_self
10
- private_constant :SELF_OBJECT
11
-
12
- attr_reader :attribute
13
-
14
- # Creates a new nested transformer
15
- #
16
- # @param attribute [Attribute::Base] The attribute with nested structure
17
- def initialize(attribute)
18
- @attribute = attribute
19
- end
20
-
21
- # Transforms nested attribute value (object or array)
22
- # Returns original value if nil or not nested
23
- #
24
- # @param value [Object] The value to transform
25
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
26
- # @return [Object] Transformed value
27
- def transform(value, root_data = {})
28
- return value if value.nil?
29
-
30
- case attribute.type
31
- when :object
32
- transform_object(value, root_data)
33
- when :array
34
- transform_array(value, root_data)
35
- else
36
- value
37
- end
38
- end
39
-
40
- private
41
-
42
- # Transforms object (hash) value
43
- #
44
- # @param value [Hash] The hash to transform
45
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
46
- # @return [Hash] Transformed hash
47
- def transform_object(value, root_data = {})
48
- return value unless attribute.nested?
49
-
50
- transformer = ObjectTransformer.new(attribute)
51
- transformer.transform(value, root_data)
52
- end
53
-
54
- # Transforms array value
55
- #
56
- # @param value [Array] The array to transform
57
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
58
- # @return [Array] Transformed array
59
- def transform_array(value, root_data = {})
60
- return value unless attribute.nested?
61
-
62
- transformer = ArrayTransformer.new(attribute)
63
- transformer.transform(value, root_data)
64
- end
65
-
66
- # Transforms object (hash) with nested attributes
67
- class ObjectTransformer
68
- attr_reader :attribute
69
-
70
- # Creates a new object transformer
71
- #
72
- # @param attribute [Attribute::Base] The object-type attribute
73
- def initialize(attribute)
74
- @attribute = attribute
75
- end
76
-
77
- # Transforms hash by processing all nested attributes
78
- #
79
- # @param value [Hash] The source hash
80
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
81
- # @return [Hash] Transformed hash with processed attributes
82
- def transform(value, root_data = {})
83
- transformed = {}
84
-
85
- attribute.collection_of_attributes.each do |nested_attribute|
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)
90
- end
91
-
92
- transformed
93
- end
94
-
95
- private
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
-
178
- # Processes a single nested attribute
179
- # Validates, transforms, and adds to target hash
180
- #
181
- # @param nested_attribute [Attribute::Base] Attribute to process
182
- # @param source_hash [Hash] Source data
183
- # @param target_hash [Hash] Target hash to populate
184
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
185
- # @return [void]
186
- def process_attribute(nested_attribute, source_hash, target_hash, root_data = {}) # rubocop:disable Metrics/MethodLength
187
- source_name = nested_attribute.name
188
- nested_value = source_hash.fetch(source_name, nil)
189
-
190
- validator = AttributeValidator.new(nested_attribute)
191
- validator.validate_schema!
192
-
193
- transformed_value = if nested_attribute.nested?
194
- nested_transformer = NestedTransformer.new(nested_attribute)
195
- validator.validate_type!(nested_value) unless nested_value.nil?
196
- validator.validate_required!(nested_value)
197
- nested_transformer.transform(nested_value, root_data)
198
- else
199
- validator.validate_value!(nested_value)
200
- validator.transform_value(nested_value, root_data)
201
- end
202
-
203
- target_name = validator.target_name
204
- target_hash[target_name] = transformed_value
205
- end
206
- end
207
-
208
- # Transforms array with nested attributes
209
- class ArrayTransformer # rubocop:disable Metrics/ClassLength
210
- SELF_OBJECT = :_self
211
- private_constant :SELF_OBJECT
212
-
213
- attr_reader :attribute
214
-
215
- # Creates a new array transformer
216
- #
217
- # @param attribute [Attribute::Base] The array-type attribute
218
- def initialize(attribute)
219
- @attribute = attribute
220
- end
221
-
222
- # Transforms array by processing each element
223
- # Handles both simple arrays (:_self) and complex arrays (objects)
224
- #
225
- # @param value [Array] The source array
226
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
227
- # @return [Array] Transformed array
228
- def transform(value, root_data = {})
229
- value.each_with_index.map do |item, index|
230
- if simple_array?
231
- transform_simple_element(item, index, root_data)
232
- else
233
- transform_array_item(item, index, root_data)
234
- end
235
- end
236
- end
237
-
238
- private
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
-
321
- # Checks if this is a simple array (primitive values)
322
- #
323
- # @return [Boolean] True if array contains primitive values with :_self attribute
324
- def simple_array?
325
- attribute.collection_of_attributes.size == 1 &&
326
- attribute.collection_of_attributes.first.name == SELF_OBJECT
327
- end
328
-
329
- # Transforms a simple array element (primitive value)
330
- # Validates and applies transformations to the element
331
- #
332
- # @param item [Object] Array element to transform
333
- # @param index [Integer] Element index for error messages
334
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
335
- # @raise [Treaty::Exceptions::Validation] If validation fails
336
- # @return [Object] Transformed element value
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)
340
- validator.validate_schema!
341
-
342
- begin
343
- validator.validate_value!(item)
344
- validator.transform_value(item, root_data)
345
- rescue Treaty::Exceptions::Validation => e
346
- raise Treaty::Exceptions::Validation,
347
- I18n.t(
348
- "treaty.attributes.validators.nested.array.element_validation_error",
349
- attribute: attribute.name,
350
- index:,
351
- errors: e.message
352
- )
353
- end
354
- end
355
-
356
- # Transforms a complex array element (hash object)
357
- #
358
- # @param item [Hash] Array element to transform
359
- # @param index [Integer] Element index for error messages
360
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
361
- # @raise [Treaty::Exceptions::Validation] If item is not a Hash
362
- # @return [Hash] Transformed hash
363
- def transform_array_item(item, index, root_data = {}) # rubocop:disable Metrics/MethodLength
364
- unless item.is_a?(Hash)
365
- raise Treaty::Exceptions::Validation,
366
- I18n.t(
367
- "treaty.attributes.validators.nested.array.element_type_error",
368
- attribute: attribute.name,
369
- index:,
370
- actual: item.class
371
- )
372
- end
373
-
374
- transformed = {}
375
-
376
- attribute.collection_of_attributes.each do |nested_attribute|
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)
381
- end
382
-
383
- transformed
384
- end
385
-
386
- # Processes a single nested attribute in array element
387
- # Validates, transforms, and adds to target hash
388
- #
389
- # @param nested_attribute [Attribute::Base] Attribute to process
390
- # @param source_hash [Hash] Source data
391
- # @param target_hash [Hash] Target hash to populate
392
- # @param index [Integer] Array index for error messages
393
- # @param root_data [Hash] Full raw data from root level (for computed modifier)
394
- # @raise [Treaty::Exceptions::Validation] If validation fails
395
- # @return [void]
396
- def process_attribute(nested_attribute, source_hash, target_hash, index, root_data = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
397
- source_name = nested_attribute.name
398
- nested_value = source_hash.fetch(source_name, nil)
399
-
400
- validator = AttributeValidator.new(nested_attribute)
401
- validator.validate_schema!
402
-
403
- begin
404
- transformed_value = if nested_attribute.nested?
405
- nested_transformer = NestedTransformer.new(nested_attribute)
406
- validator.validate_type!(nested_value) unless nested_value.nil?
407
- validator.validate_required!(nested_value)
408
- nested_transformer.transform(nested_value, root_data)
409
- else
410
- validator.validate_value!(nested_value)
411
- validator.transform_value(nested_value, root_data)
412
- end
413
- rescue Treaty::Exceptions::Validation => e
414
- raise Treaty::Exceptions::Validation,
415
- I18n.t(
416
- "treaty.attributes.validators.nested.array.attribute_error",
417
- attribute: attribute.name,
418
- index:,
419
- message: e.message
420
- )
421
- end
422
-
423
- target_name = validator.target_name
424
- target_hash[target_name] = transformed_value
425
- end
426
- end
427
- end
428
- end
429
- end
430
- end
@@ -1,260 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Treaty
4
- module Attribute
5
- module Validation
6
- module Orchestrator
7
- # Base orchestrator for validating and transforming data according to treaty definitions.
8
- #
9
- # ## Purpose
10
- #
11
- # Coordinates the validation and transformation of request/response data for a specific
12
- # API version. Processes all attributes, applying validations and transformations
13
- # defined in the treaty DSL.
14
- #
15
- # ## Responsibilities
16
- #
17
- # 1. **Attribute Processing** - Iterates through all defined attributes
18
- # 2. **Attribute Validation** - Validates each attribute's value
19
- # 3. **Data Transformation** - Transforms values (defaults, renaming)
20
- # 4. **Nested Handling** - Delegates nested structures to NestedTransformer
21
- # 5. **Result Assembly** - Builds final transformed data structure
22
- #
23
- # ## Usage
24
- #
25
- # Subclasses must implement:
26
- # - `collection_of_attributes` - Returns attributes for this context (request/response)
27
- #
28
- # Example:
29
- # orchestrator = Request::Orchestrator.new(version_factory: factory, data: params)
30
- # validated_data = orchestrator.validate!
31
- #
32
- # ## Special Case: object :_self
33
- #
34
- # - Normal object: `{ object_name: { ... } }`
35
- # - Self object (`:_self`): Attributes merged directly into parent
36
- #
37
- # ## Architecture
38
- #
39
- # Uses:
40
- # - `AttributeValidator` - Validates individual attributes
41
- # - `NestedTransformer` - Handles nested objects and arrays
42
- #
43
- # The design separates concerns:
44
- # - Orchestrator: High-level flow and attribute iteration
45
- # - Validator: Individual attribute validation
46
- # - Transformer: Nested structure transformation
47
- class Base
48
- SELF_OBJECT = :_self
49
- private_constant :SELF_OBJECT
50
-
51
- attr_reader :version_factory, :data
52
-
53
- # Class-level factory method for validation
54
- # Creates instance and calls validate!
55
- #
56
- # @param args [Hash] Arguments passed to initialize
57
- # @return [Hash] Validated and transformed data
58
- def self.validate!(...)
59
- new(...).validate!
60
- end
61
-
62
- # Creates a new orchestrator instance
63
- #
64
- # @param version_factory [VersionFactory] Factory containing version info
65
- # @param data [Hash] Data to validate and transform (default: {})
66
- def initialize(version_factory:, data: {})
67
- @version_factory = version_factory
68
- @data = data
69
- end
70
-
71
- # Validates and transforms all attributes
72
- # Iterates through attributes, processes them, handles :_self objects
73
- # Skips attributes with false conditional (if/unless option)
74
- #
75
- # @return [Hash] Transformed data with all attributes processed
76
- def validate! # rubocop:disable Metrics/MethodLength
77
- transformed_data = {}
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
-
83
- transformed_value = validate_and_transform_attribute!(attribute)
84
-
85
- if attribute.name == SELF_OBJECT && attribute.type == :object
86
- # For object :_self, merge nested attributes to root
87
- transformed_data.merge!(transformed_value)
88
- else
89
- transformed_data[attribute.name] = transformed_value
90
- end
91
- end
92
-
93
- transformed_data
94
- end
95
-
96
- private
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
-
141
- # Returns collection of attributes for this context
142
- # Must be implemented in subclasses
143
- #
144
- # @raise [Treaty::Exceptions::Validation] If not implemented
145
- # @return [Treaty::Attribute::Collection] Collection of attributes
146
- def collection_of_attributes
147
- raise Treaty::Exceptions::Validation,
148
- I18n.t("treaty.attributes.validators.nested.orchestrator.collection_not_implemented")
149
- end
150
-
151
- # Gets cached validators for attributes or builds them
152
- #
153
- # @return [Hash] Hash of attribute => validator
154
- def validators_for_attributes
155
- @validators_for_attributes ||= build_validators_for_attributes
156
- end
157
-
158
- # Builds validators for all attributes
159
- #
160
- # @return [Hash] Hash of attribute => validator
161
- def build_validators_for_attributes
162
- collection_of_attributes.each_with_object({}) do |attribute, cache|
163
- validator = AttributeValidator.new(attribute)
164
- validator.validate_schema!
165
- cache[attribute] = validator
166
- end
167
- end
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
-
203
- # Validates and transforms a single attribute
204
- # Handles both nested and regular attributes
205
- #
206
- # @param attribute [Attribute] The attribute to process
207
- # @return [Object] Transformed attribute value
208
- def validate_and_transform_attribute!(attribute) # rubocop:disable Metrics/MethodLength
209
- validator = validators_for_attributes.fetch(attribute)
210
-
211
- # For :_self object, get data from root; otherwise from attribute key
212
- value = if attribute.name == SELF_OBJECT && attribute.type == :object
213
- data
214
- else
215
- data.fetch(attribute.name, nil)
216
- end
217
-
218
- if attribute.nested?
219
- validate_and_transform_nested(attribute, value, validator)
220
- else
221
- validator.validate_value!(value)
222
- validator.transform_value(value, data)
223
- end
224
- end
225
-
226
- # Validates and transforms nested attribute (object/array)
227
- # Delegates transformation to NestedTransformer
228
- #
229
- # @param attribute [Attribute::Base] The nested attribute
230
- # @param value [Object, nil] The value to validate and transform
231
- # @param validator [AttributeValidator] The validator instance
232
- # @return [Object, nil] Transformed nested value or nil
233
- #
234
- # @note Flow control:
235
- # - If value is nil and attribute is required → validate_required! raises exception
236
- # - If value is nil and attribute is optional → validate_required! does nothing, returns nil
237
- # - If value is not nil → proceeds to transformation (value guaranteed non-nil)
238
- def validate_and_transform_nested(attribute, value, validator)
239
- # Step 1: Validate type if value is present
240
- validator.validate_type!(value) unless value.nil?
241
-
242
- # Step 2: Validate required constraint
243
- # This will raise an exception if attribute is required and value is nil
244
- validator.validate_required!(value)
245
-
246
- # Step 3: Early return for nil values
247
- # Only reaches here if attribute is optional and value is nil
248
- return nil if value.nil?
249
-
250
- # Step 4: Transform non-nil value
251
- # At this point, value is guaranteed to be non-nil
252
- # Pass full root data as context for computed modifiers
253
- transformer = NestedTransformer.new(attribute)
254
- transformer.transform(value, data)
255
- end
256
- end
257
- end
258
- end
259
- end
260
- end