treaty 0.12.0 → 0.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c10e0241b816115758a4383e524fc0f9cfb6738e95f4fddff5a1b32eb114c719
4
- data.tar.gz: 4a5bfe17af7e6efb87975770aed1a3a67fb817aeee13cdbd05c88c187407efe0
3
+ metadata.gz: 1fe35b7cf209d578b34ee7d904bdd146db4a3ac61f022a1f5f02c80bfe21875e
4
+ data.tar.gz: 4be4a2f29fbfb30115e696baeaf618eaf59fe5c2fc309a0d1ced3eff61713671
5
5
  SHA512:
6
- metadata.gz: 12b23780c22b9987d317323e723c81d5be07ff2099e74a6cf90c4eab5bb4d0d74a6f0c8b05f11f7800daf16c02d5dd7c317c2d7df770ad99c1638933e94b6297
7
- data.tar.gz: 4074fa86260a0f9e79adeaf6b41a61f70f5e568e7cbd6592b4eb8c19c270c954753db941be3490ce40944d1edc8cf9445caf75ef1de0b18e65e7939ad6ffd191
6
+ metadata.gz: d256fd30a7553ebfc015c06049c4fc0edceddaa782b3b0bda807364568009465556e72867324bc4a707d61c52da579f5a40f4324922af4349f1a7553023a9443
7
+ data.tar.gz: 4d71a4831e946faf207b9c21947531f3bc6263941f07e3a627c2e504a8c471d6aec597fe167090c0d0d49979b3b4dbf2f8e0620cfe9cd12f492286f60b5a3e7a
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  </div>
14
14
 
15
15
  > [!WARNING]
16
- > **Development Status**: Treaty is currently under active development in the 0.x version series. Breaking changes may occur between minor versions (0.x) as we refine the API and add new features. The library will stabilize with the 1.0 release. We recommend pinning to specific patch versions in your Gemfile (e.g., `gem "treaty", "~> 0.12.0"`) until the 1.0 release.
16
+ > **Development Status**: Treaty is currently under active development in the 0.x version series. Breaking changes may occur between minor versions (0.x) as we refine the API and add new features. The library will stabilize with the 1.0 release. We recommend pinning to specific patch versions in your Gemfile (e.g., `gem "treaty", "~> 0.14.0"`) until the 1.0 release.
17
17
 
18
18
  ## 📚 Documentation
19
19
 
@@ -79,7 +79,7 @@ module Posts
79
79
  string :title
80
80
  string :content
81
81
  string :summary
82
- datetime :created_at
82
+ time :created_at
83
83
  end
84
84
  end
85
85
 
@@ -17,7 +17,9 @@ en:
17
17
  boolean: "Attribute '%{attribute}' must be a Boolean (true or false), got %{actual}"
18
18
  object: "Attribute '%{attribute}' must be a Hash (object), got %{actual}"
19
19
  array: "Attribute '%{attribute}' must be an Array, got %{actual}"
20
- datetime: "Attribute '%{attribute}' must be a DateTime/Time/Date, got %{actual}"
20
+ date: "Attribute '%{attribute}' must be a Date, got %{actual}"
21
+ time: "Attribute '%{attribute}' must be a Time, got %{actual}"
22
+ datetime: "Attribute '%{attribute}' must be a DateTime, got %{actual}"
21
23
 
22
24
  inclusion:
23
25
  invalid_schema: "Option 'inclusion' for attribute '%{attribute}' must have a non-empty array of allowed values"
@@ -49,6 +51,17 @@ en:
49
51
  as:
50
52
  invalid_type: "Option 'as' for attribute '%{attribute}' must be a Symbol. Got: %{type}"
51
53
 
54
+ transform:
55
+ invalid_type: "Option 'transform' for attribute '%{attribute}' must be a Proc or Lambda. Got: %{type}"
56
+ execution_error: "Transform failed for attribute '%{attribute}': %{error}"
57
+
58
+ cast:
59
+ invalid_type: "Option 'cast' for attribute '%{attribute}' must be a Symbol. Got: %{type}"
60
+ source_not_supported: "Option 'cast' for attribute '%{attribute}' cannot be used with type '%{source_type}'. Casting is only supported for: %{allowed}"
61
+ target_not_supported: "Option 'cast' for attribute '%{attribute}' cannot cast to '%{target_type}'. Supported target types: %{allowed}"
62
+ conversion_not_supported: "Option 'cast' for attribute '%{attribute}' does not support conversion from '%{from}' to '%{to}'"
63
+ conversion_error: "Cast failed for attribute '%{attribute}' from '%{from}' to '%{to}'. Value: '%{value}'. Error: %{error}"
64
+
52
65
  # Attribute builder DSL
53
66
  builder:
54
67
  not_implemented: "%{class} must implement #create_attribute"
@@ -0,0 +1,282 @@
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
+ # @return [Object] Converted value
166
+ def transform_value(value) # rubocop:disable Metrics/MethodLength
167
+ return value if value.nil? # Cast doesn't modify nil, required validator handles it.
168
+
169
+ target_type = option_value
170
+ conversion_lambda = conversion_matrix.dig(@attribute_type, target_type)
171
+
172
+ # Call conversion lambda
173
+ conversion_lambda.call(value:)
174
+ rescue StandardError => e
175
+ attributes = {
176
+ attribute: @attribute_name,
177
+ from: @attribute_type,
178
+ to: target_type,
179
+ value:,
180
+ error: e.message
181
+ }
182
+
183
+ # Catch all exceptions from conversion execution
184
+ error_message = resolve_custom_message(**attributes) || I18n.t(
185
+ "treaty.attributes.modifiers.cast.conversion_error",
186
+ **attributes
187
+ )
188
+
189
+ raise Treaty::Exceptions::Validation, error_message
190
+ end
191
+
192
+ protected
193
+
194
+ # Override value_key to use :to instead of :is
195
+ # This makes advanced mode syntax: cast: { to: :datetime }
196
+ #
197
+ # @return [Symbol] The key :to
198
+ def value_key
199
+ :to
200
+ end
201
+
202
+ private
203
+
204
+ # Checks if conversion from source type to target type is supported
205
+ #
206
+ # @param from_type [Symbol] Source type
207
+ # @param to_type [Symbol] Target type
208
+ # @return [Boolean] True if conversion is supported
209
+ def conversion_supported?(from_type, to_type)
210
+ conversion_matrix.dig(from_type, to_type).present?
211
+ end
212
+
213
+ # Matrix of all supported type conversions
214
+ # Maps from_type => to_type => conversion_lambda
215
+ #
216
+ # @return [Hash] Conversion matrix
217
+ def conversion_matrix # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
218
+ @conversion_matrix ||= {
219
+ integer: {
220
+ integer: ->(value:) { value }, # No-op for same type
221
+ string: ->(value:) { value.to_s },
222
+ boolean: ->(value:) { value != 0 },
223
+ date: ->(value:) { Time.at(value).to_date },
224
+ time: ->(value:) { Time.at(value) },
225
+ datetime: ->(value:) { Time.at(value).to_datetime }
226
+ },
227
+ string: {
228
+ string: ->(value:) { value }, # No-op for same type
229
+ integer: ->(value:) { Integer(value) },
230
+ boolean: ->(value:) { parse_boolean(value) },
231
+ date: ->(value:) { Date.parse(value) },
232
+ time: ->(value:) { Time.parse(value) },
233
+ datetime: ->(value:) { DateTime.parse(value) }
234
+ },
235
+ boolean: {
236
+ boolean: ->(value:) { value }, # No-op for same type
237
+ string: ->(value:) { value.to_s },
238
+ integer: ->(value:) { value ? 1 : 0 }
239
+ },
240
+ date: {
241
+ date: ->(value:) { value }, # No-op for same type
242
+ string: ->(value:) { value.iso8601 },
243
+ integer: ->(value:) { value.to_time.to_i },
244
+ time: ->(value:) { value.to_time },
245
+ datetime: ->(value:) { value.to_datetime }
246
+ },
247
+ time: {
248
+ time: ->(value:) { value }, # No-op for same type
249
+ string: ->(value:) { value.iso8601 },
250
+ integer: ->(value:) { value.to_i },
251
+ date: ->(value:) { value.to_date },
252
+ datetime: ->(value:) { value.to_datetime }
253
+ },
254
+ datetime: {
255
+ datetime: ->(value:) { value }, # No-op for same type
256
+ string: ->(value:) { value.iso8601 },
257
+ integer: ->(value:) { value.to_i },
258
+ date: ->(value:) { value.to_date },
259
+ time: ->(value:) { value.to_time }
260
+ }
261
+ }
262
+ end
263
+
264
+ # Parses a string value into a boolean
265
+ # Recognizes: true/false, yes/no, 1/0, on/off (case-insensitive)
266
+ #
267
+ # @param value [String] The string value to parse
268
+ # @return [Boolean] Parsed boolean value
269
+ # @raise [ArgumentError] If string is not a recognized boolean value
270
+ def parse_boolean(value)
271
+ normalized = value.to_s.downcase.strip
272
+
273
+ return true if %w[true 1 yes on].include?(normalized)
274
+ return false if %w[false 0 no off].include?(normalized)
275
+
276
+ raise ArgumentError, "Cannot convert '#{value}' to boolean"
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,111 @@
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
+ # @return [Object] Transformed value
86
+ def transform_value(value) # rubocop:disable Metrics/MethodLength
87
+ return value if value.nil? # Transform doesn't modify nil, required validator handles it.
88
+
89
+ transform_lambda = option_value
90
+
91
+ # Call lambda with named argument
92
+ transform_lambda.call(value:)
93
+ rescue StandardError => e
94
+ attributes = {
95
+ attribute: @attribute_name,
96
+ error: e.message
97
+ }
98
+
99
+ # Catch all exceptions from lambda execution
100
+ error_message = resolve_custom_message(**attributes) || I18n.t(
101
+ "treaty.attributes.modifiers.transform.execution_error",
102
+ **attributes
103
+ )
104
+
105
+ raise Treaty::Exceptions::Validation, error_message
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -27,6 +27,8 @@ module Treaty
27
27
  #
28
28
  # - `:as` → AsModifier - Renames attributes
29
29
  # - `:default` → DefaultModifier - Provides default values
30
+ # - `:transform` → TransformModifier - Transforms values using custom lambdas
31
+ # - `:cast` → CastModifier - Converts values between types automatically
30
32
  #
31
33
  # ## Auto-Registration
32
34
  #
@@ -81,6 +83,8 @@ module Treaty
81
83
  def register_modifiers!
82
84
  Registry.register(:as, Modifiers::AsModifier, category: :modifier)
83
85
  Registry.register(:default, Modifiers::DefaultModifier, category: :modifier)
86
+ Registry.register(:transform, Modifiers::TransformModifier, category: :modifier)
87
+ Registry.register(:cast, Modifiers::CastModifier, category: :modifier)
84
88
  end
85
89
  end
86
90
  end
@@ -13,7 +13,9 @@ module Treaty
13
13
  # - `:boolean` - Ruby TrueClass or FalseClass
14
14
  # - `:object` - Ruby Hash (for nested objects)
15
15
  # - `:array` - Ruby Array (for collections)
16
- # - `:datetime` - Ruby DateTime, Time, or Date
16
+ # - `:date` - Ruby Date
17
+ # - `:time` - Ruby Time
18
+ # - `:datetime` - Ruby DateTime
17
19
  #
18
20
  # ## Usage Examples
19
21
  #
@@ -21,7 +23,9 @@ module Treaty
21
23
  # integer :age
22
24
  # string :name
23
25
  # boolean :published
24
- # datetime :created_at
26
+ # date :published_on
27
+ # time :created_at
28
+ # datetime :updated_at
25
29
  #
26
30
  # Nested structures:
27
31
  # object :author do
@@ -36,14 +40,16 @@ module Treaty
36
40
  #
37
41
  # - Validates only non-nil values (nil handling is done by RequiredValidator)
38
42
  # - Type mismatch raises Treaty::Exceptions::Validation
39
- # - Datetime accepts DateTime, Time, or Date objects
43
+ # - Date accepts only Date objects (not DateTime or Time)
44
+ # - Time accepts only Time objects (not Date or DateTime)
45
+ # - DateTime accepts only DateTime objects (not Date or Time)
40
46
  #
41
47
  # ## Note
42
48
  #
43
49
  # TypeValidator doesn't use option_schema - it validates based on attribute_type.
44
50
  # This validator is always active for all attributes.
45
51
  class TypeValidator < Treaty::Attribute::Option::Base
46
- ALLOWED_TYPES = %i[integer string boolean object array datetime].freeze
52
+ ALLOWED_TYPES = %i[integer string boolean object array date time datetime].freeze
47
53
 
48
54
  # Validates that the attribute type is one of the allowed types
49
55
  #
@@ -81,6 +87,10 @@ module Treaty
81
87
  validate_object!(value)
82
88
  when :array
83
89
  validate_array!(value)
90
+ when :date
91
+ validate_date!(value)
92
+ when :time
93
+ validate_time!(value)
84
94
  when :datetime
85
95
  validate_datetime!(value)
86
96
  end
@@ -172,14 +182,33 @@ module Treaty
172
182
  validate_type!(value, :array) { |v| v.is_a?(Array) }
173
183
  end
174
184
 
175
- # Validates that value is a DateTime, Time, or Date
185
+ # Validates that value is a Date (but not DateTime, since DateTime < Date)
176
186
  #
177
187
  # @param value [Object] The value to validate
178
- # @raise [Treaty::Exceptions::Validation] If value is not a datetime type
188
+ # @raise [Treaty::Exceptions::Validation] If value is not a Date
189
+ # @return [void]
190
+ def validate_date!(value)
191
+ validate_type!(value, :date) { |v| v.is_a?(Date) && !v.is_a?(DateTime) }
192
+ end
193
+
194
+ # Validates that value is a Time or ActiveSupport::TimeWithZone
195
+ #
196
+ # @param value [Object] The value to validate
197
+ # @raise [Treaty::Exceptions::Validation] If value is not a Time
198
+ # @return [void]
199
+ def validate_time!(value)
200
+ validate_type!(value, :time) do |v|
201
+ v.is_a?(Time) || (defined?(ActiveSupport::TimeWithZone) && v.is_a?(ActiveSupport::TimeWithZone))
202
+ end
203
+ end
204
+
205
+ # Validates that value is a DateTime
206
+ #
207
+ # @param value [Object] The value to validate
208
+ # @raise [Treaty::Exceptions::Validation] If value is not a DateTime
179
209
  # @return [void]
180
210
  def validate_datetime!(value)
181
- # TODO: It is better to divide it into different methods for each class.
182
- validate_type!(value, :datetime) { |v| v.is_a?(DateTime) || v.is_a?(Time) || v.is_a?(Date) }
211
+ validate_type!(value, :datetime) { |v| v.is_a?(DateTime) }
183
212
  end
184
213
  end
185
214
  end
@@ -70,7 +70,8 @@ module Treaty
70
70
  OPTION_KEY_MAPPING = {
71
71
  in: { advanced_key: :inclusion, value_key: :in },
72
72
  as: { advanced_key: :as, value_key: :is },
73
- default: { advanced_key: :default, value_key: :is }
73
+ default: { advanced_key: :default, value_key: :is },
74
+ cast: { advanced_key: :cast, value_key: :to }
74
75
  }.freeze
75
76
  private_constant :OPTION_KEY_MAPPING
76
77
 
@@ -138,8 +138,7 @@ module Treaty
138
138
  def transform(value)
139
139
  value.each_with_index.map do |item, index|
140
140
  if simple_array?
141
- validate_simple_element(item, index)
142
- item
141
+ transform_simple_element(item, index)
143
142
  else
144
143
  transform_array_item(item, index)
145
144
  end
@@ -156,19 +155,21 @@ module Treaty
156
155
  attribute.collection_of_attributes.first.name == SELF_OBJECT
157
156
  end
158
157
 
159
- # Validates a simple array element (primitive value)
158
+ # Transforms a simple array element (primitive value)
159
+ # Validates and applies transformations to the element
160
160
  #
161
- # @param item [Object] Array element to validate
161
+ # @param item [Object] Array element to transform
162
162
  # @param index [Integer] Element index for error messages
163
163
  # @raise [Treaty::Exceptions::Validation] If validation fails
164
- # @return [void]
165
- def validate_simple_element(item, index) # rubocop:disable Metrics/MethodLength
164
+ # @return [Object] Transformed element value
165
+ def transform_simple_element(item, index) # rubocop:disable Metrics/MethodLength
166
166
  self_attr = attribute.collection_of_attributes.first
167
167
  validator = AttributeValidator.new(self_attr)
168
168
  validator.validate_schema!
169
169
 
170
170
  begin
171
171
  validator.validate_value!(item)
172
+ validator.transform_value(item)
172
173
  rescue Treaty::Exceptions::Validation => e
173
174
  raise Treaty::Exceptions::Validation,
174
175
  I18n.t(
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 12
6
+ MINOR = 14
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: treaty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov
@@ -156,7 +156,9 @@ files:
156
156
  - lib/treaty/attribute/helper_mapper.rb
157
157
  - lib/treaty/attribute/option/base.rb
158
158
  - lib/treaty/attribute/option/modifiers/as_modifier.rb
159
+ - lib/treaty/attribute/option/modifiers/cast_modifier.rb
159
160
  - lib/treaty/attribute/option/modifiers/default_modifier.rb
161
+ - lib/treaty/attribute/option/modifiers/transform_modifier.rb
160
162
  - lib/treaty/attribute/option/registry.rb
161
163
  - lib/treaty/attribute/option/registry_initializer.rb
162
164
  - lib/treaty/attribute/option/validators/format_validator.rb