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