treaty 0.18.0 → 0.20.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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/locales/en.yml +3 -3
  4. data/lib/treaty/action/base.rb +11 -0
  5. data/lib/treaty/action/context/callable.rb +90 -0
  6. data/lib/treaty/action/context/dsl.rb +56 -0
  7. data/lib/treaty/action/context/workspace.rb +92 -0
  8. data/lib/treaty/action/executor/inventory.rb +136 -0
  9. data/lib/treaty/{info/rest → action/info}/builder.rb +2 -2
  10. data/lib/treaty/{info/rest → action/info}/dsl.rb +2 -2
  11. data/lib/treaty/{info/rest → action/info}/result.rb +2 -2
  12. data/lib/treaty/action/inventory/collection.rb +77 -0
  13. data/lib/treaty/action/inventory/factory.rb +108 -0
  14. data/lib/treaty/action/inventory/inventory.rb +146 -0
  15. data/lib/treaty/action/request/attribute/attribute.rb +76 -0
  16. data/lib/treaty/action/request/attribute/builder.rb +98 -0
  17. data/lib/treaty/action/request/entity.rb +78 -0
  18. data/lib/treaty/action/request/factory.rb +116 -0
  19. data/lib/treaty/action/request/validator.rb +120 -0
  20. data/lib/treaty/action/response/attribute/attribute.rb +79 -0
  21. data/lib/treaty/action/response/attribute/builder.rb +96 -0
  22. data/lib/treaty/action/response/entity.rb +79 -0
  23. data/lib/treaty/action/response/factory.rb +129 -0
  24. data/lib/treaty/action/response/validator.rb +111 -0
  25. data/lib/treaty/action/result.rb +81 -0
  26. data/lib/treaty/action/versions/collection.rb +47 -0
  27. data/lib/treaty/action/versions/dsl.rb +116 -0
  28. data/lib/treaty/action/versions/execution/request.rb +287 -0
  29. data/lib/treaty/action/versions/executor.rb +61 -0
  30. data/lib/treaty/action/versions/factory.rb +253 -0
  31. data/lib/treaty/action/versions/resolver.rb +150 -0
  32. data/lib/treaty/action/versions/semantic.rb +64 -0
  33. data/lib/treaty/action/versions/workspace.rb +106 -0
  34. data/lib/treaty/action.rb +31 -0
  35. data/lib/treaty/controller/dsl.rb +1 -1
  36. data/lib/treaty/engine.rb +1 -1
  37. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  38. data/lib/treaty/entity/attribute/base.rb +184 -0
  39. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  40. data/lib/treaty/entity/attribute/collection.rb +67 -0
  41. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  42. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  43. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  44. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  45. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  46. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  47. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  48. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  49. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  50. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  51. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  52. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  53. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  54. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  55. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  56. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  57. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  58. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  59. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  60. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  61. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  62. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  63. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  64. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  65. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  66. data/lib/treaty/entity/base.rb +90 -0
  67. data/lib/treaty/entity/builder.rb +101 -0
  68. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  69. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  70. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  71. data/lib/treaty/entity.rb +7 -79
  72. data/lib/treaty/version.rb +1 -1
  73. metadata +66 -64
  74. data/lib/treaty/attribute/base.rb +0 -182
  75. data/lib/treaty/attribute/builder/base.rb +0 -273
  76. data/lib/treaty/attribute/collection.rb +0 -65
  77. data/lib/treaty/attribute/dsl.rb +0 -90
  78. data/lib/treaty/attribute/entity/builder.rb +0 -46
  79. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  80. data/lib/treaty/attribute/option/base.rb +0 -188
  81. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  82. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  83. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  84. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  85. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  86. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  87. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  88. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  89. data/lib/treaty/attribute/option/registry.rb +0 -155
  90. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  91. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  92. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  93. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  94. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  95. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  96. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  97. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  98. data/lib/treaty/attribute/validation/base.rb +0 -74
  99. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  100. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  101. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  102. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
  103. data/lib/treaty/base.rb +0 -9
  104. data/lib/treaty/context/callable.rb +0 -26
  105. data/lib/treaty/context/dsl.rb +0 -12
  106. data/lib/treaty/context/workspace.rb +0 -32
  107. data/lib/treaty/executor/inventory.rb +0 -122
  108. data/lib/treaty/inventory/collection.rb +0 -71
  109. data/lib/treaty/inventory/factory.rb +0 -91
  110. data/lib/treaty/inventory/inventory.rb +0 -92
  111. data/lib/treaty/request/attribute/attribute.rb +0 -25
  112. data/lib/treaty/request/attribute/builder.rb +0 -46
  113. data/lib/treaty/request/entity.rb +0 -33
  114. data/lib/treaty/request/factory.rb +0 -81
  115. data/lib/treaty/request/validator.rb +0 -60
  116. data/lib/treaty/response/attribute/attribute.rb +0 -25
  117. data/lib/treaty/response/attribute/builder.rb +0 -46
  118. data/lib/treaty/response/entity.rb +0 -33
  119. data/lib/treaty/response/factory.rb +0 -87
  120. data/lib/treaty/response/validator.rb +0 -53
  121. data/lib/treaty/result.rb +0 -23
  122. data/lib/treaty/versions/collection.rb +0 -15
  123. data/lib/treaty/versions/dsl.rb +0 -42
  124. data/lib/treaty/versions/execution/request.rb +0 -177
  125. data/lib/treaty/versions/executor.rb +0 -14
  126. data/lib/treaty/versions/factory.rb +0 -112
  127. data/lib/treaty/versions/resolver.rb +0 -70
  128. data/lib/treaty/versions/semantic.rb +0 -22
  129. data/lib/treaty/versions/workspace.rb +0 -43
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Validation
7
+ # Validates and transforms individual attributes.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Acts as the main interface for attribute validation and transformation.
12
+ # Delegates option processing to OptionOrchestrator and handles nested validation.
13
+ #
14
+ # ## Responsibilities
15
+ #
16
+ # 1. **Schema Validation** - Validates DSL definition correctness
17
+ # 2. **Value Validation** - Validates runtime data values
18
+ # 3. **Value Transformation** - Transforms values (defaults, etc.)
19
+ # 4. **Name Transformation** - Provides target name (for `as:` option)
20
+ # 5. **Nested Validation** - Delegates to NestedObjectValidator/NestedArrayValidator
21
+ #
22
+ # ## Usage
23
+ #
24
+ # Used by Orchestrator to validate each attribute:
25
+ #
26
+ # validator = AttributeValidator.new(attribute)
27
+ # validator.validate_schema!
28
+ # validator.validate_value!(value)
29
+ # transformed = validator.transform_value(value)
30
+ # target_name = validator.target_name
31
+ #
32
+ # ## Architecture
33
+ #
34
+ # Delegates to:
35
+ # - `OptionOrchestrator` - Coordinates all option processors
36
+ # - `NestedObjectValidator` - Validates nested object structures
37
+ # - `NestedArrayValidator` - Validates nested array structures
38
+ class AttributeValidator
39
+ attr_reader :attribute, :option_orchestrator
40
+
41
+ # Creates a new attribute validator instance
42
+ #
43
+ # @param attribute [Attribute::Base] The attribute to validate
44
+ def initialize(attribute)
45
+ @attribute = attribute
46
+ @option_orchestrator = OptionOrchestrator.new(attribute)
47
+ @nested_object_validator = nil
48
+ @nested_array_validator = nil
49
+ end
50
+
51
+ # Validates the attribute schema (DSL definition)
52
+ #
53
+ # @raise [Treaty::Exceptions::Validation] If schema is invalid
54
+ # @return [void]
55
+ def validate_schema!
56
+ option_orchestrator.validate_schema!
57
+ end
58
+
59
+ # Validates attribute value against all constraints
60
+ #
61
+ # @param value [Object] The value to validate
62
+ # @raise [Treaty::Exceptions::Validation] If validation fails
63
+ # @return [void]
64
+ def validate_value!(value)
65
+ option_orchestrator.validate_value!(value)
66
+ validate_nested!(value) if attribute.nested? && !value.nil?
67
+ end
68
+
69
+ # Transforms attribute value through all modifiers
70
+ #
71
+ # @param value [Object] The value to transform
72
+ # @param root_data [Hash] Full raw data from root level (used by computed modifier)
73
+ # @return [Object] Transformed value
74
+ def transform_value(value, root_data = {})
75
+ option_orchestrator.transform_value(value, root_data)
76
+ end
77
+
78
+ # Checks if attribute name is transformed
79
+ #
80
+ # @return [Boolean] True if name is transformed (as: option)
81
+ def transforms_name?
82
+ option_orchestrator.transforms_name?
83
+ end
84
+
85
+ # Gets the target attribute name
86
+ #
87
+ # @return [Symbol] The target name (or original if not transformed)
88
+ def target_name
89
+ option_orchestrator.target_name
90
+ end
91
+
92
+ # Validates only the type constraint
93
+ # Used by nested transformers to validate types before nested validation
94
+ #
95
+ # @param value [Object] The value to validate
96
+ # @raise [Treaty::Exceptions::Validation] If type validation fails
97
+ # @return [void]
98
+ def validate_type!(value)
99
+ type_processor = option_orchestrator.processor_for(:type)
100
+ type_processor&.validate_value!(value)
101
+ end
102
+
103
+ # Validates only the required constraint
104
+ # Used by nested transformers to validate presence before nested validation
105
+ #
106
+ # @param value [Object] The value to validate
107
+ # @raise [Treaty::Exceptions::Validation] If required validation fails
108
+ # @return [void]
109
+ def validate_required!(value)
110
+ required_processor = option_orchestrator.processor_for(:required)
111
+ required_processor&.validate_value!(value) if attribute.options.key?(:required)
112
+ end
113
+
114
+ private
115
+
116
+ # Validates nested attributes for object/array types
117
+ #
118
+ # @param value [Object] The value to validate
119
+ # @raise [Treaty::Exceptions::Validation] If nested validation fails
120
+ # @return [void]
121
+ def validate_nested!(value)
122
+ case attribute.type
123
+ when :object
124
+ nested_object_validator.validate!(value)
125
+ when :array
126
+ nested_array_validator.validate!(value)
127
+ end
128
+ end
129
+
130
+ # Gets or creates nested object validator
131
+ #
132
+ # @return [NestedObjectValidator] Validator for nested objects
133
+ def nested_object_validator
134
+ @nested_object_validator ||= NestedObjectValidator.new(attribute)
135
+ end
136
+
137
+ # Gets or creates nested array validator
138
+ #
139
+ # @return [NestedArrayValidator] Validator for nested arrays
140
+ def nested_array_validator
141
+ @nested_array_validator ||= NestedArrayValidator.new(attribute)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Validation
7
+ # Base class for request and response validation.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Provides common interface for validation used in Treaty.
12
+ # Subclasses implement specific validation logic for requests and responses.
13
+ #
14
+ # ## Responsibilities
15
+ #
16
+ # 1. **Validation Interface** - Defines common validation interface
17
+ # 2. **Factory Pattern** - Provides class-level validate! method
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
+ # ## Factory Method
33
+ #
34
+ # The `self.validate!(...)` class method provides a convenient factory pattern:
35
+ # ```ruby
36
+ # Request::Validation.validate!(version_factory: factory, data: params)
37
+ # # Equivalent to:
38
+ # Request::Validation.new(version_factory: factory).validate!(data: params)
39
+ # ```
40
+ #
41
+ # ## Architecture
42
+ #
43
+ # Works with:
44
+ # - VersionFactory - Provides version information
45
+ # - Orchestrator::Base - Performs actual validation and transformation
46
+ class Base
47
+ # Class-level factory method for validation
48
+ # Creates instance and calls validate!
49
+ #
50
+ # @param args [Hash] Arguments passed to initialize and validate!
51
+ # @return [Hash] Validated and transformed data
52
+ def self.validate!(...)
53
+ new(...).validate!
54
+ end
55
+
56
+ # Creates a new validation instance
57
+ #
58
+ # @param version_factory [VersionFactory] Factory containing version information
59
+ def initialize(version_factory:)
60
+ @version_factory = version_factory
61
+ end
62
+
63
+ # Performs validation and transformation
64
+ # Must be implemented in subclasses
65
+ #
66
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
67
+ # @return [Hash] Validated and transformed data
68
+ def validate!
69
+ raise Treaty::Exceptions::Validation,
70
+ I18n.t("treaty.attributes.validators.nested.orchestrator.collection_not_implemented")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Validation
7
+ # Validates array elements against nested attribute definitions.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Performs validation for nested array attributes during the validation phase.
12
+ # Handles both simple arrays (with :_self attribute) and complex arrays (objects).
13
+ #
14
+ # ## Responsibilities
15
+ #
16
+ # 1. **Simple Array Validation** - Validates primitive values in arrays
17
+ # 2. **Complex Array Validation** - Validates hash objects within arrays
18
+ # 3. **Error Context** - Provides clear error messages with array index
19
+ # 4. **Type Checking** - Ensures elements match expected types
20
+ #
21
+ # ## Array Types
22
+ #
23
+ # ### Simple Array (`:_self` attribute)
24
+ # Array containing primitive values (strings, integers, etc.)
25
+ #
26
+ # ```ruby
27
+ # array :tags do
28
+ # string :_self # Array of strings
29
+ # end
30
+ # ```
31
+ #
32
+ # Validates: `["ruby", "rails", "api"]`
33
+ #
34
+ # ### Complex Array (regular attributes)
35
+ # Array containing hash objects with defined structure
36
+ #
37
+ # ```ruby
38
+ # array :authors do
39
+ # string :name, :required
40
+ # string :email
41
+ # end
42
+ # ```
43
+ #
44
+ # Validates: `[{ name: "Alice", email: "alice@example.com" }, ...]`
45
+ #
46
+ # ## Usage
47
+ #
48
+ # Called by AttributeValidator for nested arrays:
49
+ #
50
+ # validator = NestedArrayValidator.new(attribute)
51
+ # validator.validate!(array_value)
52
+ #
53
+ # ## Error Handling
54
+ #
55
+ # Provides contextual error messages including:
56
+ # - Array attribute name
57
+ # - Element index (0-based)
58
+ # - Specific validation error
59
+ #
60
+ # Example error:
61
+ # "Error in array 'tags' at index 2: Element must match one of the defined types"
62
+ #
63
+ # ## Architecture
64
+ #
65
+ # Uses:
66
+ # - `AttributeValidator` - Validates individual elements
67
+ # - Caches validators for performance
68
+ # - Separates self validators from regular validators
69
+ class NestedArrayValidator
70
+ # Creates a new nested array validator
71
+ #
72
+ # @param attribute [Attribute::Base] The array-type attribute with nested attributes
73
+ def initialize(attribute)
74
+ @attribute = attribute
75
+ @self_validators = nil
76
+ @regular_validators = nil
77
+ end
78
+
79
+ # Validates all items in an array
80
+ # Skips validation if value is not an Array
81
+ #
82
+ # @param array [Array] The array to validate
83
+ # @raise [Treaty::Exceptions::Validation] If any item validation fails
84
+ # @return [void]
85
+ def validate!(array)
86
+ return unless array.is_a?(Array)
87
+
88
+ array.each_with_index do |array_item, index|
89
+ validate_self_array_item!(array_item, index) if self_validators.any?
90
+
91
+ validate_regular_array_item!(array_item, index) if regular_validators.any?
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Validates array item for simple arrays (with :_self attribute)
98
+ # Simple array contains primitive values: strings, integers, datetimes, etc.
99
+ # Example: ["ruby", "rails", "api"] where each item is a String
100
+ #
101
+ # @param array_item [String, Integer, DateTime] Primitive value from simple array
102
+ # @param index [Integer] Array index for error messages
103
+ # @raise [Treaty::Exceptions::Validation] If primitive value doesn't match defined type
104
+ # @return [void]
105
+ def validate_self_array_item!(array_item, index) # rubocop:disable Metrics/MethodLength
106
+ errors = []
107
+
108
+ validated = self_validators.any? do |validator|
109
+ validator.validate_value!(array_item)
110
+ true
111
+ rescue Treaty::Exceptions::Validation => e
112
+ errors << e.message
113
+ false
114
+ end
115
+
116
+ return if validated
117
+
118
+ raise Treaty::Exceptions::Validation,
119
+ I18n.t(
120
+ "treaty.attributes.validators.nested.array.element_validation_error",
121
+ attribute: @attribute.name,
122
+ index:,
123
+ errors: errors.join("; ")
124
+ )
125
+ end
126
+
127
+ # Validates array item for complex arrays (with regular attributes)
128
+ # Complex array contains hash objects with defined structure
129
+ # Example: [{ name: "Alice", email: "alice@example.com" }, ...] where each item is a Hash
130
+ #
131
+ # @param array_item [Hash] Hash object from complex array
132
+ # @param index [Integer] Array index for error messages
133
+ # @raise [Treaty::Exceptions::Validation] If item is not Hash or nested validation fails
134
+ # @return [void]
135
+ def validate_regular_array_item!(array_item, index) # rubocop:disable Metrics/MethodLength
136
+ unless array_item.is_a?(Hash)
137
+ raise Treaty::Exceptions::Validation,
138
+ I18n.t(
139
+ "treaty.attributes.validators.nested.array.element_type_error",
140
+ attribute: @attribute.name,
141
+ index:,
142
+ actual: array_item.class
143
+ )
144
+ end
145
+
146
+ regular_validators.each do |nested_attribute, validator|
147
+ nested_value = array_item.fetch(nested_attribute.name, nil)
148
+ validator.validate_value!(nested_value)
149
+ rescue Treaty::Exceptions::Validation => e
150
+ raise Treaty::Exceptions::Validation,
151
+ I18n.t(
152
+ "treaty.attributes.validators.nested.array.attribute_error",
153
+ attribute: @attribute.name,
154
+ index:,
155
+ message: e.message
156
+ )
157
+ end
158
+ end
159
+
160
+ ########################################################################
161
+
162
+ # Gets cached self validators or builds them
163
+ #
164
+ # @return [Array<AttributeValidator>] Validators for :_self attributes
165
+ def self_validators
166
+ @self_validators ||= build_self_validators
167
+ end
168
+
169
+ # Gets cached regular validators or builds them
170
+ #
171
+ # @return [Hash] Hash of nested_attribute => validator
172
+ def regular_validators
173
+ @regular_validators ||= build_regular_validators
174
+ end
175
+
176
+ ########################################################################
177
+
178
+ # Builds validators for :_self attributes (simple array elements)
179
+ #
180
+ # @return [Array<AttributeValidator>] Array of validators
181
+ def build_self_validators
182
+ @attribute.collection_of_attributes
183
+ .select { |attr| attr.name == :_self }
184
+ .map do |self_attribute|
185
+ validator = AttributeValidator.new(self_attribute)
186
+ validator.validate_schema!
187
+ validator
188
+ end
189
+ end
190
+
191
+ # Builds validators for regular attributes (complex array elements)
192
+ #
193
+ # @return [Hash] Hash of nested_attribute => validator
194
+ def build_regular_validators
195
+ @attribute.collection_of_attributes
196
+ .reject { |attr| attr.name == :_self }
197
+ .each_with_object({}) do |nested_attribute, cache|
198
+ validator = AttributeValidator.new(nested_attribute)
199
+ validator.validate_schema!
200
+ cache[nested_attribute] = validator
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Validation
7
+ # Validates nested object (hash) attributes against their defined structure.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Performs validation for nested object attributes during the validation phase.
12
+ # Ensures hash values conform to the nested attribute definitions.
13
+ #
14
+ # ## Responsibilities
15
+ #
16
+ # 1. **Structure Validation** - Validates hash structure matches definition
17
+ # 2. **Attribute Validation** - Validates each nested attribute's value
18
+ # 3. **Type Safety** - Ensures value is a Hash before validation
19
+ # 4. **Validator Caching** - Builds and caches validators for performance
20
+ #
21
+ # ## Usage
22
+ #
23
+ # Used for object-type attributes with nested definitions:
24
+ #
25
+ # ```ruby
26
+ # object :author do
27
+ # string :name, :required
28
+ # string :email
29
+ # integer :age
30
+ # end
31
+ # ```
32
+ #
33
+ # Validates: `{ name: "Alice", email: "alice@example.com", age: 30 }`
34
+ #
35
+ # ## Usage in Code
36
+ #
37
+ # Called by AttributeValidator for nested objects:
38
+ #
39
+ # validator = NestedObjectValidator.new(attribute)
40
+ # validator.validate!(hash_value)
41
+ #
42
+ # ## Validation Flow
43
+ #
44
+ # 1. Check if value is a Hash
45
+ # 2. Build validators for all nested attributes (cached)
46
+ # 3. For each nested attribute:
47
+ # - Extract value from hash
48
+ # - Validate using AttributeValidator
49
+ # 4. Raise exception if any validation fails
50
+ #
51
+ # ## Architecture
52
+ #
53
+ # Uses:
54
+ # - `AttributeValidator` - Validates individual nested attributes
55
+ # - Caches validators to avoid rebuilding on each validation
56
+ class NestedObjectValidator
57
+ attr_reader :attribute
58
+
59
+ # Creates a new nested object validator
60
+ #
61
+ # @param attribute [Attribute::Base] The object-type attribute with nested attributes
62
+ def initialize(attribute)
63
+ @attribute = attribute
64
+ @validators_cache = nil
65
+ end
66
+
67
+ # Validates all nested attributes in a hash
68
+ # Skips validation if value is not a Hash
69
+ #
70
+ # @param hash [Hash] The hash to validate
71
+ # @raise [Treaty::Exceptions::Validation] If any nested validation fails
72
+ # @return [void]
73
+ def validate!(hash)
74
+ return unless hash.is_a?(Hash)
75
+
76
+ validators.each do |nested_attribute, nested_validator|
77
+ nested_value = hash.fetch(nested_attribute.name, nil)
78
+ nested_validator.validate_value!(nested_value)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # Gets cached validators or builds them
85
+ #
86
+ # @return [Hash] Hash of nested_attribute => validator
87
+ def validators
88
+ @validators ||= build_validators
89
+ end
90
+
91
+ # Builds validators for all nested attributes
92
+ #
93
+ # @return [Hash] Hash of nested_attribute => validator
94
+ def build_validators
95
+ attribute.collection_of_attributes.each_with_object({}) do |nested_attribute, cache|
96
+ validator = AttributeValidator.new(nested_attribute)
97
+ validator.validate_schema!
98
+ cache[nested_attribute] = validator
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end