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: 44bc6d4e290eabdc45efdc7d1e73003f2bb1077e4eee99ad2e9912852d87ecb4
4
- data.tar.gz: 72d4767ab3b9e69ec5eec87429cfabd54e6e82783ac4ea321b99eeb04eb85c07
3
+ metadata.gz: 8fd8fd4a097f7ff97cdb53e0d7825d79da5562d0bb6347e8e40794c158f8cb41
4
+ data.tar.gz: b0e119a2ba1018b4eeeb49c753b2949fb4c320ca89be29d633e7e6367cbd7360
5
5
  SHA512:
6
- metadata.gz: c5d922b7116ebb82cf7d9c60d8bca8c9f45594744cc10e19319f0b3e22d631f8310a1695861f7979f8bf472b87b8bf5687248faae8ca93f90bec04e6fcfa7fd1
7
- data.tar.gz: 2fbebe3ceaeeaf1bd743f36fbd40678bda0f9c8dbdbd284610f2de4a0101c6e8d865fa11f555b1fa195fd37e06eae519f82aa08fae8fb29694296ad65fa42be7
6
+ metadata.gz: 10514553e41066a04e066e6561744f69ca779bc4a87594bab7dce48b7e6217a5d5a6f2b7e28f1770876a9d3ff82d2f715cf4d167ef683dc36723b65a1549dc62
7
+ data.tar.gz: e262a6a4b2dbe068c53ced57ddcd7d9687a8e0be7d13f90ab8c2b9176015a3099829af702f59d22d28588a8c929e7178746d79356c35d4d0e899cc46991ce439
@@ -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
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 8
6
+ MINOR = 9
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.8.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