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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/config/locales/en.yml +17 -0
- data/lib/treaty/attribute/option/base.rb +14 -2
- data/lib/treaty/attribute/option/conditionals/base.rb +90 -0
- data/lib/treaty/attribute/option/conditionals/if_conditional.rb +134 -0
- data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +151 -0
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +2 -1
- data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +2 -1
- data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +126 -0
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +2 -2
- data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +2 -1
- data/lib/treaty/attribute/option/registry.rb +18 -2
- data/lib/treaty/attribute/option/registry_initializer.rb +24 -6
- data/lib/treaty/attribute/option_orchestrator.rb +3 -2
- data/lib/treaty/attribute/validation/attribute_validator.rb +3 -2
- data/lib/treaty/attribute/validation/nested_transformer.rb +202 -25
- data/lib/treaty/attribute/validation/orchestrator/base.rb +85 -3
- data/lib/treaty/result.rb +4 -3
- data/lib/treaty/version.rb +1 -1
- data/lib/treaty/versions/workspace.rb +2 -1
- metadata +5 -1
|
@@ -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
|
|
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
|
|
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. **
|
|
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
|
-
# - `:
|
|
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(:
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
validator = AttributeValidator.new(
|
|
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
|
-
|
|
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
|