treaty 0.0.1 → 0.1.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 +19 -18
- data/Rakefile +4 -2
- data/lib/treaty/attribute/base.rb +172 -0
- data/lib/treaty/attribute/builder/base.rb +142 -0
- data/lib/treaty/attribute/collection.rb +65 -0
- data/lib/treaty/attribute/helper_mapper.rb +72 -0
- data/lib/treaty/attribute/option/base.rb +159 -0
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +87 -0
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +103 -0
- data/lib/treaty/attribute/option/registry.rb +128 -0
- data/lib/treaty/attribute/option/registry_initializer.rb +90 -0
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +80 -0
- data/lib/treaty/attribute/option/validators/required_validator.rb +94 -0
- data/lib/treaty/attribute/option/validators/type_validator.rb +153 -0
- data/lib/treaty/attribute/option_normalizer.rb +150 -0
- data/lib/treaty/attribute/option_orchestrator.rb +186 -0
- data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
- data/lib/treaty/attribute/validation/base.rb +93 -0
- data/lib/treaty/attribute/validation/nested_array_validator.rb +194 -0
- data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
- data/lib/treaty/attribute/validation/nested_transformer.rb +240 -0
- data/lib/treaty/attribute/validation/orchestrator/base.rb +196 -0
- data/lib/treaty/base.rb +9 -0
- data/lib/treaty/configuration.rb +17 -0
- data/lib/treaty/context/callable.rb +24 -0
- data/lib/treaty/context/dsl.rb +12 -0
- data/lib/treaty/context/workspace.rb +28 -0
- data/lib/treaty/controller/dsl.rb +38 -0
- data/lib/treaty/engine.rb +37 -0
- data/lib/treaty/exceptions/base.rb +8 -0
- data/lib/treaty/exceptions/class_name.rb +11 -0
- data/lib/treaty/exceptions/deprecated.rb +8 -0
- data/lib/treaty/exceptions/execution.rb +8 -0
- data/lib/treaty/exceptions/method_name.rb +8 -0
- data/lib/treaty/exceptions/nested_attributes.rb +8 -0
- data/lib/treaty/exceptions/strategy.rb +8 -0
- data/lib/treaty/exceptions/unexpected.rb +8 -0
- data/lib/treaty/exceptions/validation.rb +8 -0
- data/lib/treaty/info/builder.rb +122 -0
- data/lib/treaty/info/dsl.rb +26 -0
- data/lib/treaty/info/result.rb +13 -0
- data/lib/treaty/request/attribute/attribute.rb +24 -0
- data/lib/treaty/request/attribute/builder.rb +22 -0
- data/lib/treaty/request/attribute/validation/orchestrator.rb +27 -0
- data/lib/treaty/request/attribute/validator.rb +50 -0
- data/lib/treaty/request/factory.rb +32 -0
- data/lib/treaty/request/scope/collection.rb +21 -0
- data/lib/treaty/request/scope/factory.rb +42 -0
- data/lib/treaty/response/attribute/attribute.rb +24 -0
- data/lib/treaty/response/attribute/builder.rb +22 -0
- data/lib/treaty/response/attribute/validation/orchestrator.rb +27 -0
- data/lib/treaty/response/attribute/validator.rb +44 -0
- data/lib/treaty/response/factory.rb +38 -0
- data/lib/treaty/response/scope/collection.rb +21 -0
- data/lib/treaty/response/scope/factory.rb +42 -0
- data/lib/treaty/result.rb +22 -0
- data/lib/treaty/strategy.rb +31 -0
- data/lib/treaty/support/loader.rb +24 -0
- data/lib/treaty/version.rb +8 -1
- data/lib/treaty/versions/collection.rb +15 -0
- data/lib/treaty/versions/dsl.rb +30 -0
- data/lib/treaty/versions/execution/request.rb +151 -0
- data/lib/treaty/versions/executor.rb +14 -0
- data/lib/treaty/versions/factory.rb +93 -0
- data/lib/treaty/versions/resolver.rb +72 -0
- data/lib/treaty/versions/semantic.rb +22 -0
- data/lib/treaty/versions/workspace.rb +40 -0
- data/lib/treaty.rb +3 -3
- metadata +184 -27
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -84
- data/LICENSE.txt +0 -21
- data/sig/treaty.rbs +0 -4
- data/treaty.gemspec +0 -35
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Validation
|
|
6
|
+
# Base class for validation strategies (adapter vs non-adapter).
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Provides common interface for validation strategies used in Treaty.
|
|
11
|
+
# Subclasses implement specific validation logic for different strategies.
|
|
12
|
+
#
|
|
13
|
+
# ## Responsibilities
|
|
14
|
+
#
|
|
15
|
+
# 1. **Strategy Interface** - Defines common validation interface
|
|
16
|
+
# 2. **Factory Pattern** - Provides class-level validate! method
|
|
17
|
+
# 3. **Strategy Detection** - Checks if adapter strategy is active
|
|
18
|
+
#
|
|
19
|
+
# ## Subclasses
|
|
20
|
+
#
|
|
21
|
+
# - Request::Validation - Validates request data (uses Orchestrator::Request)
|
|
22
|
+
# - Response::Validation - Validates response data (uses Orchestrator::Response)
|
|
23
|
+
#
|
|
24
|
+
# ## Usage
|
|
25
|
+
#
|
|
26
|
+
# Subclasses must implement:
|
|
27
|
+
# - `validate!` - Performs validation and returns transformed data
|
|
28
|
+
#
|
|
29
|
+
# Example usage:
|
|
30
|
+
# Request::Validation.validate!(version_factory: factory, data: params)
|
|
31
|
+
#
|
|
32
|
+
# ## Strategy Pattern
|
|
33
|
+
#
|
|
34
|
+
# The validation system supports two strategies:
|
|
35
|
+
# - **Adapter Strategy** - Adapts between different API versions
|
|
36
|
+
# - **Standard Strategy** - Direct version handling
|
|
37
|
+
#
|
|
38
|
+
# This base class provides `adapter_strategy?` helper to check current strategy.
|
|
39
|
+
#
|
|
40
|
+
# ## Factory Method
|
|
41
|
+
#
|
|
42
|
+
# The `self.validate!(...)` class method provides a convenient factory pattern:
|
|
43
|
+
# ```ruby
|
|
44
|
+
# Request::Validation.validate!(version_factory: factory, data: params)
|
|
45
|
+
# # Equivalent to:
|
|
46
|
+
# Request::Validation.new(version_factory: factory).validate!(data: params)
|
|
47
|
+
# ```
|
|
48
|
+
#
|
|
49
|
+
# ## Architecture
|
|
50
|
+
#
|
|
51
|
+
# Works with:
|
|
52
|
+
# - VersionFactory - Provides version and strategy information
|
|
53
|
+
# - Orchestrator::Base - Performs actual validation and transformation
|
|
54
|
+
class Base
|
|
55
|
+
# Class-level factory method for validation
|
|
56
|
+
# Creates instance and calls validate!
|
|
57
|
+
#
|
|
58
|
+
# @param args [Hash] Arguments passed to initialize and validate!
|
|
59
|
+
# @return [Hash] Validated and transformed data
|
|
60
|
+
def self.validate!(...)
|
|
61
|
+
new(...).validate!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Creates a new validation instance
|
|
65
|
+
#
|
|
66
|
+
# @param version_factory [VersionFactory] Factory containing version and strategy
|
|
67
|
+
def initialize(version_factory:)
|
|
68
|
+
@version_factory = version_factory
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Performs validation and transformation
|
|
72
|
+
# Must be implemented in subclasses
|
|
73
|
+
#
|
|
74
|
+
# @raise [NotImplementedError] If subclass doesn't implement
|
|
75
|
+
# @return [Hash] Validated and transformed data
|
|
76
|
+
def validate!
|
|
77
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
78
|
+
raise Treaty::Exceptions::Validation,
|
|
79
|
+
"Subclass must implement the validate! method"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Checks if adapter strategy is active
|
|
85
|
+
#
|
|
86
|
+
# @return [Boolean] True if using adapter strategy
|
|
87
|
+
def adapter_strategy?
|
|
88
|
+
@version_factory.strategy_instance.adapter?
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Validation
|
|
6
|
+
# Validates array elements against nested attribute definitions.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Performs validation for nested array attributes during the validation phase.
|
|
11
|
+
# Handles both simple arrays (with :_self scope) and complex arrays (objects).
|
|
12
|
+
#
|
|
13
|
+
# ## Responsibilities
|
|
14
|
+
#
|
|
15
|
+
# 1. **Simple Array Validation** - Validates primitive values in arrays
|
|
16
|
+
# 2. **Complex Array Validation** - Validates hash objects within arrays
|
|
17
|
+
# 3. **Error Context** - Provides clear error messages with array index
|
|
18
|
+
# 4. **Type Checking** - Ensures elements match expected types
|
|
19
|
+
#
|
|
20
|
+
# ## Array Types
|
|
21
|
+
#
|
|
22
|
+
# ### Simple Array (`:_self` scope)
|
|
23
|
+
# Array containing primitive values (strings, integers, etc.)
|
|
24
|
+
#
|
|
25
|
+
# ```ruby
|
|
26
|
+
# array :tags do
|
|
27
|
+
# string :_self # Array of strings
|
|
28
|
+
# end
|
|
29
|
+
# ```
|
|
30
|
+
#
|
|
31
|
+
# Validates: `["ruby", "rails", "api"]`
|
|
32
|
+
#
|
|
33
|
+
# ### Complex Array (regular attributes)
|
|
34
|
+
# Array containing hash objects with defined structure
|
|
35
|
+
#
|
|
36
|
+
# ```ruby
|
|
37
|
+
# array :authors do
|
|
38
|
+
# string :name, :required
|
|
39
|
+
# string :email
|
|
40
|
+
# end
|
|
41
|
+
# ```
|
|
42
|
+
#
|
|
43
|
+
# Validates: `[{ name: "Alice", email: "alice@example.com" }, ...]`
|
|
44
|
+
#
|
|
45
|
+
# ## Usage
|
|
46
|
+
#
|
|
47
|
+
# Called by AttributeValidator for nested arrays:
|
|
48
|
+
#
|
|
49
|
+
# validator = NestedArrayValidator.new(attribute)
|
|
50
|
+
# validator.validate!(array_value)
|
|
51
|
+
#
|
|
52
|
+
# ## Error Handling
|
|
53
|
+
#
|
|
54
|
+
# Provides contextual error messages including:
|
|
55
|
+
# - Array attribute name
|
|
56
|
+
# - Element index (0-based)
|
|
57
|
+
# - Specific validation error
|
|
58
|
+
#
|
|
59
|
+
# Example error:
|
|
60
|
+
# "Error in array 'tags' at index 2: Element must match one of the defined types"
|
|
61
|
+
#
|
|
62
|
+
# ## Architecture
|
|
63
|
+
#
|
|
64
|
+
# Uses:
|
|
65
|
+
# - `AttributeValidator` - Validates individual elements
|
|
66
|
+
# - Caches validators for performance
|
|
67
|
+
# - Separates self validators from regular validators
|
|
68
|
+
class NestedArrayValidator
|
|
69
|
+
# Creates a new nested array validator
|
|
70
|
+
#
|
|
71
|
+
# @param attribute [Attribute::Base] The array-type attribute with nested attributes
|
|
72
|
+
def initialize(attribute)
|
|
73
|
+
@attribute = attribute
|
|
74
|
+
@self_validators = nil
|
|
75
|
+
@regular_validators = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Validates all items in an array
|
|
79
|
+
# Skips validation if value is not an Array
|
|
80
|
+
#
|
|
81
|
+
# @param array [Array] The array to validate
|
|
82
|
+
# @raise [Treaty::Exceptions::Validation] If any item validation fails
|
|
83
|
+
# @return [void]
|
|
84
|
+
def validate!(array)
|
|
85
|
+
return unless array.is_a?(Array)
|
|
86
|
+
|
|
87
|
+
array.each_with_index do |array_item, index|
|
|
88
|
+
validate_self_array_item!(array_item, index) if self_validators.any?
|
|
89
|
+
|
|
90
|
+
validate_regular_array_item!(array_item, index) if regular_validators.any?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Validates array item for simple arrays (with :_self scope)
|
|
97
|
+
# Simple array contains primitive values: strings, integers, datetimes, etc.
|
|
98
|
+
# Example: ["ruby", "rails", "api"] where each item is a String
|
|
99
|
+
#
|
|
100
|
+
# @param array_item [String, Integer, DateTime] Primitive value from simple array
|
|
101
|
+
# @param index [Integer] Array index for error messages
|
|
102
|
+
# @raise [Treaty::Exceptions::Validation] If primitive value doesn't match defined type
|
|
103
|
+
# @return [void]
|
|
104
|
+
def validate_self_array_item!(array_item, index) # rubocop:disable Metrics/MethodLength
|
|
105
|
+
errors = []
|
|
106
|
+
|
|
107
|
+
validated = self_validators.any? do |validator|
|
|
108
|
+
validator.validate_value!(array_item)
|
|
109
|
+
true
|
|
110
|
+
rescue Treaty::Exceptions::Validation => e
|
|
111
|
+
errors << e.message
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
return if validated
|
|
116
|
+
|
|
117
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
118
|
+
raise Treaty::Exceptions::Validation,
|
|
119
|
+
"Error in array '#{@attribute.name}' at index #{index}: " \
|
|
120
|
+
"Item must match one of the defined types. Errors: #{errors.join('; ')}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Validates array item for complex arrays (with regular attributes)
|
|
124
|
+
# Complex array contains hash objects with defined structure
|
|
125
|
+
# Example: [{ name: "Alice", email: "alice@example.com" }, ...] where each item is a Hash
|
|
126
|
+
#
|
|
127
|
+
# @param array_item [Hash] Hash object from complex array
|
|
128
|
+
# @param index [Integer] Array index for error messages
|
|
129
|
+
# @raise [Treaty::Exceptions::Validation] If item is not Hash or nested validation fails
|
|
130
|
+
# @return [void]
|
|
131
|
+
def validate_regular_array_item!(array_item, index) # rubocop:disable Metrics/MethodLength
|
|
132
|
+
unless array_item.is_a?(Hash)
|
|
133
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
134
|
+
raise Treaty::Exceptions::Validation,
|
|
135
|
+
"Error in array '#{@attribute.name}' at index #{index}: Expected Hash but got #{array_item.class}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
regular_validators.each do |nested_attribute, validator|
|
|
139
|
+
nested_value = array_item.fetch(nested_attribute.name, nil)
|
|
140
|
+
validator.validate_value!(nested_value)
|
|
141
|
+
rescue Treaty::Exceptions::Validation => e
|
|
142
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
143
|
+
raise Treaty::Exceptions::Validation,
|
|
144
|
+
"Error in array '#{@attribute.name}' at index #{index}: #{e.message}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
########################################################################
|
|
149
|
+
|
|
150
|
+
# Gets cached self validators or builds them
|
|
151
|
+
#
|
|
152
|
+
# @return [Array<AttributeValidator>] Validators for :_self attributes
|
|
153
|
+
def self_validators
|
|
154
|
+
@self_validators ||= build_self_validators
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Gets cached regular validators or builds them
|
|
158
|
+
#
|
|
159
|
+
# @return [Hash] Hash of nested_attribute => validator
|
|
160
|
+
def regular_validators
|
|
161
|
+
@regular_validators ||= build_regular_validators
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
########################################################################
|
|
165
|
+
|
|
166
|
+
# Builds validators for :_self attributes (simple array elements)
|
|
167
|
+
#
|
|
168
|
+
# @return [Array<AttributeValidator>] Array of validators
|
|
169
|
+
def build_self_validators
|
|
170
|
+
@attribute.collection_of_attributes
|
|
171
|
+
.select { |attr| attr.name == :_self }
|
|
172
|
+
.map do |self_attribute|
|
|
173
|
+
validator = AttributeValidator.new(self_attribute)
|
|
174
|
+
validator.validate_schema!
|
|
175
|
+
validator
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Builds validators for regular attributes (complex array elements)
|
|
180
|
+
#
|
|
181
|
+
# @return [Hash] Hash of nested_attribute => validator
|
|
182
|
+
def build_regular_validators
|
|
183
|
+
@attribute.collection_of_attributes
|
|
184
|
+
.reject { |attr| attr.name == :_self }
|
|
185
|
+
.each_with_object({}) do |nested_attribute, cache|
|
|
186
|
+
validator = AttributeValidator.new(nested_attribute)
|
|
187
|
+
validator.validate_schema!
|
|
188
|
+
cache[nested_attribute] = validator
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Validation
|
|
6
|
+
# Validates nested object (hash) attributes against their defined structure.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Performs validation for nested object attributes during the validation phase.
|
|
11
|
+
# Ensures hash values conform to the nested attribute definitions.
|
|
12
|
+
#
|
|
13
|
+
# ## Responsibilities
|
|
14
|
+
#
|
|
15
|
+
# 1. **Structure Validation** - Validates hash structure matches definition
|
|
16
|
+
# 2. **Attribute Validation** - Validates each nested attribute's value
|
|
17
|
+
# 3. **Type Safety** - Ensures value is a Hash before validation
|
|
18
|
+
# 4. **Validator Caching** - Builds and caches validators for performance
|
|
19
|
+
#
|
|
20
|
+
# ## Usage
|
|
21
|
+
#
|
|
22
|
+
# Used for object-type attributes with nested definitions:
|
|
23
|
+
#
|
|
24
|
+
# ```ruby
|
|
25
|
+
# object :author do
|
|
26
|
+
# string :name, :required
|
|
27
|
+
# string :email
|
|
28
|
+
# integer :age
|
|
29
|
+
# end
|
|
30
|
+
# ```
|
|
31
|
+
#
|
|
32
|
+
# Validates: `{ name: "Alice", email: "alice@example.com", age: 30 }`
|
|
33
|
+
#
|
|
34
|
+
# ## Usage in Code
|
|
35
|
+
#
|
|
36
|
+
# Called by AttributeValidator for nested objects:
|
|
37
|
+
#
|
|
38
|
+
# validator = NestedObjectValidator.new(attribute)
|
|
39
|
+
# validator.validate!(hash_value)
|
|
40
|
+
#
|
|
41
|
+
# ## Validation Flow
|
|
42
|
+
#
|
|
43
|
+
# 1. Check if value is a Hash
|
|
44
|
+
# 2. Build validators for all nested attributes (cached)
|
|
45
|
+
# 3. For each nested attribute:
|
|
46
|
+
# - Extract value from hash
|
|
47
|
+
# - Validate using AttributeValidator
|
|
48
|
+
# 4. Raise exception if any validation fails
|
|
49
|
+
#
|
|
50
|
+
# ## Architecture
|
|
51
|
+
#
|
|
52
|
+
# Uses:
|
|
53
|
+
# - `AttributeValidator` - Validates individual nested attributes
|
|
54
|
+
# - Caches validators to avoid rebuilding on each validation
|
|
55
|
+
class NestedObjectValidator
|
|
56
|
+
attr_reader :attribute
|
|
57
|
+
|
|
58
|
+
# Creates a new nested object validator
|
|
59
|
+
#
|
|
60
|
+
# @param attribute [Attribute::Base] The object-type attribute with nested attributes
|
|
61
|
+
def initialize(attribute)
|
|
62
|
+
@attribute = attribute
|
|
63
|
+
@validators_cache = nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Validates all nested attributes in a hash
|
|
67
|
+
# Skips validation if value is not a Hash
|
|
68
|
+
#
|
|
69
|
+
# @param hash [Hash] The hash to validate
|
|
70
|
+
# @raise [Treaty::Exceptions::Validation] If any nested validation fails
|
|
71
|
+
# @return [void]
|
|
72
|
+
def validate!(hash)
|
|
73
|
+
return unless hash.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
validators.each do |nested_attribute, nested_validator|
|
|
76
|
+
nested_value = hash.fetch(nested_attribute.name, nil)
|
|
77
|
+
nested_validator.validate_value!(nested_value)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Gets cached validators or builds them
|
|
84
|
+
#
|
|
85
|
+
# @return [Hash] Hash of nested_attribute => validator
|
|
86
|
+
def validators
|
|
87
|
+
@validators ||= build_validators
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Builds validators for all nested attributes
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash] Hash of nested_attribute => validator
|
|
93
|
+
def build_validators
|
|
94
|
+
attribute.collection_of_attributes.each_with_object({}) do |nested_attribute, cache|
|
|
95
|
+
validator = AttributeValidator.new(nested_attribute)
|
|
96
|
+
validator.validate_schema!
|
|
97
|
+
cache[nested_attribute] = validator
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Validation
|
|
6
|
+
# Handles transformation of nested attributes (objects and arrays).
|
|
7
|
+
# Extracted from Orchestrator::Base to reduce complexity.
|
|
8
|
+
class NestedTransformer
|
|
9
|
+
SELF_SCOPE = :_self
|
|
10
|
+
private_constant :SELF_SCOPE
|
|
11
|
+
|
|
12
|
+
attr_reader :attribute
|
|
13
|
+
|
|
14
|
+
# Creates a new nested transformer
|
|
15
|
+
#
|
|
16
|
+
# @param attribute [Attribute::Base] The attribute with nested structure
|
|
17
|
+
def initialize(attribute)
|
|
18
|
+
@attribute = attribute
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Transforms nested attribute value (object or array)
|
|
22
|
+
# Returns original value if nil or not nested
|
|
23
|
+
#
|
|
24
|
+
# @param value [Object] The value to transform
|
|
25
|
+
# @return [Object] Transformed value
|
|
26
|
+
def transform(value)
|
|
27
|
+
return value if value.nil?
|
|
28
|
+
|
|
29
|
+
case attribute.type
|
|
30
|
+
when :object
|
|
31
|
+
transform_object(value)
|
|
32
|
+
when :array
|
|
33
|
+
transform_array(value)
|
|
34
|
+
else
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Transforms object (hash) value
|
|
42
|
+
#
|
|
43
|
+
# @param value [Hash] The hash to transform
|
|
44
|
+
# @return [Hash] Transformed hash
|
|
45
|
+
def transform_object(value)
|
|
46
|
+
return value unless attribute.nested?
|
|
47
|
+
|
|
48
|
+
transformer = ObjectTransformer.new(attribute)
|
|
49
|
+
transformer.transform(value)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Transforms array value
|
|
53
|
+
#
|
|
54
|
+
# @param value [Array] The array to transform
|
|
55
|
+
# @return [Array] Transformed array
|
|
56
|
+
def transform_array(value)
|
|
57
|
+
return value unless attribute.nested?
|
|
58
|
+
|
|
59
|
+
transformer = ArrayTransformer.new(attribute)
|
|
60
|
+
transformer.transform(value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Transforms object (hash) with nested attributes
|
|
64
|
+
class ObjectTransformer
|
|
65
|
+
attr_reader :attribute
|
|
66
|
+
|
|
67
|
+
# Creates a new object transformer
|
|
68
|
+
#
|
|
69
|
+
# @param attribute [Attribute::Base] The object-type attribute
|
|
70
|
+
def initialize(attribute)
|
|
71
|
+
@attribute = attribute
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Transforms hash by processing all nested attributes
|
|
75
|
+
#
|
|
76
|
+
# @param value [Hash] The source hash
|
|
77
|
+
# @return [Hash] Transformed hash with processed attributes
|
|
78
|
+
def transform(value)
|
|
79
|
+
transformed = {}
|
|
80
|
+
|
|
81
|
+
attribute.collection_of_attributes.each do |nested_attribute|
|
|
82
|
+
process_attribute(nested_attribute, value, transformed)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
transformed
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Processes a single nested attribute
|
|
91
|
+
# Validates, transforms, and adds to target hash
|
|
92
|
+
#
|
|
93
|
+
# @param nested_attribute [Attribute::Base] Attribute to process
|
|
94
|
+
# @param source_hash [Hash] Source data
|
|
95
|
+
# @param target_hash [Hash] Target hash to populate
|
|
96
|
+
# @return [void]
|
|
97
|
+
def process_attribute(nested_attribute, source_hash, target_hash) # rubocop:disable Metrics/MethodLength
|
|
98
|
+
source_name = nested_attribute.name
|
|
99
|
+
nested_value = source_hash.fetch(source_name, nil)
|
|
100
|
+
|
|
101
|
+
validator = AttributeValidator.new(nested_attribute)
|
|
102
|
+
validator.validate_schema!
|
|
103
|
+
|
|
104
|
+
transformed_value = if nested_attribute.nested?
|
|
105
|
+
nested_transformer = NestedTransformer.new(nested_attribute)
|
|
106
|
+
validator.validate_type!(nested_value) unless nested_value.nil?
|
|
107
|
+
validator.validate_required!(nested_value)
|
|
108
|
+
nested_transformer.transform(nested_value)
|
|
109
|
+
else
|
|
110
|
+
validator.validate_value!(nested_value)
|
|
111
|
+
validator.transform_value(nested_value)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
target_name = validator.target_name
|
|
115
|
+
target_hash[target_name] = transformed_value
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Transforms array with nested attributes
|
|
120
|
+
class ArrayTransformer
|
|
121
|
+
SELF_SCOPE = :_self
|
|
122
|
+
private_constant :SELF_SCOPE
|
|
123
|
+
|
|
124
|
+
attr_reader :attribute
|
|
125
|
+
|
|
126
|
+
# Creates a new array transformer
|
|
127
|
+
#
|
|
128
|
+
# @param attribute [Attribute::Base] The array-type attribute
|
|
129
|
+
def initialize(attribute)
|
|
130
|
+
@attribute = attribute
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Transforms array by processing each element
|
|
134
|
+
# Handles both simple arrays (:_self) and complex arrays (objects)
|
|
135
|
+
#
|
|
136
|
+
# @param value [Array] The source array
|
|
137
|
+
# @return [Array] Transformed array
|
|
138
|
+
def transform(value)
|
|
139
|
+
value.each_with_index.map do |item, index|
|
|
140
|
+
if simple_array?
|
|
141
|
+
validate_simple_element(item, index)
|
|
142
|
+
item
|
|
143
|
+
else
|
|
144
|
+
transform_array_item(item, index)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
# Checks if this is a simple array (primitive values)
|
|
152
|
+
#
|
|
153
|
+
# @return [Boolean] True if array contains primitive values with :_self scope
|
|
154
|
+
def simple_array?
|
|
155
|
+
attribute.collection_of_attributes.size == 1 &&
|
|
156
|
+
attribute.collection_of_attributes.first.name == SELF_SCOPE
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Validates a simple array element (primitive value)
|
|
160
|
+
#
|
|
161
|
+
# @param item [Object] Array element to validate
|
|
162
|
+
# @param index [Integer] Element index for error messages
|
|
163
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
164
|
+
# @return [void]
|
|
165
|
+
def validate_simple_element(item, index) # rubocop:disable Metrics/MethodLength
|
|
166
|
+
self_attr = attribute.collection_of_attributes.first
|
|
167
|
+
validator = AttributeValidator.new(self_attr)
|
|
168
|
+
validator.validate_schema!
|
|
169
|
+
|
|
170
|
+
begin
|
|
171
|
+
validator.validate_value!(item)
|
|
172
|
+
rescue Treaty::Exceptions::Validation => e
|
|
173
|
+
raise Treaty::Exceptions::Validation,
|
|
174
|
+
"Error in array '#{attribute.name}' at index #{index}: " \
|
|
175
|
+
"Element must match one of the defined types. " \
|
|
176
|
+
"Errors: #{e.message}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Transforms a complex array element (hash object)
|
|
181
|
+
#
|
|
182
|
+
# @param item [Hash] Array element to transform
|
|
183
|
+
# @param index [Integer] Element index for error messages
|
|
184
|
+
# @raise [Treaty::Exceptions::Validation] If item is not a Hash
|
|
185
|
+
# @return [Hash] Transformed hash
|
|
186
|
+
def transform_array_item(item, index)
|
|
187
|
+
unless item.is_a?(Hash)
|
|
188
|
+
raise Treaty::Exceptions::Validation,
|
|
189
|
+
"Error in array '#{attribute.name}' at index #{index}: " \
|
|
190
|
+
"Expected Hash but got #{item.class}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
transformed = {}
|
|
194
|
+
|
|
195
|
+
attribute.collection_of_attributes.each do |nested_attribute|
|
|
196
|
+
process_attribute(nested_attribute, item, transformed, index)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
transformed
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Processes a single nested attribute in array element
|
|
203
|
+
# Validates, transforms, and adds to target hash
|
|
204
|
+
#
|
|
205
|
+
# @param nested_attribute [Attribute::Base] Attribute to process
|
|
206
|
+
# @param source_hash [Hash] Source data
|
|
207
|
+
# @param target_hash [Hash] Target hash to populate
|
|
208
|
+
# @param index [Integer] Array index for error messages
|
|
209
|
+
# @raise [Treaty::Exceptions::Validation] If validation fails
|
|
210
|
+
# @return [void]
|
|
211
|
+
def process_attribute(nested_attribute, source_hash, target_hash, index) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
212
|
+
source_name = nested_attribute.name
|
|
213
|
+
nested_value = source_hash.fetch(source_name, nil)
|
|
214
|
+
|
|
215
|
+
validator = AttributeValidator.new(nested_attribute)
|
|
216
|
+
validator.validate_schema!
|
|
217
|
+
|
|
218
|
+
begin
|
|
219
|
+
transformed_value = if nested_attribute.nested?
|
|
220
|
+
nested_transformer = NestedTransformer.new(nested_attribute)
|
|
221
|
+
validator.validate_type!(nested_value) unless nested_value.nil?
|
|
222
|
+
validator.validate_required!(nested_value)
|
|
223
|
+
nested_transformer.transform(nested_value)
|
|
224
|
+
else
|
|
225
|
+
validator.validate_value!(nested_value)
|
|
226
|
+
validator.transform_value(nested_value)
|
|
227
|
+
end
|
|
228
|
+
rescue Treaty::Exceptions::Validation => e
|
|
229
|
+
raise Treaty::Exceptions::Validation,
|
|
230
|
+
"Error in array '#{attribute.name}' at index #{index}: #{e.message}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
target_name = validator.target_name
|
|
234
|
+
target_hash[target_name] = transformed_value
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|