treaty 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/config/locales/en.yml +6 -2
- data/lib/treaty/engine.rb +1 -1
- data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
- data/lib/treaty/entity/attribute/base.rb +184 -0
- data/lib/treaty/entity/attribute/builder/base.rb +275 -0
- data/lib/treaty/entity/attribute/collection.rb +67 -0
- data/lib/treaty/entity/attribute/dsl.rb +92 -0
- data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
- data/lib/treaty/entity/attribute/option/base.rb +190 -0
- data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
- data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
- data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
- data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
- data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
- data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
- data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
- data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
- data/lib/treaty/entity/attribute/option/registry.rb +157 -0
- data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
- data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
- data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
- data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
- data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
- data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
- data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
- data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
- data/lib/treaty/entity/attribute/validation/base.rb +76 -0
- data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
- data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
- data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
- data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
- data/lib/treaty/entity/base.rb +90 -0
- data/lib/treaty/entity/builder.rb +44 -0
- data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
- data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
- data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
- data/lib/treaty/entity.rb +7 -75
- data/lib/treaty/request/attribute/attribute.rb +1 -1
- data/lib/treaty/request/attribute/builder.rb +24 -1
- data/lib/treaty/request/entity.rb +1 -1
- data/lib/treaty/request/factory.rb +6 -6
- data/lib/treaty/request/validator.rb +1 -1
- data/lib/treaty/response/attribute/attribute.rb +1 -1
- data/lib/treaty/response/attribute/builder.rb +24 -1
- data/lib/treaty/response/entity.rb +1 -1
- data/lib/treaty/response/factory.rb +6 -6
- data/lib/treaty/response/validator.rb +1 -1
- data/lib/treaty/version.rb +1 -1
- metadata +35 -34
- data/lib/treaty/attribute/base.rb +0 -182
- data/lib/treaty/attribute/builder/base.rb +0 -143
- data/lib/treaty/attribute/collection.rb +0 -65
- data/lib/treaty/attribute/dsl.rb +0 -90
- data/lib/treaty/attribute/entity/builder.rb +0 -23
- data/lib/treaty/attribute/helper_mapper.rb +0 -72
- data/lib/treaty/attribute/option/base.rb +0 -188
- data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
- data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
- data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
- data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
- data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
- data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
- data/lib/treaty/attribute/option/registry.rb +0 -155
- data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
- data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
- data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
- data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
- data/lib/treaty/attribute/option_normalizer.rb +0 -166
- data/lib/treaty/attribute/option_orchestrator.rb +0 -190
- data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
- data/lib/treaty/attribute/validation/base.rb +0 -74
- data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
- data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
- data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
- data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
|
@@ -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
|