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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/config/locales/en.yml +6 -2
- 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 -75
- data/lib/treaty/request/attribute/attribute.rb +1 -1
- data/lib/treaty/request/attribute/builder.rb +24 -1
- data/lib/treaty/request/entity.rb +1 -1
- data/lib/treaty/request/factory.rb +6 -6
- data/lib/treaty/request/validator.rb +1 -1
- data/lib/treaty/response/attribute/attribute.rb +1 -1
- data/lib/treaty/response/attribute/builder.rb +24 -1
- data/lib/treaty/response/entity.rb +1 -1
- data/lib/treaty/response/factory.rb +6 -6
- 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 -143
- data/lib/treaty/attribute/collection.rb +0 -65
- data/lib/treaty/attribute/dsl.rb +0 -90
- data/lib/treaty/attribute/entity/builder.rb +0 -23
- 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,283 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
module Modifiers
|
|
7
|
-
# Converts attribute values between different types automatically.
|
|
8
|
-
#
|
|
9
|
-
# ## Usage Examples
|
|
10
|
-
#
|
|
11
|
-
# Simple mode:
|
|
12
|
-
# string :created_at, cast: :datetime
|
|
13
|
-
# datetime :timestamp, cast: :string
|
|
14
|
-
# integer :active, cast: :boolean
|
|
15
|
-
#
|
|
16
|
-
# Advanced mode with custom error message:
|
|
17
|
-
# string :created_at, cast: {
|
|
18
|
-
# to: :datetime,
|
|
19
|
-
# message: "Invalid date format"
|
|
20
|
-
# }
|
|
21
|
-
#
|
|
22
|
-
# ## Use Cases
|
|
23
|
-
#
|
|
24
|
-
# 1. **Request type conversion**:
|
|
25
|
-
# ```ruby
|
|
26
|
-
# request do
|
|
27
|
-
# string :created_at, cast: :datetime
|
|
28
|
-
# end
|
|
29
|
-
# # Input: { created_at: "2024-01-15T10:30:00Z" }
|
|
30
|
-
# # Service receives: { created_at: DateTime object }
|
|
31
|
-
# ```
|
|
32
|
-
#
|
|
33
|
-
# 2. **Response type conversion**:
|
|
34
|
-
# ```ruby
|
|
35
|
-
# response 200 do
|
|
36
|
-
# datetime :created_at, cast: :string
|
|
37
|
-
# end
|
|
38
|
-
# # Service returns: { created_at: DateTime object }
|
|
39
|
-
# # Output: { created_at: "2024-01-15T10:30:00Z" }
|
|
40
|
-
# ```
|
|
41
|
-
#
|
|
42
|
-
# 3. **Unix timestamp conversion**:
|
|
43
|
-
# ```ruby
|
|
44
|
-
# integer :timestamp, cast: :datetime
|
|
45
|
-
# datetime :created_at, cast: :integer
|
|
46
|
-
# ```
|
|
47
|
-
#
|
|
48
|
-
# ## Supported Conversions
|
|
49
|
-
#
|
|
50
|
-
# ### From Integer
|
|
51
|
-
# - integer -> string: Converts to string representation
|
|
52
|
-
# - integer -> boolean: 0 = false, non-zero = true
|
|
53
|
-
# - integer -> date: Treats as Unix timestamp, converts to date
|
|
54
|
-
# - integer -> time: Treats as Unix timestamp
|
|
55
|
-
# - integer -> datetime: Treats as Unix timestamp, converts to datetime
|
|
56
|
-
#
|
|
57
|
-
# ### From String
|
|
58
|
-
# - string -> integer: Parses integer from string
|
|
59
|
-
# - string -> boolean: Parses truthy/falsy strings (true/false, yes/no, 1/0, on/off)
|
|
60
|
-
# - string -> date: Parses date string
|
|
61
|
-
# - string -> time: Parses time string
|
|
62
|
-
# - string -> datetime: Parses datetime string (ISO8601, RFC3339, etc.)
|
|
63
|
-
#
|
|
64
|
-
# ### From Boolean
|
|
65
|
-
# - boolean -> string: Converts to "true" or "false"
|
|
66
|
-
# - boolean -> integer: true = 1, false = 0
|
|
67
|
-
#
|
|
68
|
-
# ### From Date
|
|
69
|
-
# - date -> string: Converts to ISO8601 format
|
|
70
|
-
# - date -> integer: Converts to Unix timestamp
|
|
71
|
-
# - date -> time: Converts to Time at midnight
|
|
72
|
-
# - date -> datetime: Converts to DateTime at midnight
|
|
73
|
-
#
|
|
74
|
-
# ### From Time
|
|
75
|
-
# - time -> string: Converts to ISO8601 format
|
|
76
|
-
# - time -> integer: Converts to Unix timestamp
|
|
77
|
-
# - time -> date: Converts to Date
|
|
78
|
-
# - time -> datetime: Converts to DateTime
|
|
79
|
-
#
|
|
80
|
-
# ### From DateTime
|
|
81
|
-
# - datetime -> string: Converts to ISO8601 format
|
|
82
|
-
# - datetime -> integer: Converts to Unix timestamp
|
|
83
|
-
# - datetime -> date: Converts to Date
|
|
84
|
-
# - datetime -> time: Converts to Time
|
|
85
|
-
#
|
|
86
|
-
# ## Important Notes
|
|
87
|
-
#
|
|
88
|
-
# - Cast option only works with scalar types (integer, string, boolean, date, time, datetime)
|
|
89
|
-
# - Array and Object types are not supported for casting
|
|
90
|
-
# - Casting to the same type is allowed (no-op)
|
|
91
|
-
# - Nil values are not transformed (handled by RequiredValidator)
|
|
92
|
-
# - All conversion errors are caught and re-raised as Validation errors
|
|
93
|
-
#
|
|
94
|
-
# ## Error Handling
|
|
95
|
-
#
|
|
96
|
-
# If conversion fails (e.g., invalid date string, non-numeric string to integer),
|
|
97
|
-
# the error is caught and converted to a Treaty::Exceptions::Validation error.
|
|
98
|
-
#
|
|
99
|
-
# ## Advanced Mode
|
|
100
|
-
#
|
|
101
|
-
# Schema format: `{ to: :target_type, message: "Custom error" }`
|
|
102
|
-
# Note: Uses `:to` key instead of the default `:is` key.
|
|
103
|
-
class CastModifier < Treaty::Attribute::Option::Base # rubocop:disable Metrics/ClassLength
|
|
104
|
-
# Types that support casting (scalar types only)
|
|
105
|
-
ALLOWED_CAST_TYPES = %i[integer string boolean date time datetime].freeze
|
|
106
|
-
|
|
107
|
-
# Validates that cast option is correctly configured
|
|
108
|
-
#
|
|
109
|
-
# @raise [Treaty::Exceptions::Validation] If cast configuration is invalid
|
|
110
|
-
# @return [void]
|
|
111
|
-
def validate_schema! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
112
|
-
# If option_schema is nil, cast is not used for this attribute
|
|
113
|
-
return if @option_schema.nil?
|
|
114
|
-
|
|
115
|
-
target_type = option_value
|
|
116
|
-
|
|
117
|
-
# Validate that target type is a Symbol
|
|
118
|
-
unless target_type.is_a?(Symbol)
|
|
119
|
-
raise Treaty::Exceptions::Validation,
|
|
120
|
-
I18n.t(
|
|
121
|
-
"treaty.attributes.modifiers.cast.invalid_type",
|
|
122
|
-
attribute: @attribute_name,
|
|
123
|
-
type: target_type.class
|
|
124
|
-
)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Validate that source type supports casting
|
|
128
|
-
unless ALLOWED_CAST_TYPES.include?(@attribute_type)
|
|
129
|
-
raise Treaty::Exceptions::Validation,
|
|
130
|
-
I18n.t(
|
|
131
|
-
"treaty.attributes.modifiers.cast.source_not_supported",
|
|
132
|
-
attribute: @attribute_name,
|
|
133
|
-
source_type: @attribute_type,
|
|
134
|
-
allowed: ALLOWED_CAST_TYPES.join(", ")
|
|
135
|
-
)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Validate that target type is allowed
|
|
139
|
-
unless ALLOWED_CAST_TYPES.include?(target_type)
|
|
140
|
-
raise Treaty::Exceptions::Validation,
|
|
141
|
-
I18n.t(
|
|
142
|
-
"treaty.attributes.modifiers.cast.target_not_supported",
|
|
143
|
-
attribute: @attribute_name,
|
|
144
|
-
target_type:,
|
|
145
|
-
allowed: ALLOWED_CAST_TYPES.join(", ")
|
|
146
|
-
)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Validate that conversion from source to target is supported
|
|
150
|
-
return if conversion_supported?(@attribute_type, target_type)
|
|
151
|
-
|
|
152
|
-
raise Treaty::Exceptions::Validation,
|
|
153
|
-
I18n.t(
|
|
154
|
-
"treaty.attributes.modifiers.cast.conversion_not_supported",
|
|
155
|
-
attribute: @attribute_name,
|
|
156
|
-
from: @attribute_type,
|
|
157
|
-
to: target_type
|
|
158
|
-
)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Applies type conversion to the value
|
|
162
|
-
# Skips conversion for nil values (handled by RequiredValidator)
|
|
163
|
-
#
|
|
164
|
-
# @param value [Object] The current value
|
|
165
|
-
# @param _root_data [Hash] Unused root data parameter
|
|
166
|
-
# @return [Object] Converted value
|
|
167
|
-
def transform_value(value, _root_data = {}) # rubocop:disable Metrics/MethodLength
|
|
168
|
-
return value if value.nil? # Cast doesn't modify nil, required validator handles it.
|
|
169
|
-
|
|
170
|
-
target_type = option_value
|
|
171
|
-
conversion_lambda = conversion_matrix.dig(@attribute_type, target_type)
|
|
172
|
-
|
|
173
|
-
# Call conversion lambda
|
|
174
|
-
conversion_lambda.call(value:)
|
|
175
|
-
rescue StandardError => e
|
|
176
|
-
attributes = {
|
|
177
|
-
attribute: @attribute_name,
|
|
178
|
-
from: @attribute_type,
|
|
179
|
-
to: target_type,
|
|
180
|
-
value:,
|
|
181
|
-
error: e.message
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
# Catch all exceptions from conversion execution
|
|
185
|
-
error_message = resolve_custom_message(**attributes) || I18n.t(
|
|
186
|
-
"treaty.attributes.modifiers.cast.conversion_error",
|
|
187
|
-
**attributes
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
raise Treaty::Exceptions::Validation, error_message
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
protected
|
|
194
|
-
|
|
195
|
-
# Override value_key to use :to instead of :is
|
|
196
|
-
# This makes advanced mode syntax: cast: { to: :datetime }
|
|
197
|
-
#
|
|
198
|
-
# @return [Symbol] The key :to
|
|
199
|
-
def value_key
|
|
200
|
-
:to
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
private
|
|
204
|
-
|
|
205
|
-
# Checks if conversion from source type to target type is supported
|
|
206
|
-
#
|
|
207
|
-
# @param from_type [Symbol] Source type
|
|
208
|
-
# @param to_type [Symbol] Target type
|
|
209
|
-
# @return [Boolean] True if conversion is supported
|
|
210
|
-
def conversion_supported?(from_type, to_type)
|
|
211
|
-
conversion_matrix.dig(from_type, to_type).present?
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Matrix of all supported type conversions
|
|
215
|
-
# Maps from_type => to_type => conversion_lambda
|
|
216
|
-
#
|
|
217
|
-
# @return [Hash] Conversion matrix
|
|
218
|
-
def conversion_matrix # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
219
|
-
@conversion_matrix ||= {
|
|
220
|
-
integer: {
|
|
221
|
-
integer: ->(value:) { value }, # No-op for same type
|
|
222
|
-
string: ->(value:) { value.to_s },
|
|
223
|
-
boolean: ->(value:) { value != 0 },
|
|
224
|
-
date: ->(value:) { Time.at(value).to_date },
|
|
225
|
-
time: ->(value:) { Time.at(value) },
|
|
226
|
-
datetime: ->(value:) { Time.at(value).to_datetime }
|
|
227
|
-
},
|
|
228
|
-
string: {
|
|
229
|
-
string: ->(value:) { value }, # No-op for same type
|
|
230
|
-
integer: ->(value:) { Integer(value) },
|
|
231
|
-
boolean: ->(value:) { parse_boolean(value) },
|
|
232
|
-
date: ->(value:) { Date.parse(value) },
|
|
233
|
-
time: ->(value:) { Time.parse(value) },
|
|
234
|
-
datetime: ->(value:) { DateTime.parse(value) }
|
|
235
|
-
},
|
|
236
|
-
boolean: {
|
|
237
|
-
boolean: ->(value:) { value }, # No-op for same type
|
|
238
|
-
string: ->(value:) { value.to_s },
|
|
239
|
-
integer: ->(value:) { value ? 1 : 0 }
|
|
240
|
-
},
|
|
241
|
-
date: {
|
|
242
|
-
date: ->(value:) { value }, # No-op for same type
|
|
243
|
-
string: ->(value:) { value.iso8601 },
|
|
244
|
-
integer: ->(value:) { value.to_time.to_i },
|
|
245
|
-
time: ->(value:) { value.to_time },
|
|
246
|
-
datetime: ->(value:) { value.to_datetime }
|
|
247
|
-
},
|
|
248
|
-
time: {
|
|
249
|
-
time: ->(value:) { value }, # No-op for same type
|
|
250
|
-
string: ->(value:) { value.iso8601 },
|
|
251
|
-
integer: ->(value:) { value.to_i },
|
|
252
|
-
date: ->(value:) { value.to_date },
|
|
253
|
-
datetime: ->(value:) { value.to_datetime }
|
|
254
|
-
},
|
|
255
|
-
datetime: {
|
|
256
|
-
datetime: ->(value:) { value }, # No-op for same type
|
|
257
|
-
string: ->(value:) { value.iso8601 },
|
|
258
|
-
integer: ->(value:) { value.to_i },
|
|
259
|
-
date: ->(value:) { value.to_date },
|
|
260
|
-
time: ->(value:) { value.to_time }
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Parses a string value into a boolean
|
|
266
|
-
# Recognizes: true/false, yes/no, 1/0, on/off (case-insensitive)
|
|
267
|
-
#
|
|
268
|
-
# @param value [String] The string value to parse
|
|
269
|
-
# @return [Boolean] Parsed boolean value
|
|
270
|
-
# @raise [ArgumentError] If string is not a recognized boolean value
|
|
271
|
-
def parse_boolean(value)
|
|
272
|
-
normalized = value.to_s.downcase.strip
|
|
273
|
-
|
|
274
|
-
return true if %w[true 1 yes on].include?(normalized)
|
|
275
|
-
return false if %w[false 0 no off].include?(normalized)
|
|
276
|
-
|
|
277
|
-
raise ArgumentError, "Cannot convert '#{value}' to boolean"
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
end
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
module Modifiers
|
|
7
|
-
# Computes attribute values from all available raw data.
|
|
8
|
-
#
|
|
9
|
-
# ## Key Difference from Transform
|
|
10
|
-
#
|
|
11
|
-
# - `transform:` receives only `value:` (the current attribute's value)
|
|
12
|
-
# - `computed:` receives `**attributes` (ALL raw data from root level)
|
|
13
|
-
#
|
|
14
|
-
# ## Usage Examples
|
|
15
|
-
#
|
|
16
|
-
# Simple mode:
|
|
17
|
-
# string :full_name, computed: ->(**attrs) {
|
|
18
|
-
# "#{attrs.dig(:user, :first_name)} #{attrs.dig(:user, :last_name)}"
|
|
19
|
-
# }
|
|
20
|
-
#
|
|
21
|
-
# Advanced mode with custom error message:
|
|
22
|
-
# string :full_name, computed: {
|
|
23
|
-
# is: ->(**attrs) { "#{attrs.dig(:user, :first_name)} #{attrs.dig(:user, :last_name)}" },
|
|
24
|
-
# message: "Failed to compute full name"
|
|
25
|
-
# }
|
|
26
|
-
#
|
|
27
|
-
# ## Use Cases
|
|
28
|
-
#
|
|
29
|
-
# 1. **Derived fields (full name from parts)**:
|
|
30
|
-
# ```ruby
|
|
31
|
-
# response 200 do
|
|
32
|
-
# object :user do
|
|
33
|
-
# string :first_name
|
|
34
|
-
# string :last_name
|
|
35
|
-
# string :full_name, computed: ->(**attrs) {
|
|
36
|
-
# "#{attrs.dig(:user, :first_name)} #{attrs.dig(:user, :last_name)}"
|
|
37
|
-
# }
|
|
38
|
-
# end
|
|
39
|
-
# end
|
|
40
|
-
# ```
|
|
41
|
-
#
|
|
42
|
-
# 2. **Calculated values (word count)**:
|
|
43
|
-
# ```ruby
|
|
44
|
-
# response 200 do
|
|
45
|
-
# object :post do
|
|
46
|
-
# string :content
|
|
47
|
-
# integer :word_count, computed: ->(**attrs) {
|
|
48
|
-
# attrs.dig(:post, :content).to_s.split.size
|
|
49
|
-
# }
|
|
50
|
-
# end
|
|
51
|
-
# end
|
|
52
|
-
# ```
|
|
53
|
-
#
|
|
54
|
-
# 3. **Cross-object computations**:
|
|
55
|
-
# ```ruby
|
|
56
|
-
# response 200 do
|
|
57
|
-
# object :order do
|
|
58
|
-
# integer :quantity
|
|
59
|
-
# integer :unit_price
|
|
60
|
-
# integer :total, computed: ->(**attrs) {
|
|
61
|
-
# attrs.dig(:order, :quantity).to_i * attrs.dig(:order, :unit_price).to_i
|
|
62
|
-
# }
|
|
63
|
-
# end
|
|
64
|
-
# end
|
|
65
|
-
# ```
|
|
66
|
-
#
|
|
67
|
-
# ## Important Notes
|
|
68
|
-
#
|
|
69
|
-
# - Lambda must accept `**attributes` (named argument splat)
|
|
70
|
-
# - Receives full raw data from root level (not just current object)
|
|
71
|
-
# - **Always computes** - ignores any existing value, result replaces everything
|
|
72
|
-
# - All exceptions raised in lambda are caught and re-raised as Validation errors
|
|
73
|
-
# - Computation is applied during Phase 3 (transformation phase)
|
|
74
|
-
# - Executes FIRST in modifier chain: computed -> transform -> cast -> default -> as
|
|
75
|
-
#
|
|
76
|
-
# ## Advanced Mode
|
|
77
|
-
#
|
|
78
|
-
# Schema format: `{ is: lambda, message: nil }`
|
|
79
|
-
class ComputedModifier < Treaty::Attribute::Option::Base
|
|
80
|
-
# Validates that computed value is a lambda
|
|
81
|
-
#
|
|
82
|
-
# @raise [Treaty::Exceptions::Validation] If computed is not a Proc/lambda
|
|
83
|
-
# @return [void]
|
|
84
|
-
def validate_schema!
|
|
85
|
-
computed_lambda = option_value
|
|
86
|
-
|
|
87
|
-
return if computed_lambda.respond_to?(:call)
|
|
88
|
-
|
|
89
|
-
raise Treaty::Exceptions::Validation,
|
|
90
|
-
I18n.t(
|
|
91
|
-
"treaty.attributes.modifiers.computed.invalid_type",
|
|
92
|
-
attribute: @attribute_name,
|
|
93
|
-
type: computed_lambda.class
|
|
94
|
-
)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Computes value using the provided lambda and full root data
|
|
98
|
-
# Always executes - ignores any existing value
|
|
99
|
-
#
|
|
100
|
-
# @param _value [Object] The current value (ignored - always computes)
|
|
101
|
-
# @param root_data [Hash] Full raw data from root level
|
|
102
|
-
# @return [Object] Computed value
|
|
103
|
-
def transform_value(_value, root_data = {}) # rubocop:disable Metrics/MethodLength
|
|
104
|
-
computed_lambda = option_value
|
|
105
|
-
|
|
106
|
-
# Call lambda with full root data as named arguments
|
|
107
|
-
computed_lambda.call(**root_data)
|
|
108
|
-
rescue StandardError => e
|
|
109
|
-
attributes = {
|
|
110
|
-
attribute: @attribute_name,
|
|
111
|
-
error: e.message
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
# Catch all exceptions from lambda execution
|
|
115
|
-
error_message = resolve_custom_message(**attributes) || I18n.t(
|
|
116
|
-
"treaty.attributes.modifiers.computed.execution_error",
|
|
117
|
-
**attributes
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
raise Treaty::Exceptions::Validation, error_message
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
module Modifiers
|
|
7
|
-
# Sets default values for attributes when value is nil.
|
|
8
|
-
#
|
|
9
|
-
# ## Usage Examples
|
|
10
|
-
#
|
|
11
|
-
# Simple mode with static value:
|
|
12
|
-
# integer :limit, default: 12
|
|
13
|
-
# string :status, default: "pending"
|
|
14
|
-
# boolean :active, default: false
|
|
15
|
-
#
|
|
16
|
-
# Simple mode with dynamic value (Proc):
|
|
17
|
-
# datetime :created_at, default: -> { Time.current }
|
|
18
|
-
# string :uuid, default: -> { SecureRandom.uuid }
|
|
19
|
-
#
|
|
20
|
-
# Advanced mode:
|
|
21
|
-
# integer :limit, default: { is: 12, message: nil }
|
|
22
|
-
#
|
|
23
|
-
# ## Use Cases
|
|
24
|
-
#
|
|
25
|
-
# 1. **Response defaults** (most common):
|
|
26
|
-
# ```ruby
|
|
27
|
-
# response 200 do
|
|
28
|
-
# object :meta do
|
|
29
|
-
# integer :limit, default: 12
|
|
30
|
-
# integer :page, default: 1
|
|
31
|
-
# end
|
|
32
|
-
# end
|
|
33
|
-
# # Service returns: { meta: { page: 1 } }
|
|
34
|
-
# # Output: { meta: { page: 1, limit: 12 } }
|
|
35
|
-
# ```
|
|
36
|
-
#
|
|
37
|
-
# 2. **Request defaults**:
|
|
38
|
-
# ```ruby
|
|
39
|
-
# request do
|
|
40
|
-
# string :format, default: "json"
|
|
41
|
-
# end
|
|
42
|
-
# # Input: {}
|
|
43
|
-
# # Service receives: { format: "json" }
|
|
44
|
-
# ```
|
|
45
|
-
#
|
|
46
|
-
# ## Important Notes
|
|
47
|
-
#
|
|
48
|
-
# - Default is applied ONLY when value is nil
|
|
49
|
-
# - Empty strings, empty arrays, false are NOT replaced
|
|
50
|
-
# - Proc defaults are called at transformation time
|
|
51
|
-
# - Procs receive no arguments
|
|
52
|
-
#
|
|
53
|
-
# ## Array and Object Types
|
|
54
|
-
#
|
|
55
|
-
# NOTE: DO NOT use `default: []` or `default: {}` for array/object types!
|
|
56
|
-
# Array and object types automatically represent empty collections.
|
|
57
|
-
#
|
|
58
|
-
# Incorrect:
|
|
59
|
-
# array :tags, default: [] # Wrong! Redundant
|
|
60
|
-
# object :meta, default: {} # Wrong! Redundant
|
|
61
|
-
#
|
|
62
|
-
# Correct:
|
|
63
|
-
# array :tags # Automatically handles empty array
|
|
64
|
-
# object :meta # Automatically handles empty object
|
|
65
|
-
#
|
|
66
|
-
# ## Advanced Mode
|
|
67
|
-
#
|
|
68
|
-
# Schema format: `{ is: value_or_proc, message: nil }`
|
|
69
|
-
class DefaultModifier < Treaty::Attribute::Option::Base
|
|
70
|
-
# Validates schema (no validation needed)
|
|
71
|
-
# Default value can be any type
|
|
72
|
-
#
|
|
73
|
-
# @return [void]
|
|
74
|
-
def validate_schema!
|
|
75
|
-
# Schema structure is already normalized by OptionNormalizer.
|
|
76
|
-
# Default value can be any type, so nothing specific to validate here.
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Applies default value if current value is nil
|
|
80
|
-
# Empty strings, empty arrays, and false are NOT replaced
|
|
81
|
-
#
|
|
82
|
-
# @param value [Object] The current value
|
|
83
|
-
# @param _root_data [Hash] Unused root data parameter
|
|
84
|
-
# @return [Object] Default value if original is nil, otherwise original value
|
|
85
|
-
def transform_value(value, _root_data = {})
|
|
86
|
-
# Only apply default if value is nil
|
|
87
|
-
# Empty strings, empty arrays, false are NOT replaced
|
|
88
|
-
return value unless value.nil?
|
|
89
|
-
|
|
90
|
-
default_value = option_value
|
|
91
|
-
|
|
92
|
-
# If default value is a Proc, call it to get the value
|
|
93
|
-
if default_value.is_a?(Proc)
|
|
94
|
-
default_value.call
|
|
95
|
-
else
|
|
96
|
-
default_value
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Treaty
|
|
4
|
-
module Attribute
|
|
5
|
-
module Option
|
|
6
|
-
module Modifiers
|
|
7
|
-
# Transforms attribute values using custom lambda functions.
|
|
8
|
-
#
|
|
9
|
-
# ## Usage Examples
|
|
10
|
-
#
|
|
11
|
-
# Simple mode:
|
|
12
|
-
# integer :amount, transform: ->(value:) { value * 100 }
|
|
13
|
-
# string :title, transform: ->(value:) { value.strip.upcase }
|
|
14
|
-
#
|
|
15
|
-
# Advanced mode with custom error message:
|
|
16
|
-
# integer :amount, transform: {
|
|
17
|
-
# is: ->(value:) { value * 100 },
|
|
18
|
-
# message: "Failed to transform amount"
|
|
19
|
-
# }
|
|
20
|
-
#
|
|
21
|
-
# ## Use Cases
|
|
22
|
-
#
|
|
23
|
-
# 1. **Request transformation**:
|
|
24
|
-
# ```ruby
|
|
25
|
-
# request do
|
|
26
|
-
# integer :amount_cents, transform: ->(value:) { value * 100 }
|
|
27
|
-
# end
|
|
28
|
-
# # Input: { amount_cents: 10 }
|
|
29
|
-
# # Service receives: { amount_cents: 1000 }
|
|
30
|
-
# ```
|
|
31
|
-
#
|
|
32
|
-
# 2. **Response transformation**:
|
|
33
|
-
# ```ruby
|
|
34
|
-
# response 200 do
|
|
35
|
-
# string :title, transform: ->(value:) { value.titleize }
|
|
36
|
-
# end
|
|
37
|
-
# # Service returns: { title: "hello world" }
|
|
38
|
-
# # Output: { title: "Hello World" }
|
|
39
|
-
# ```
|
|
40
|
-
#
|
|
41
|
-
# 3. **Complex transformations**:
|
|
42
|
-
# ```ruby
|
|
43
|
-
# string :email, transform: ->(value:) { value.downcase.strip }
|
|
44
|
-
# datetime :timestamp, transform: ->(value:) { value.iso8601 }
|
|
45
|
-
# ```
|
|
46
|
-
#
|
|
47
|
-
# ## Important Notes
|
|
48
|
-
#
|
|
49
|
-
# - Lambda must accept named argument `value:`
|
|
50
|
-
# - All exceptions raised in lambda are caught and re-raised as Validation errors
|
|
51
|
-
# - Transformation is applied during Phase 3 (after validation)
|
|
52
|
-
# - Can be combined with other options (required, default, as, etc.)
|
|
53
|
-
#
|
|
54
|
-
# ## Error Handling
|
|
55
|
-
#
|
|
56
|
-
# If the lambda raises any exception, it's caught and converted to a
|
|
57
|
-
# Treaty::Exceptions::Validation with appropriate error message.
|
|
58
|
-
#
|
|
59
|
-
# ## Advanced Mode
|
|
60
|
-
#
|
|
61
|
-
# Schema format: `{ is: lambda, message: nil }`
|
|
62
|
-
class TransformModifier < Treaty::Attribute::Option::Base
|
|
63
|
-
# Validates that transform value is a lambda
|
|
64
|
-
#
|
|
65
|
-
# @raise [Treaty::Exceptions::Validation] If transform is not a Proc/lambda
|
|
66
|
-
# @return [void]
|
|
67
|
-
def validate_schema!
|
|
68
|
-
transform_lambda = option_value
|
|
69
|
-
|
|
70
|
-
return if transform_lambda.respond_to?(:call)
|
|
71
|
-
|
|
72
|
-
raise Treaty::Exceptions::Validation,
|
|
73
|
-
I18n.t(
|
|
74
|
-
"treaty.attributes.modifiers.transform.invalid_type",
|
|
75
|
-
attribute: @attribute_name,
|
|
76
|
-
type: transform_lambda.class
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Applies transformation to the value using the provided lambda
|
|
81
|
-
# Catches all exceptions and re-raises as Validation errors
|
|
82
|
-
# Skips transformation for nil values (handled by RequiredValidator)
|
|
83
|
-
#
|
|
84
|
-
# @param value [Object] The current value
|
|
85
|
-
# @param _root_data [Hash] Unused root data parameter
|
|
86
|
-
# @return [Object] Transformed value
|
|
87
|
-
def transform_value(value, _root_data = {}) # rubocop:disable Metrics/MethodLength
|
|
88
|
-
return value if value.nil? # Transform doesn't modify nil, required validator handles it.
|
|
89
|
-
|
|
90
|
-
transform_lambda = option_value
|
|
91
|
-
|
|
92
|
-
# Call lambda with named argument
|
|
93
|
-
transform_lambda.call(value:)
|
|
94
|
-
rescue StandardError => e
|
|
95
|
-
attributes = {
|
|
96
|
-
attribute: @attribute_name,
|
|
97
|
-
error: e.message
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
# Catch all exceptions from lambda execution
|
|
101
|
-
error_message = resolve_custom_message(**attributes) || I18n.t(
|
|
102
|
-
"treaty.attributes.modifiers.transform.execution_error",
|
|
103
|
-
**attributes
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
raise Treaty::Exceptions::Validation, error_message
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|