treaty 0.8.0 → 0.9.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fd8fd4a097f7ff97cdb53e0d7825d79da5562d0bb6347e8e40794c158f8cb41
|
|
4
|
+
data.tar.gz: b0e119a2ba1018b4eeeb49c753b2949fb4c320ca89be29d633e7e6367cbd7360
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10514553e41066a04e066e6561744f69ca779bc4a87594bab7dce48b7e6217a5d5a6f2b7e28f1770876a9d3ff82d2f715cf4d167ef683dc36723b65a1549dc62
|
|
7
|
+
data.tar.gz: e262a6a4b2dbe068c53ced57ddcd7d9687a8e0be7d13f90ab8c2b9176015a3099829af702f59d22d28588a8c929e7178746d79356c35d4d0e899cc46991ce439
|
data/config/locales/en.yml
CHANGED
|
@@ -23,6 +23,11 @@ en:
|
|
|
23
23
|
invalid_schema: "Option 'inclusion' for attribute '%{attribute}' must have a non-empty array of allowed values"
|
|
24
24
|
not_included: "Attribute '%{attribute}' must be one of: %{allowed}. Got: '%{value}'"
|
|
25
25
|
|
|
26
|
+
format:
|
|
27
|
+
type_mismatch: "Option 'format' for attribute '%{attribute}' can only be used with String type. Current type: %{type}"
|
|
28
|
+
unknown_format: "Unknown format '%{format_name}' for attribute '%{attribute}'. Allowed formats: %{allowed}"
|
|
29
|
+
mismatch: "Attribute '%{attribute}' has invalid %{format_name} format: '%{value}'"
|
|
30
|
+
|
|
26
31
|
# Nested structures validation
|
|
27
32
|
nested:
|
|
28
33
|
# Orchestrator errors
|
|
@@ -21,6 +21,7 @@ module Treaty
|
|
|
21
21
|
# - `:required` → RequiredValidator - Validates required/optional attributes
|
|
22
22
|
# - `:type` → TypeValidator - Validates value types
|
|
23
23
|
# - `:inclusion` → InclusionValidator - Validates value is in allowed set
|
|
24
|
+
# - `:format` → FormatValidator - Validates string values match specific formats
|
|
24
25
|
#
|
|
25
26
|
# ## Built-in Modifiers
|
|
26
27
|
#
|
|
@@ -71,6 +72,7 @@ module Treaty
|
|
|
71
72
|
Registry.register(:required, Validators::RequiredValidator, category: :validator)
|
|
72
73
|
Registry.register(:type, Validators::TypeValidator, category: :validator)
|
|
73
74
|
Registry.register(:inclusion, Validators::InclusionValidator, category: :validator)
|
|
75
|
+
Registry.register(:format, Validators::FormatValidator, category: :validator)
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
# Registers all built-in modifiers
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Option
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that string attribute value matches a specific format.
|
|
8
|
+
#
|
|
9
|
+
# ## Supported Formats
|
|
10
|
+
#
|
|
11
|
+
# - `:uuid` - Universally unique identifier
|
|
12
|
+
# - `:email` - Email address (RFC 2822 compliant)
|
|
13
|
+
# - `:password` - Password (8-16 chars, must contain digit, lowercase, and uppercase)
|
|
14
|
+
# - `:duration` - ActiveSupport::Duration compatible string (e.g., "1 day", "2 hours")
|
|
15
|
+
# - `:date` - ISO 8601 date string (e.g., "2025-01-15")
|
|
16
|
+
# - `:datetime` - ISO 8601 datetime string (e.g., "2025-01-15T10:30:00Z")
|
|
17
|
+
# - `:time` - Time string (e.g., "10:30:00", "10:30 AM")
|
|
18
|
+
# - `:boolean` - Boolean string ("true", "false", "0", "1")
|
|
19
|
+
#
|
|
20
|
+
# ## Usage Examples
|
|
21
|
+
#
|
|
22
|
+
# Simple mode:
|
|
23
|
+
# string :email, format: :email
|
|
24
|
+
# string :started_on, format: :date
|
|
25
|
+
#
|
|
26
|
+
# Advanced mode:
|
|
27
|
+
# string :email, format: { is: :email }
|
|
28
|
+
# string :started_on, format: { is: :date, message: "Invalid date format" }
|
|
29
|
+
# string :started_on, format: { is: :date, message: ->(attribute:, value:, **) { "#{attribute} has invalid date: #{value}" } } # rubocop:disable Layout/LineLength
|
|
30
|
+
#
|
|
31
|
+
# ## Validation Rules
|
|
32
|
+
#
|
|
33
|
+
# - Only works with `:string` type attributes
|
|
34
|
+
# - Raises Treaty::Exceptions::Validation if used with non-string types
|
|
35
|
+
# - Skips validation for nil values (handled by RequiredValidator)
|
|
36
|
+
# - Each format has a pattern and/or validator for flexible validation
|
|
37
|
+
#
|
|
38
|
+
# ## Extensibility
|
|
39
|
+
#
|
|
40
|
+
# To add new formats, extend DEFAULT_FORMATS hash with format definition:
|
|
41
|
+
# DEFAULT_FORMATS[:custom_format] = {
|
|
42
|
+
# pattern: /regex/,
|
|
43
|
+
# validator: ->(value) { custom_validation_logic }
|
|
44
|
+
# }
|
|
45
|
+
class FormatValidator < Treaty::Attribute::Option::Base # rubocop:disable Metrics/ClassLength
|
|
46
|
+
# UUID format regex (8-4-4-4-12 hexadecimal pattern)
|
|
47
|
+
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
|
|
48
|
+
|
|
49
|
+
# Password format regex (8-16 chars, at least one digit, lowercase, and uppercase)
|
|
50
|
+
PASSWORD_PATTERN = /\A(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,16}\z/
|
|
51
|
+
|
|
52
|
+
# Boolean string format regex (accepts "true", "false", "0", "1" case-insensitive)
|
|
53
|
+
BOOLEAN_PATTERN = /\A(true|false|0|1)\z/i
|
|
54
|
+
|
|
55
|
+
# Default format definitions with patterns and validators
|
|
56
|
+
# Each format can have:
|
|
57
|
+
# - pattern: Regex for pattern matching
|
|
58
|
+
# - validator: Lambda for custom validation logic
|
|
59
|
+
DEFAULT_FORMATS = {
|
|
60
|
+
uuid: {
|
|
61
|
+
pattern: UUID_PATTERN,
|
|
62
|
+
validator: nil
|
|
63
|
+
},
|
|
64
|
+
email: {
|
|
65
|
+
pattern: URI::MailTo::EMAIL_REGEXP,
|
|
66
|
+
validator: nil
|
|
67
|
+
},
|
|
68
|
+
password: {
|
|
69
|
+
pattern: PASSWORD_PATTERN,
|
|
70
|
+
validator: nil
|
|
71
|
+
},
|
|
72
|
+
duration: {
|
|
73
|
+
pattern: nil,
|
|
74
|
+
validator: lambda do |value|
|
|
75
|
+
ActiveSupport::Duration.parse(value)
|
|
76
|
+
true
|
|
77
|
+
rescue StandardError
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
},
|
|
81
|
+
date: {
|
|
82
|
+
pattern: nil,
|
|
83
|
+
validator: lambda do |value|
|
|
84
|
+
Date.parse(value)
|
|
85
|
+
true
|
|
86
|
+
rescue ArgumentError, TypeError
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
},
|
|
90
|
+
datetime: {
|
|
91
|
+
pattern: nil,
|
|
92
|
+
validator: lambda do |value|
|
|
93
|
+
DateTime.parse(value)
|
|
94
|
+
true
|
|
95
|
+
rescue ArgumentError, TypeError
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
},
|
|
99
|
+
time: {
|
|
100
|
+
pattern: nil,
|
|
101
|
+
validator: lambda do |value|
|
|
102
|
+
Time.parse(value)
|
|
103
|
+
true
|
|
104
|
+
rescue ArgumentError, TypeError
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
},
|
|
108
|
+
boolean: {
|
|
109
|
+
pattern: BOOLEAN_PATTERN,
|
|
110
|
+
validator: nil
|
|
111
|
+
}
|
|
112
|
+
}.freeze
|
|
113
|
+
|
|
114
|
+
# Validates that format is only used with string type attributes
|
|
115
|
+
# and that the format name is valid
|
|
116
|
+
#
|
|
117
|
+
# @raise [Treaty::Exceptions::Validation] If format is used with non-string type
|
|
118
|
+
# @raise [Treaty::Exceptions::Validation] If format name is unknown
|
|
119
|
+
# @return [void]
|
|
120
|
+
def validate_schema! # rubocop:disable Metrics/MethodLength
|
|
121
|
+
# Format option only works with string types
|
|
122
|
+
unless @attribute_type == :string
|
|
123
|
+
raise Treaty::Exceptions::Validation,
|
|
124
|
+
I18n.t(
|
|
125
|
+
"treaty.attributes.validators.format.type_mismatch",
|
|
126
|
+
attribute: @attribute_name,
|
|
127
|
+
type: @attribute_type
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
format_name = option_value
|
|
132
|
+
|
|
133
|
+
# Validate that format name exists
|
|
134
|
+
return if formats.key?(format_name)
|
|
135
|
+
|
|
136
|
+
raise Treaty::Exceptions::Validation,
|
|
137
|
+
I18n.t(
|
|
138
|
+
"treaty.attributes.validators.format.unknown_format",
|
|
139
|
+
attribute: @attribute_name,
|
|
140
|
+
format_name:,
|
|
141
|
+
allowed: formats.keys.join(", ")
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Validates that the value matches the specified format
|
|
146
|
+
# Skips validation for nil values (handled by RequiredValidator)
|
|
147
|
+
#
|
|
148
|
+
# @param value [String] The value to validate
|
|
149
|
+
# @raise [Treaty::Exceptions::Validation] If value doesn't match format
|
|
150
|
+
# @return [void]
|
|
151
|
+
def validate_value!(value) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
152
|
+
return if value.nil? # Format validation doesn't check for nil, required does.
|
|
153
|
+
|
|
154
|
+
format_name = option_value
|
|
155
|
+
format_definition = formats[format_name]
|
|
156
|
+
|
|
157
|
+
# Allow blank values (empty strings should be caught by required validator)
|
|
158
|
+
return if value.to_s.strip.empty?
|
|
159
|
+
|
|
160
|
+
# Apply pattern matching if defined
|
|
161
|
+
if format_definition.fetch(:pattern)
|
|
162
|
+
return if value.match?(format_definition.fetch(:pattern))
|
|
163
|
+
|
|
164
|
+
# Pattern failed, and no validator - raise error
|
|
165
|
+
unless format_definition.fetch(:validator)
|
|
166
|
+
attributes = {
|
|
167
|
+
attribute: @attribute_name,
|
|
168
|
+
value:,
|
|
169
|
+
format_name:
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
message = resolve_custom_message(**attributes) || default_message(**attributes)
|
|
173
|
+
|
|
174
|
+
raise Treaty::Exceptions::Validation, message
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Apply validator if defined
|
|
179
|
+
return unless format_definition.fetch(:validator)
|
|
180
|
+
return if format_definition.fetch(:validator).call(value)
|
|
181
|
+
|
|
182
|
+
attributes = {
|
|
183
|
+
attribute: @attribute_name,
|
|
184
|
+
value:,
|
|
185
|
+
format_name:
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
message = resolve_custom_message(**attributes) || default_message(**attributes)
|
|
189
|
+
|
|
190
|
+
raise Treaty::Exceptions::Validation, message
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
# Returns the available formats (allows for extension)
|
|
196
|
+
#
|
|
197
|
+
# @return [Hash] Hash of available formats with their definitions
|
|
198
|
+
def formats
|
|
199
|
+
DEFAULT_FORMATS
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Generates default error message for format validation failure using I18n
|
|
203
|
+
#
|
|
204
|
+
# @param attribute [Symbol] The attribute name
|
|
205
|
+
# @param value [Object] The actual value that failed validation
|
|
206
|
+
# @param format_name [Symbol] The format name
|
|
207
|
+
# @return [String] Default error message
|
|
208
|
+
def default_message(attribute:, value:, format_name:)
|
|
209
|
+
I18n.t(
|
|
210
|
+
"treaty.attributes.validators.format.mismatch",
|
|
211
|
+
attribute:,
|
|
212
|
+
value:,
|
|
213
|
+
format_name:
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
data/lib/treaty/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anton Sokolov
|
|
@@ -159,6 +159,7 @@ files:
|
|
|
159
159
|
- lib/treaty/attribute/option/modifiers/default_modifier.rb
|
|
160
160
|
- lib/treaty/attribute/option/registry.rb
|
|
161
161
|
- lib/treaty/attribute/option/registry_initializer.rb
|
|
162
|
+
- lib/treaty/attribute/option/validators/format_validator.rb
|
|
162
163
|
- lib/treaty/attribute/option/validators/inclusion_validator.rb
|
|
163
164
|
- lib/treaty/attribute/option/validators/required_validator.rb
|
|
164
165
|
- lib/treaty/attribute/option/validators/type_validator.rb
|