treaty 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/config/locales/en.yml +6 -2
  4. data/lib/treaty/engine.rb +1 -1
  5. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  6. data/lib/treaty/entity/attribute/base.rb +184 -0
  7. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  8. data/lib/treaty/entity/attribute/collection.rb +67 -0
  9. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  10. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  11. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  12. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  13. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  14. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  15. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  16. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  17. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  18. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  19. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  20. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  21. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  22. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  23. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  24. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  25. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  26. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  27. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  28. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  29. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  30. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  31. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  32. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  33. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  34. data/lib/treaty/entity/base.rb +90 -0
  35. data/lib/treaty/entity/builder.rb +44 -0
  36. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  37. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  38. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  39. data/lib/treaty/entity.rb +7 -75
  40. data/lib/treaty/request/attribute/attribute.rb +1 -1
  41. data/lib/treaty/request/attribute/builder.rb +24 -1
  42. data/lib/treaty/request/entity.rb +1 -1
  43. data/lib/treaty/request/factory.rb +6 -6
  44. data/lib/treaty/request/validator.rb +1 -1
  45. data/lib/treaty/response/attribute/attribute.rb +1 -1
  46. data/lib/treaty/response/attribute/builder.rb +24 -1
  47. data/lib/treaty/response/entity.rb +1 -1
  48. data/lib/treaty/response/factory.rb +6 -6
  49. data/lib/treaty/response/validator.rb +1 -1
  50. data/lib/treaty/version.rb +1 -1
  51. metadata +35 -34
  52. data/lib/treaty/attribute/base.rb +0 -182
  53. data/lib/treaty/attribute/builder/base.rb +0 -143
  54. data/lib/treaty/attribute/collection.rb +0 -65
  55. data/lib/treaty/attribute/dsl.rb +0 -90
  56. data/lib/treaty/attribute/entity/builder.rb +0 -23
  57. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  58. data/lib/treaty/attribute/option/base.rb +0 -188
  59. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  60. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  61. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  62. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  63. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  64. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  65. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  66. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  67. data/lib/treaty/attribute/option/registry.rb +0 -155
  68. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  69. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  70. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  71. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  72. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  73. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  74. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  75. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  76. data/lib/treaty/attribute/validation/base.rb +0 -74
  77. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  78. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  79. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  80. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
@@ -1,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