treaty 0.18.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/config/locales/en.yml +3 -3
- data/lib/treaty/engine.rb +1 -1
- data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
- data/lib/treaty/entity/attribute/base.rb +184 -0
- data/lib/treaty/entity/attribute/builder/base.rb +275 -0
- data/lib/treaty/entity/attribute/collection.rb +67 -0
- data/lib/treaty/entity/attribute/dsl.rb +92 -0
- data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
- data/lib/treaty/entity/attribute/option/base.rb +190 -0
- data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
- data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
- data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
- data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
- data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
- data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
- data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
- data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
- data/lib/treaty/entity/attribute/option/registry.rb +157 -0
- data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
- data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
- data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
- data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
- data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
- data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
- data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
- data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
- data/lib/treaty/entity/attribute/validation/base.rb +76 -0
- data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
- data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
- data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
- data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
- data/lib/treaty/entity/base.rb +90 -0
- data/lib/treaty/entity/builder.rb +44 -0
- data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
- data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
- data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
- data/lib/treaty/entity.rb +7 -79
- data/lib/treaty/request/attribute/attribute.rb +1 -1
- data/lib/treaty/request/attribute/builder.rb +2 -2
- data/lib/treaty/request/entity.rb +1 -1
- data/lib/treaty/request/factory.rb +5 -5
- data/lib/treaty/request/validator.rb +1 -1
- data/lib/treaty/response/attribute/attribute.rb +1 -1
- data/lib/treaty/response/attribute/builder.rb +2 -2
- data/lib/treaty/response/entity.rb +1 -1
- data/lib/treaty/response/factory.rb +5 -5
- data/lib/treaty/response/validator.rb +1 -1
- data/lib/treaty/version.rb +1 -1
- metadata +35 -34
- data/lib/treaty/attribute/base.rb +0 -182
- data/lib/treaty/attribute/builder/base.rb +0 -273
- data/lib/treaty/attribute/collection.rb +0 -65
- data/lib/treaty/attribute/dsl.rb +0 -90
- data/lib/treaty/attribute/entity/builder.rb +0 -46
- data/lib/treaty/attribute/helper_mapper.rb +0 -72
- data/lib/treaty/attribute/option/base.rb +0 -188
- data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
- data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
- data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
- data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
- data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
- data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
- data/lib/treaty/attribute/option/registry.rb +0 -155
- data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
- data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
- data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
- data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
- data/lib/treaty/attribute/option_normalizer.rb +0 -166
- data/lib/treaty/attribute/option_orchestrator.rb +0 -190
- data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
- data/lib/treaty/attribute/validation/base.rb +0 -74
- data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
- data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
- data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
- 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
|