treaty 0.7.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 +4 -4
- data/README.md +5 -0
- data/config/locales/en.yml +8 -0
- data/lib/treaty/attribute/base.rb +13 -5
- data/lib/treaty/attribute/dsl.rb +90 -0
- data/lib/treaty/attribute/entity/attribute.rb +25 -0
- data/lib/treaty/attribute/entity/builder.rb +23 -0
- data/lib/treaty/attribute/option/base.rb +17 -1
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +5 -3
- data/lib/treaty/attribute/option/registry_initializer.rb +2 -0
- data/lib/treaty/attribute/option/validators/format_validator.rb +220 -0
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +20 -8
- data/lib/treaty/attribute/option/validators/required_validator.rb +8 -2
- data/lib/treaty/attribute/option/validators/type_validator.rb +51 -40
- data/lib/treaty/attribute/option_orchestrator.rb +7 -5
- data/lib/treaty/attribute/validation/nested_array_validator.rb +18 -12
- data/lib/treaty/attribute/validation/nested_transformer.rb +18 -12
- data/lib/treaty/base.rb +1 -1
- data/lib/treaty/controller/dsl.rb +4 -1
- data/lib/treaty/entity.rb +84 -0
- data/lib/treaty/info/entity/builder.rb +50 -0
- data/lib/treaty/info/entity/dsl.rb +28 -0
- data/lib/treaty/info/entity/result.rb +15 -0
- data/lib/treaty/info/rest/builder.rb +110 -0
- data/lib/treaty/info/rest/dsl.rb +28 -0
- data/lib/treaty/info/rest/result.rb +15 -0
- data/lib/treaty/request/attribute/attribute.rb +1 -0
- data/lib/treaty/request/attribute/builder.rb +1 -0
- data/lib/treaty/request/entity.rb +33 -0
- data/lib/treaty/request/factory.rb +61 -14
- data/lib/treaty/request/validator.rb +65 -0
- data/lib/treaty/response/attribute/attribute.rb +1 -0
- data/lib/treaty/response/attribute/builder.rb +1 -0
- data/lib/treaty/response/entity.rb +33 -0
- data/lib/treaty/response/factory.rb +61 -14
- data/lib/treaty/response/validator.rb +57 -0
- data/lib/treaty/version.rb +1 -1
- data/lib/treaty/versions/execution/request.rb +10 -5
- data/lib/treaty/versions/factory.rb +16 -5
- data/lib/treaty/versions/resolver.rb +8 -2
- data/lib/treaty/versions/workspace.rb +2 -2
- metadata +16 -8
- data/lib/treaty/info/builder.rb +0 -108
- data/lib/treaty/info/dsl.rb +0 -26
- data/lib/treaty/info/result.rb +0 -13
- data/lib/treaty/request/attribute/validation/orchestrator.rb +0 -19
- data/lib/treaty/request/attribute/validator.rb +0 -50
- data/lib/treaty/response/attribute/validation/orchestrator.rb +0 -19
- data/lib/treaty/response/attribute/validator.rb +0 -44
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/README.md
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
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.5.0"`) until the 1.0 release.
|
|
17
|
+
|
|
15
18
|
## 📚 Documentation
|
|
16
19
|
|
|
17
20
|
Explore comprehensive guides and documentation at [docs](./docs):
|
|
@@ -29,6 +32,8 @@ Treaty provides a complete solution for building versioned APIs in Ruby on Rails
|
|
|
29
32
|
|
|
30
33
|
- **Type Safety** - Enforce strict type checking for request and response data
|
|
31
34
|
- **API Versioning** - Manage multiple concurrent API versions effortlessly
|
|
35
|
+
- **Unified Architecture** - Request blocks, response blocks, and Entity classes share the same validation system
|
|
36
|
+
- **Entity Classes (DTOs)** - Define reusable data transfer objects for better code organization
|
|
32
37
|
- **Built-in Validation** - Validate incoming requests and outgoing responses automatically
|
|
33
38
|
- **Data Transformation** - Transform data seamlessly between different API versions
|
|
34
39
|
- **Deprecation Management** - Mark versions as deprecated with flexible conditions
|
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
|
|
@@ -47,6 +52,7 @@ en:
|
|
|
47
52
|
# Attribute builder DSL
|
|
48
53
|
builder:
|
|
49
54
|
not_implemented: "%{class} must implement #create_attribute"
|
|
55
|
+
create_attribute_not_implemented: "Subclass %{class} must implement #create_attribute method"
|
|
50
56
|
|
|
51
57
|
# Attribute-level errors
|
|
52
58
|
errors:
|
|
@@ -61,6 +67,7 @@ en:
|
|
|
61
67
|
# Request factory DSL
|
|
62
68
|
factory:
|
|
63
69
|
unknown_method: "Unknown method '%{method}' in request definition. Use 'object :name do ... end' to define request structure"
|
|
70
|
+
invalid_entity_class: "Request expects a Treaty::Entity subclass, got %{type}: %{value}"
|
|
64
71
|
|
|
65
72
|
# ============================================================================
|
|
66
73
|
# Response: Response definition and structure
|
|
@@ -69,6 +76,7 @@ en:
|
|
|
69
76
|
# Response factory DSL
|
|
70
77
|
factory:
|
|
71
78
|
unknown_method: "Unknown method '%{method}' in response definition. Use 'object :name do ... end' to define response structure"
|
|
79
|
+
invalid_entity_class: "Response expects a Treaty::Entity subclass, got %{type}: %{value}"
|
|
72
80
|
|
|
73
81
|
# ============================================================================
|
|
74
82
|
# Versioning: API version management and resolution
|
|
@@ -122,9 +122,11 @@ module Treaty
|
|
|
122
122
|
return unless @nesting_level > Treaty::Engine.config.treaty.attribute_nesting_level
|
|
123
123
|
|
|
124
124
|
raise Treaty::Exceptions::NestedAttributes,
|
|
125
|
-
I18n.t(
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
I18n.t(
|
|
126
|
+
"treaty.attributes.errors.nesting_level_exceeded",
|
|
127
|
+
level: @nesting_level,
|
|
128
|
+
max_level: Treaty::Engine.config.treaty.attribute_nesting_level
|
|
129
|
+
)
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
# Extracts helper symbols from arguments
|
|
@@ -155,7 +157,10 @@ module Treaty
|
|
|
155
157
|
def apply_defaults!
|
|
156
158
|
# Must be implemented in subclasses
|
|
157
159
|
raise Treaty::Exceptions::NotImplemented,
|
|
158
|
-
I18n.t(
|
|
160
|
+
I18n.t(
|
|
161
|
+
"treaty.attributes.errors.apply_defaults_not_implemented",
|
|
162
|
+
class: self.class
|
|
163
|
+
)
|
|
159
164
|
end
|
|
160
165
|
|
|
161
166
|
# Processes nested attributes block for object/array types
|
|
@@ -167,7 +172,10 @@ module Treaty
|
|
|
167
172
|
def process_nested_attributes
|
|
168
173
|
# Must be implemented in subclasses
|
|
169
174
|
raise Treaty::Exceptions::NotImplemented,
|
|
170
|
-
I18n.t(
|
|
175
|
+
I18n.t(
|
|
176
|
+
"treaty.attributes.errors.process_nested_not_implemented",
|
|
177
|
+
class: self.class
|
|
178
|
+
)
|
|
171
179
|
end
|
|
172
180
|
end
|
|
173
181
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
# DSL module for defining attributes in Entity-like classes.
|
|
6
|
+
#
|
|
7
|
+
# This module provides the class-level DSL for defining attributes.
|
|
8
|
+
# It can be included in any class that needs attribute definition capabilities.
|
|
9
|
+
#
|
|
10
|
+
# ## Usage
|
|
11
|
+
#
|
|
12
|
+
# ```ruby
|
|
13
|
+
# class MyEntity
|
|
14
|
+
# include Treaty::Attribute::DSL
|
|
15
|
+
#
|
|
16
|
+
# string :name
|
|
17
|
+
# integer :age
|
|
18
|
+
# end
|
|
19
|
+
# ```
|
|
20
|
+
module DSL
|
|
21
|
+
def self.included(base)
|
|
22
|
+
base.extend(ClassMethods)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
# Defines an attribute with explicit type
|
|
27
|
+
#
|
|
28
|
+
# @param name [Symbol] The attribute name
|
|
29
|
+
# @param type [Symbol] The attribute type
|
|
30
|
+
# @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
|
|
31
|
+
# @param options [Hash] Attribute options
|
|
32
|
+
# @param block [Proc] Block for nested attributes
|
|
33
|
+
# @return [void]
|
|
34
|
+
def attribute(name, type, *helpers, **options, &block)
|
|
35
|
+
collection_of_attributes << create_attribute(
|
|
36
|
+
name,
|
|
37
|
+
type,
|
|
38
|
+
*helpers,
|
|
39
|
+
nesting_level: 0,
|
|
40
|
+
**options,
|
|
41
|
+
&block
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns collection of attributes for this class
|
|
46
|
+
#
|
|
47
|
+
# @return [Collection] Collection of attributes
|
|
48
|
+
def collection_of_attributes
|
|
49
|
+
@collection_of_attributes ||= Treaty::Attribute::Collection.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Handles DSL methods like `string :name` where method name is the type
|
|
53
|
+
#
|
|
54
|
+
# @param type [Symbol] The attribute type (method name)
|
|
55
|
+
# @param name [Symbol] The attribute name (first argument)
|
|
56
|
+
# @param helpers [Array<Symbol>] Helper symbols
|
|
57
|
+
# @param options [Hash] Attribute options
|
|
58
|
+
# @param block [Proc] Block for nested attributes
|
|
59
|
+
# @return [void]
|
|
60
|
+
def method_missing(type, *helpers, **options, &block)
|
|
61
|
+
name = helpers.shift
|
|
62
|
+
|
|
63
|
+
# If no attribute name provided, this is not an attribute definition
|
|
64
|
+
# Pass to super to handle it properly (e.g., for methods like 'info', 'call!', etc.)
|
|
65
|
+
return super if name.nil?
|
|
66
|
+
|
|
67
|
+
attribute(name, type, *helpers, **options, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def respond_to_missing?(name, *)
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Creates an attribute instance (must be implemented by including class)
|
|
77
|
+
#
|
|
78
|
+
# @raise [Treaty::Exceptions::NotImplemented] If not implemented
|
|
79
|
+
# @return [Attribute::Base] Created attribute instance
|
|
80
|
+
def create_attribute(*)
|
|
81
|
+
raise Treaty::Exceptions::NotImplemented,
|
|
82
|
+
I18n.t(
|
|
83
|
+
"treaty.attributes.dsl.create_attribute_not_implemented",
|
|
84
|
+
class: self
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Entity
|
|
6
|
+
# Entity-specific attribute that defaults to required: true
|
|
7
|
+
class Attribute < Treaty::Attribute::Base
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def apply_defaults!
|
|
11
|
+
# For entity: required by default (true).
|
|
12
|
+
# message: nil means use I18n default message from validators
|
|
13
|
+
@options[:required] ||= { is: true, message: nil }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process_nested_attributes(&block)
|
|
17
|
+
return unless object_or_array?
|
|
18
|
+
|
|
19
|
+
builder = Builder.new(collection_of_attributes, @nesting_level + 1)
|
|
20
|
+
builder.instance_eval(&block)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Entity
|
|
6
|
+
# Entity-specific attribute builder
|
|
7
|
+
class Builder < Treaty::Attribute::Builder::Base
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def create_attribute(name, type, *helpers, nesting_level:, **options, &block)
|
|
11
|
+
Attribute.new(
|
|
12
|
+
name,
|
|
13
|
+
type,
|
|
14
|
+
*helpers,
|
|
15
|
+
nesting_level:,
|
|
16
|
+
**options,
|
|
17
|
+
&block
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -134,13 +134,29 @@ module Treaty
|
|
|
134
134
|
# Gets custom error message from advanced mode schema
|
|
135
135
|
# Returns nil if no custom message, which triggers I18n default message
|
|
136
136
|
#
|
|
137
|
-
# @return [String, nil] Custom error message or nil for default message
|
|
137
|
+
# @return [String, Proc, nil] Custom error message, lambda, or nil for default message
|
|
138
138
|
def custom_message
|
|
139
139
|
return nil unless @option_schema.is_a?(Hash)
|
|
140
140
|
|
|
141
141
|
@option_schema.fetch(:message, nil)
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
# Resolves custom message with lambda support
|
|
145
|
+
# If message is a lambda, calls it with provided named arguments
|
|
146
|
+
#
|
|
147
|
+
# @param context [Hash] Named arguments to pass to lambda
|
|
148
|
+
# @return [String, nil] Resolved message string or nil
|
|
149
|
+
def resolve_custom_message(**context)
|
|
150
|
+
message = custom_message
|
|
151
|
+
return nil if message.nil?
|
|
152
|
+
|
|
153
|
+
if message.respond_to?(:call)
|
|
154
|
+
message.call(**context)
|
|
155
|
+
else
|
|
156
|
+
message
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
144
160
|
# Checks if schema is in advanced mode
|
|
145
161
|
#
|
|
146
162
|
# @return [Boolean] True if schema is in advanced mode (hash with value key)
|
|
@@ -54,9 +54,11 @@ module Treaty
|
|
|
54
54
|
return if target.is_a?(Symbol)
|
|
55
55
|
|
|
56
56
|
raise Treaty::Exceptions::Validation,
|
|
57
|
-
I18n.t(
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
I18n.t(
|
|
58
|
+
"treaty.attributes.modifiers.as.invalid_type",
|
|
59
|
+
attribute: @attribute_name,
|
|
60
|
+
type: target.class
|
|
61
|
+
)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
# Indicates that AsModifier transforms attribute names
|
|
@@ -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
|
|
@@ -29,7 +29,10 @@ module Treaty
|
|
|
29
29
|
return if allowed_values.is_a?(Array) && !allowed_values.empty?
|
|
30
30
|
|
|
31
31
|
raise Treaty::Exceptions::Validation,
|
|
32
|
-
I18n.t(
|
|
32
|
+
I18n.t(
|
|
33
|
+
"treaty.attributes.validators.inclusion.invalid_schema",
|
|
34
|
+
attribute: @attribute_name
|
|
35
|
+
)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# Validates that value is included in allowed set
|
|
@@ -45,7 +48,13 @@ module Treaty
|
|
|
45
48
|
|
|
46
49
|
return if allowed_values.include?(value)
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
attributes = {
|
|
52
|
+
attribute: @attribute_name,
|
|
53
|
+
value:,
|
|
54
|
+
allowed_values:
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
message = resolve_custom_message(**attributes) || default_message(**attributes)
|
|
49
58
|
|
|
50
59
|
raise Treaty::Exceptions::Validation, message
|
|
51
60
|
end
|
|
@@ -64,14 +73,17 @@ module Treaty
|
|
|
64
73
|
|
|
65
74
|
# Generates default error message with allowed values using I18n
|
|
66
75
|
#
|
|
67
|
-
# @param
|
|
76
|
+
# @param attribute [Symbol] The attribute name
|
|
68
77
|
# @param value [Object] The actual value that failed validation
|
|
78
|
+
# @param allowed_values [Array] Array of allowed values
|
|
69
79
|
# @return [String] Default error message
|
|
70
|
-
def default_message(
|
|
71
|
-
I18n.t(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
def default_message(attribute:, value:, allowed_values:)
|
|
81
|
+
I18n.t(
|
|
82
|
+
"treaty.attributes.validators.inclusion.not_included",
|
|
83
|
+
attribute:,
|
|
84
|
+
allowed: allowed_values.join(", "),
|
|
85
|
+
value:
|
|
86
|
+
)
|
|
75
87
|
end
|
|
76
88
|
end
|
|
77
89
|
end
|
|
@@ -51,7 +51,10 @@ module Treaty
|
|
|
51
51
|
return unless required?
|
|
52
52
|
return if present?(value)
|
|
53
53
|
|
|
54
|
-
message =
|
|
54
|
+
message = resolve_custom_message(
|
|
55
|
+
attribute: @attribute_name,
|
|
56
|
+
value:
|
|
57
|
+
) || default_message
|
|
55
58
|
|
|
56
59
|
raise Treaty::Exceptions::Validation, message
|
|
57
60
|
end
|
|
@@ -83,7 +86,10 @@ module Treaty
|
|
|
83
86
|
#
|
|
84
87
|
# @return [String] Default error message
|
|
85
88
|
def default_message
|
|
86
|
-
I18n.t(
|
|
89
|
+
I18n.t(
|
|
90
|
+
"treaty.attributes.validators.required.blank",
|
|
91
|
+
attribute: @attribute_name
|
|
92
|
+
)
|
|
87
93
|
end
|
|
88
94
|
end
|
|
89
95
|
end
|