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
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that string attribute value matches a specific format.
9
+ #
10
+ # ## Supported Formats
11
+ #
12
+ # - `:uuid` - Universally unique identifier
13
+ # - `:email` - Email address (RFC 2822 compliant)
14
+ # - `:password` - Password (8-16 chars, must contain digit, lowercase, and uppercase)
15
+ # - `:duration` - ActiveSupport::Duration compatible string (e.g., "1 day", "2 hours")
16
+ # - `:date` - ISO 8601 date string (e.g., "2025-01-15")
17
+ # - `:datetime` - ISO 8601 datetime string (e.g., "2025-01-15T10:30:00Z")
18
+ # - `:time` - Time string (e.g., "10:30:00", "10:30 AM")
19
+ # - `:boolean` - Boolean string ("true", "false", "0", "1")
20
+ #
21
+ # ## Usage Examples
22
+ #
23
+ # Simple mode:
24
+ # string :email, format: :email
25
+ # string :started_on, format: :date
26
+ #
27
+ # Advanced mode:
28
+ # string :email, format: { is: :email }
29
+ # string :started_on, format: { is: :date, message: "Invalid date format" }
30
+ # string :started_on, format: { is: :date, message: ->(attribute:, value:, **) { "#{attribute} has invalid date: #{value}" } } # rubocop:disable Layout/LineLength
31
+ #
32
+ # ## Validation Rules
33
+ #
34
+ # - Only works with `:string` type attributes
35
+ # - Raises Treaty::Exceptions::Validation if used with non-string types
36
+ # - Skips validation for nil values (handled by RequiredValidator)
37
+ # - Each format has a pattern and/or validator for flexible validation
38
+ #
39
+ # ## Extensibility
40
+ #
41
+ # To add new formats, extend DEFAULT_FORMATS hash with format definition:
42
+ # DEFAULT_FORMATS[:custom_format] = {
43
+ # pattern: /regex/,
44
+ # validator: ->(value) { custom_validation_logic }
45
+ # }
46
+ class FormatValidator < Treaty::Entity::Attribute::Option::Base # rubocop:disable Metrics/ClassLength
47
+ # UUID format regex (8-4-4-4-12 hexadecimal pattern)
48
+ UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
49
+
50
+ # Password format regex (8-16 chars, at least one digit, lowercase, and uppercase)
51
+ PASSWORD_PATTERN = /\A(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,16}\z/
52
+
53
+ # Boolean string format regex (accepts "true", "false", "0", "1" case-insensitive)
54
+ BOOLEAN_PATTERN = /\A(true|false|0|1)\z/i
55
+
56
+ # Default format definitions with patterns and validators
57
+ # Each format can have:
58
+ # - pattern: Regex for pattern matching
59
+ # - validator: Lambda for custom validation logic
60
+ DEFAULT_FORMATS = {
61
+ uuid: {
62
+ pattern: UUID_PATTERN,
63
+ validator: nil
64
+ },
65
+ email: {
66
+ pattern: URI::MailTo::EMAIL_REGEXP,
67
+ validator: nil
68
+ },
69
+ password: {
70
+ pattern: PASSWORD_PATTERN,
71
+ validator: nil
72
+ },
73
+ duration: {
74
+ pattern: nil,
75
+ validator: lambda do |value|
76
+ ActiveSupport::Duration.parse(value)
77
+ true
78
+ rescue StandardError
79
+ false
80
+ end
81
+ },
82
+ date: {
83
+ pattern: nil,
84
+ validator: lambda do |value|
85
+ Date.parse(value)
86
+ true
87
+ rescue ArgumentError, TypeError
88
+ false
89
+ end
90
+ },
91
+ datetime: {
92
+ pattern: nil,
93
+ validator: lambda do |value|
94
+ DateTime.parse(value)
95
+ true
96
+ rescue ArgumentError, TypeError
97
+ false
98
+ end
99
+ },
100
+ time: {
101
+ pattern: nil,
102
+ validator: lambda do |value|
103
+ Time.parse(value)
104
+ true
105
+ rescue ArgumentError, TypeError
106
+ false
107
+ end
108
+ },
109
+ boolean: {
110
+ pattern: BOOLEAN_PATTERN,
111
+ validator: nil
112
+ }
113
+ }.freeze
114
+
115
+ # Validates that format is only used with string type attributes
116
+ # and that the format name is valid
117
+ #
118
+ # @raise [Treaty::Exceptions::Validation] If format is used with non-string type
119
+ # @raise [Treaty::Exceptions::Validation] If format name is unknown
120
+ # @return [void]
121
+ def validate_schema! # rubocop:disable Metrics/MethodLength
122
+ # Format option only works with string types
123
+ unless @attribute_type == :string
124
+ raise Treaty::Exceptions::Validation,
125
+ I18n.t(
126
+ "treaty.attributes.validators.format.type_mismatch",
127
+ attribute: @attribute_name,
128
+ type: @attribute_type
129
+ )
130
+ end
131
+
132
+ format_name = option_value
133
+
134
+ # Validate that format name exists
135
+ return if formats.key?(format_name)
136
+
137
+ raise Treaty::Exceptions::Validation,
138
+ I18n.t(
139
+ "treaty.attributes.validators.format.unknown_format",
140
+ attribute: @attribute_name,
141
+ format_name:,
142
+ allowed: formats.keys.join(", ")
143
+ )
144
+ end
145
+
146
+ # Validates that the value matches the specified format
147
+ # Skips validation for nil values (handled by RequiredValidator)
148
+ #
149
+ # @param value [String] The value to validate
150
+ # @raise [Treaty::Exceptions::Validation] If value doesn't match format
151
+ # @return [void]
152
+ def validate_value!(value) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
153
+ return if value.nil? # Format validation doesn't check for nil, required does.
154
+
155
+ format_name = option_value
156
+ format_definition = formats[format_name]
157
+
158
+ # Allow blank values (empty strings should be caught by required validator)
159
+ return if value.to_s.strip.empty?
160
+
161
+ # Apply pattern matching if defined
162
+ if format_definition.fetch(:pattern)
163
+ return if value.match?(format_definition.fetch(:pattern))
164
+
165
+ # Pattern failed, and no validator - raise error
166
+ unless format_definition.fetch(:validator)
167
+ attributes = {
168
+ attribute: @attribute_name,
169
+ value:,
170
+ format_name:
171
+ }
172
+
173
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
174
+
175
+ raise Treaty::Exceptions::Validation, message
176
+ end
177
+ end
178
+
179
+ # Apply validator if defined
180
+ return unless format_definition.fetch(:validator)
181
+ return if format_definition.fetch(:validator).call(value)
182
+
183
+ attributes = {
184
+ attribute: @attribute_name,
185
+ value:,
186
+ format_name:
187
+ }
188
+
189
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
190
+
191
+ raise Treaty::Exceptions::Validation, message
192
+ end
193
+
194
+ private
195
+
196
+ # Returns the available formats (allows for extension)
197
+ #
198
+ # @return [Hash] Hash of available formats with their definitions
199
+ def formats
200
+ DEFAULT_FORMATS
201
+ end
202
+
203
+ # Generates default error message for format validation failure using I18n
204
+ #
205
+ # @param attribute [Symbol] The attribute name
206
+ # @param value [Object] The actual value that failed validation
207
+ # @param format_name [Symbol] The format name
208
+ # @return [String] Default error message
209
+ def default_message(attribute:, value:, format_name:)
210
+ I18n.t(
211
+ "treaty.attributes.validators.format.mismatch",
212
+ attribute:,
213
+ value:,
214
+ format_name:
215
+ )
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that attribute value is included in allowed set.
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Simple mode:
13
+ # string :provider, in: %w[twitter linkedin github]
14
+ #
15
+ # Advanced mode:
16
+ # string :provider, inclusion: { in: %w[twitter linkedin github], message: "Invalid provider" }
17
+ #
18
+ # ## Advanced Mode
19
+ #
20
+ # Uses `:in` as the value key (instead of default `:is`).
21
+ # Schema format: `{ in: [...], message: nil }`
22
+ class InclusionValidator < Treaty::Entity::Attribute::Option::Base
23
+ # Validates that allowed values are provided as non-empty array
24
+ #
25
+ # @raise [Treaty::Exceptions::Validation] If allowed values are not valid
26
+ # @return [void]
27
+ def validate_schema!
28
+ allowed_values = option_value
29
+
30
+ return if allowed_values.is_a?(Array) && !allowed_values.empty?
31
+
32
+ raise Treaty::Exceptions::Validation,
33
+ I18n.t(
34
+ "treaty.attributes.validators.inclusion.invalid_schema",
35
+ attribute: @attribute_name
36
+ )
37
+ end
38
+
39
+ # Validates that value is included in allowed set
40
+ # Skips validation for nil values (handled by RequiredValidator)
41
+ #
42
+ # @param value [Object] The value to validate
43
+ # @raise [Treaty::Exceptions::Validation] If value is not in allowed set
44
+ # @return [void]
45
+ def validate_value!(value)
46
+ return if value.nil? # Inclusion validation doesn't check for nil, required does.
47
+
48
+ allowed_values = option_value
49
+
50
+ return if allowed_values.include?(value)
51
+
52
+ attributes = {
53
+ attribute: @attribute_name,
54
+ value:,
55
+ allowed_values:
56
+ }
57
+
58
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
59
+
60
+ raise Treaty::Exceptions::Validation, message
61
+ end
62
+
63
+ protected
64
+
65
+ # Returns the value key for inclusion validator
66
+ # Uses :in instead of default :is
67
+ #
68
+ # @return [Symbol] The value key (:in)
69
+ def value_key
70
+ :in
71
+ end
72
+
73
+ private
74
+
75
+ # Generates default error message with allowed values using I18n
76
+ #
77
+ # @param attribute [Symbol] The attribute name
78
+ # @param value [Object] The actual value that failed validation
79
+ # @param allowed_values [Array] Array of allowed values
80
+ # @return [String] Default error message
81
+ def default_message(attribute:, value:, allowed_values:)
82
+ I18n.t(
83
+ "treaty.attributes.validators.inclusion.not_included",
84
+ attribute:,
85
+ allowed: allowed_values.join(", "),
86
+ value:
87
+ )
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that attribute value is present (not nil and not empty).
9
+ #
10
+ # ## Usage Examples
11
+ #
12
+ # Helper mode:
13
+ # string :title, :required # Maps to required: true
14
+ # string :bio, :optional # Maps to required: false
15
+ #
16
+ # Simple mode:
17
+ # string :title, required: true
18
+ # string :bio, required: false
19
+ #
20
+ # Advanced mode:
21
+ # string :title, required: { is: true, message: "Title is mandatory" }
22
+ #
23
+ # ## Default Behavior
24
+ #
25
+ # - Request attributes: required by default (required: true)
26
+ # - Response attributes: optional by default (required: false)
27
+ #
28
+ # ## Validation Rules
29
+ #
30
+ # A value is considered present if:
31
+ # - It is not nil
32
+ # - It is not empty (for String, Array, Hash)
33
+ #
34
+ # ## Advanced Mode
35
+ #
36
+ # Schema format: `{ is: true/false, message: nil }`
37
+ class RequiredValidator < Treaty::Entity::Attribute::Option::Base
38
+ # Validates schema (no validation needed, already normalized)
39
+ #
40
+ # @return [void]
41
+ def validate_schema!
42
+ # Schema structure is already normalized by OptionNormalizer.
43
+ # Nothing to validate here.
44
+ end
45
+
46
+ # Validates that required attribute has a present value
47
+ #
48
+ # @param value [Object] The value to validate
49
+ # @raise [Treaty::Exceptions::Validation] If required but value is missing/empty
50
+ # @return [void]
51
+ def validate_value!(value)
52
+ return unless required?
53
+ return if present?(value)
54
+
55
+ message = resolve_custom_message(
56
+ attribute: @attribute_name,
57
+ value:
58
+ ) || default_message
59
+
60
+ raise Treaty::Exceptions::Validation, message
61
+ end
62
+
63
+ private
64
+
65
+ # Checks if attribute is required
66
+ #
67
+ # @return [Boolean] True if attribute is required
68
+ def required?
69
+ return false if @option_schema.nil?
70
+
71
+ # Use option_value helper which correctly extracts value based on mode
72
+ option_value == true
73
+ end
74
+
75
+ # Checks if value is present (not nil and not empty)
76
+ #
77
+ # @param value [Object] The value to check
78
+ # @return [Boolean] True if value is present
79
+ def present?(value)
80
+ return false if value.nil?
81
+ return false if value.respond_to?(:empty?) && value.empty?
82
+
83
+ true
84
+ end
85
+
86
+ # Generates default error message using I18n
87
+ #
88
+ # @return [String] Default error message
89
+ def default_message
90
+ I18n.t(
91
+ "treaty.attributes.validators.required.blank",
92
+ attribute: @attribute_name
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Option
7
+ module Validators
8
+ # Validates that attribute value matches the declared type.
9
+ #
10
+ # ## Supported Types
11
+ #
12
+ # - `:integer` - Ruby Integer
13
+ # - `:string` - Ruby String
14
+ # - `:boolean` - Ruby TrueClass or FalseClass
15
+ # - `:object` - Ruby Hash (for nested objects)
16
+ # - `:array` - Ruby Array (for collections)
17
+ # - `:date` - Ruby Date
18
+ # - `:time` - Ruby Time
19
+ # - `:datetime` - Ruby DateTime
20
+ #
21
+ # ## Usage Examples
22
+ #
23
+ # Simple types:
24
+ # integer :age
25
+ # string :name
26
+ # boolean :published
27
+ # date :published_on
28
+ # time :created_at
29
+ # datetime :updated_at
30
+ #
31
+ # Nested structures:
32
+ # object :author do
33
+ # string :name
34
+ # end
35
+ #
36
+ # array :tags do
37
+ # string :_self # Simple array of strings
38
+ # end
39
+ #
40
+ # ## Validation Rules
41
+ #
42
+ # - Validates only non-nil values (nil handling is done by RequiredValidator)
43
+ # - Type mismatch raises Treaty::Exceptions::Validation
44
+ # - Date accepts only Date objects (not DateTime or Time)
45
+ # - Time accepts only Time objects (not Date or DateTime)
46
+ # - DateTime accepts only DateTime objects (not Date or Time)
47
+ #
48
+ # ## Note
49
+ #
50
+ # TypeValidator doesn't use option_schema - it validates based on attribute_type.
51
+ # This validator is always active for all attributes.
52
+ class TypeValidator < Treaty::Entity::Attribute::Option::Base
53
+ ALLOWED_TYPES = %i[integer string boolean object array date time datetime].freeze
54
+
55
+ # Validates that the attribute type is one of the allowed types
56
+ #
57
+ # @raise [Treaty::Exceptions::Validation] If type is not allowed
58
+ # @return [void]
59
+ def validate_schema!
60
+ return if ALLOWED_TYPES.include?(@attribute_type)
61
+
62
+ raise Treaty::Exceptions::Validation,
63
+ I18n.t(
64
+ "treaty.attributes.validators.type.unknown_type",
65
+ type: @attribute_type,
66
+ attribute: @attribute_name,
67
+ allowed: ALLOWED_TYPES.join(", ")
68
+ )
69
+ end
70
+
71
+ # Validates that the value matches the declared type
72
+ # Skips validation for nil values (handled by RequiredValidator)
73
+ #
74
+ # @param value [Object] The value to validate
75
+ # @raise [Treaty::Exceptions::Validation] If value type doesn't match
76
+ # @return [void]
77
+ def validate_value!(value) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
78
+ return if value.nil? # Type validation doesn't check for nil, required does.
79
+
80
+ case @attribute_type
81
+ when :integer
82
+ validate_integer!(value)
83
+ when :string
84
+ validate_string!(value)
85
+ when :boolean
86
+ validate_boolean!(value)
87
+ when :object
88
+ validate_object!(value)
89
+ when :array
90
+ validate_array!(value)
91
+ when :date
92
+ validate_date!(value)
93
+ when :time
94
+ validate_time!(value)
95
+ when :datetime
96
+ validate_datetime!(value)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # Common type validation logic
103
+ # Checks if value matches expected type and raises exception with appropriate message
104
+ #
105
+ # @param value [Object] The value to validate
106
+ # @param expected_type [Symbol] The expected type symbol
107
+ # @yield Block that returns true if value is valid
108
+ # @raise [Treaty::Exceptions::Validation] If type validation fails
109
+ # @return [void]
110
+ def validate_type!(value, expected_type)
111
+ return if yield(value)
112
+
113
+ actual_type = value.class
114
+
115
+ attributes = {
116
+ attribute: @attribute_name,
117
+ value:,
118
+ expected_type:,
119
+ actual_type:
120
+ }
121
+
122
+ message = resolve_custom_message(**attributes) || default_message(**attributes)
123
+
124
+ raise Treaty::Exceptions::Validation, message
125
+ end
126
+
127
+ # Generates default error message for type mismatch using I18n
128
+ #
129
+ # @param attribute [Symbol] The attribute name
130
+ # @param expected_type [Symbol] The expected type
131
+ # @param actual_type [Class] The actual class of the value
132
+ # @return [String] Default error message
133
+ def default_message(attribute:, expected_type:, actual_type:, **)
134
+ I18n.t(
135
+ "treaty.attributes.validators.type.mismatch.#{expected_type}",
136
+ attribute:,
137
+ actual: actual_type
138
+ )
139
+ end
140
+
141
+ # Validates that value is an Integer
142
+ #
143
+ # @param value [Object] The value to validate
144
+ # @raise [Treaty::Exceptions::Validation] If value is not an Integer
145
+ # @return [void]
146
+ def validate_integer!(value)
147
+ validate_type!(value, :integer) { |v| v.is_a?(Integer) }
148
+ end
149
+
150
+ # Validates that value is a String
151
+ #
152
+ # @param value [Object] The value to validate
153
+ # @raise [Treaty::Exceptions::Validation] If value is not a String
154
+ # @return [void]
155
+ def validate_string!(value)
156
+ validate_type!(value, :string) { |v| v.is_a?(String) }
157
+ end
158
+
159
+ # Validates that value is a Boolean (TrueClass or FalseClass)
160
+ #
161
+ # @param value [Object] The value to validate
162
+ # @raise [Treaty::Exceptions::Validation] If value is not a Boolean
163
+ # @return [void]
164
+ def validate_boolean!(value)
165
+ validate_type!(value, :boolean) { |v| v.is_a?(TrueClass) || v.is_a?(FalseClass) }
166
+ end
167
+
168
+ # Validates that value is a Hash (object type)
169
+ #
170
+ # @param value [Object] The value to validate
171
+ # @raise [Treaty::Exceptions::Validation] If value is not a Hash
172
+ # @return [void]
173
+ def validate_object!(value)
174
+ validate_type!(value, :object) { |v| v.is_a?(Hash) }
175
+ end
176
+
177
+ # Validates that value is an Array
178
+ #
179
+ # @param value [Object] The value to validate
180
+ # @raise [Treaty::Exceptions::Validation] If value is not an Array
181
+ # @return [void]
182
+ def validate_array!(value)
183
+ validate_type!(value, :array) { |v| v.is_a?(Array) }
184
+ end
185
+
186
+ # Validates that value is a Date (but not DateTime, since DateTime < Date)
187
+ #
188
+ # @param value [Object] The value to validate
189
+ # @raise [Treaty::Exceptions::Validation] If value is not a Date
190
+ # @return [void]
191
+ def validate_date!(value)
192
+ validate_type!(value, :date) { |v| v.is_a?(Date) && !v.is_a?(DateTime) }
193
+ end
194
+
195
+ # Validates that value is a Time or ActiveSupport::TimeWithZone
196
+ #
197
+ # @param value [Object] The value to validate
198
+ # @raise [Treaty::Exceptions::Validation] If value is not a Time
199
+ # @return [void]
200
+ def validate_time!(value)
201
+ validate_type!(value, :time) do |v|
202
+ v.is_a?(Time) || (defined?(ActiveSupport::TimeWithZone) && v.is_a?(ActiveSupport::TimeWithZone))
203
+ end
204
+ end
205
+
206
+ # Validates that value is a DateTime
207
+ #
208
+ # @param value [Object] The value to validate
209
+ # @raise [Treaty::Exceptions::Validation] If value is not a DateTime
210
+ # @return [void]
211
+ def validate_datetime!(value)
212
+ validate_type!(value, :datetime) { |v| v.is_a?(DateTime) }
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end