treaty 0.18.0 → 0.19.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 (80) 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/engine.rb +1 -1
  5. data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
  6. data/lib/treaty/entity/attribute/base.rb +184 -0
  7. data/lib/treaty/entity/attribute/builder/base.rb +275 -0
  8. data/lib/treaty/entity/attribute/collection.rb +67 -0
  9. data/lib/treaty/entity/attribute/dsl.rb +92 -0
  10. data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
  11. data/lib/treaty/entity/attribute/option/base.rb +190 -0
  12. data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
  13. data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
  14. data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
  15. data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
  16. data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
  17. data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
  18. data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
  19. data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
  20. data/lib/treaty/entity/attribute/option/registry.rb +157 -0
  21. data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
  22. data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
  23. data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
  24. data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
  25. data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
  26. data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
  27. data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
  28. data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
  29. data/lib/treaty/entity/attribute/validation/base.rb +76 -0
  30. data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
  31. data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
  32. data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
  33. data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
  34. data/lib/treaty/entity/base.rb +90 -0
  35. data/lib/treaty/entity/builder.rb +44 -0
  36. data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
  37. data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
  38. data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
  39. data/lib/treaty/entity.rb +7 -79
  40. data/lib/treaty/request/attribute/attribute.rb +1 -1
  41. data/lib/treaty/request/attribute/builder.rb +2 -2
  42. data/lib/treaty/request/entity.rb +1 -1
  43. data/lib/treaty/request/factory.rb +5 -5
  44. data/lib/treaty/request/validator.rb +1 -1
  45. data/lib/treaty/response/attribute/attribute.rb +1 -1
  46. data/lib/treaty/response/attribute/builder.rb +2 -2
  47. data/lib/treaty/response/entity.rb +1 -1
  48. data/lib/treaty/response/factory.rb +5 -5
  49. data/lib/treaty/response/validator.rb +1 -1
  50. data/lib/treaty/version.rb +1 -1
  51. metadata +35 -34
  52. data/lib/treaty/attribute/base.rb +0 -182
  53. data/lib/treaty/attribute/builder/base.rb +0 -273
  54. data/lib/treaty/attribute/collection.rb +0 -65
  55. data/lib/treaty/attribute/dsl.rb +0 -90
  56. data/lib/treaty/attribute/entity/builder.rb +0 -46
  57. data/lib/treaty/attribute/helper_mapper.rb +0 -72
  58. data/lib/treaty/attribute/option/base.rb +0 -188
  59. data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
  60. data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
  61. data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
  62. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
  63. data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
  64. data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
  65. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
  66. data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
  67. data/lib/treaty/attribute/option/registry.rb +0 -155
  68. data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
  69. data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
  70. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
  71. data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
  72. data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
  73. data/lib/treaty/attribute/option_normalizer.rb +0 -166
  74. data/lib/treaty/attribute/option_orchestrator.rb +0 -190
  75. data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
  76. data/lib/treaty/attribute/validation/base.rb +0 -74
  77. data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
  78. data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
  79. data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
  80. data/lib/treaty/attribute/validation/orchestrator/base.rb +0 -260
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed95ed37e55c61ca17604d5ef395f93405adbb8259064a08b9857b032d267c93
4
- data.tar.gz: 48a97168517da5bbca25f73174e5caa666939505193d556b6612ea238f67b98a
3
+ metadata.gz: c7b03c7d0eed4f560087ed9662f711e50ec947a78357d687ea2be29ddd9b7872
4
+ data.tar.gz: c9b3b121e50d75518e4db1a0b3c388f36334cce4b9995aefb5b24e3101e5d984
5
5
  SHA512:
6
- metadata.gz: 8ae24db235dc2b5127d42e4823d7a103dc6bb85f39bd6ef7ef2ab27cf406bf6adb555177bea649bc3231fb7f8741753c217d976d11b65c10ae5675757aeeca98
7
- data.tar.gz: 6985a2a682ee06acf5ae091bef6c6c15469ede66d4eebd6bf510973a2e84d16aa3144e82429e9ebbaaa662628eec7054287d708a9917f67e8214987174aa2b03
6
+ metadata.gz: 13ca45c37ea6988a5b046a8c7825cf11acfd62fbc5c32d120888c66623da24f52dbea656120a71e815018765573aa3b1c05370bba356fc4913dd02ec342022c6
7
+ data.tar.gz: 431831fb76b2a44997aef884aa3662d7cff2a474f13a308472af78cd298471776f001454b98d02ed38a11e3d9f9f84ff2e5034f77004533d092483fd4b8f6420
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  </div>
14
14
 
15
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.18.0"`) until the 1.0 release.
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.19.0"`) until the 1.0 release.
17
17
 
18
18
  ## 📚 Documentation
19
19
 
@@ -84,7 +84,7 @@ en:
84
84
  not_implemented: "%{class} must implement #create_attribute"
85
85
  create_attribute_not_implemented: "Subclass %{class} must implement #create_attribute method"
86
86
  deep_copy_not_implemented: "%{class} must implement #deep_copy_attribute"
87
- invalid_entity_class: "use_entity expects a Treaty::Entity subclass, got %{type}: %{value}"
87
+ invalid_entity_class: "use_entity expects a Treaty::Entity::Base subclass, got %{type}: %{value}"
88
88
  use_entity_after_attributes: "use_entity must be the only statement in the block. Cannot call use_entity after defining other attributes."
89
89
  attributes_after_use_entity: "use_entity must be the only statement in the block. Cannot define attributes after calling use_entity."
90
90
 
@@ -101,7 +101,7 @@ en:
101
101
  # Request factory DSL
102
102
  factory:
103
103
  unknown_method: "Unknown method '%{method}' in request definition. Use 'object :name do ... end' to define request structure"
104
- invalid_entity_class: "Request expects a Treaty::Entity subclass, got %{type}: %{value}"
104
+ invalid_entity_class: "Request expects a Treaty::Entity::Base subclass, got %{type}: %{value}"
105
105
 
106
106
  # ============================================================================
107
107
  # Response: Response definition and structure
@@ -110,7 +110,7 @@ en:
110
110
  # Response factory DSL
111
111
  factory:
112
112
  unknown_method: "Unknown method '%{method}' in response definition. Use 'object :name do ... end' to define response structure"
113
- invalid_entity_class: "Response expects a Treaty::Entity subclass, got %{type}: %{value}"
113
+ invalid_entity_class: "Response expects a Treaty::Entity::Base subclass, got %{type}: %{value}"
114
114
 
115
115
  # ============================================================================
116
116
  # Versioning: API version management and resolution
data/lib/treaty/engine.rb CHANGED
@@ -12,7 +12,7 @@ module Treaty
12
12
 
13
13
  initializer "treaty.register_option_processors", before: :load_config_initializers do
14
14
  # Register all option processors (validators and modifiers)
15
- require "treaty/attribute/option/registry_initializer"
15
+ require "treaty/entity/attribute/option/registry_initializer"
16
16
  end
17
17
 
18
18
  initializer "treaty.validate_configuration" do
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Treaty
4
- module Attribute
5
- module Entity
4
+ module Entity
5
+ module Attribute
6
6
  # Entity-specific attribute that defaults to required: true
7
- class Attribute < Treaty::Attribute::Base
7
+ class Attribute < Base
8
8
  private
9
9
 
10
10
  def apply_defaults!
@@ -16,7 +16,7 @@ module Treaty
16
16
  def process_nested_attributes(&block)
17
17
  return unless object_or_array?
18
18
 
19
- builder = Builder.new(collection_of_attributes, @nesting_level + 1)
19
+ builder = Treaty::Entity::Builder.new(collection_of_attributes, @nesting_level + 1)
20
20
  builder.instance_eval(&block)
21
21
  end
22
22
  end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ # Base class for all attribute definitions in Treaty DSL.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Represents a single attribute defined in request/response definitions.
11
+ # Handles:
12
+ # - Attribute metadata (name, type, nesting level)
13
+ # - Helper mode to simple mode conversion
14
+ # - Simple mode to advanced mode normalization
15
+ # - Nested attributes (for object and array types)
16
+ #
17
+ # ## Usage
18
+ #
19
+ # Attributes are created through DSL methods:
20
+ # string :title, :required
21
+ # integer :age, default: 18
22
+ # object :author do
23
+ # string :name
24
+ # end
25
+ #
26
+ # ## Processing Flow
27
+ #
28
+ # 1. Extract helpers from arguments (`:required`, `:optional`)
29
+ # 2. Convert helpers to simple mode options
30
+ # 3. Merge with explicit options
31
+ # 4. Normalize all options to advanced mode
32
+ # 5. Apply defaults (required: true for request, false for response)
33
+ # 6. Process nested attributes if block given
34
+ #
35
+ # ## Nested Attributes
36
+ #
37
+ # Object and array types can have nested attributes:
38
+ # - `object` - nested attributes as direct children
39
+ # - `array` - nested attributes define array element structure
40
+ #
41
+ # Special attribute name `:_self` is used for simple arrays:
42
+ # array :tags do
43
+ # string :_self # Array of strings
44
+ # end
45
+ class Base
46
+ attr_reader :name,
47
+ :type,
48
+ :options,
49
+ :nesting_level
50
+
51
+ # Creates a new attribute instance
52
+ #
53
+ # @param name [Symbol] The attribute name
54
+ # @param type [Symbol] The attribute type (:string, :integer, :object, :array, etc.)
55
+ # @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
56
+ # @param nesting_level [Integer] Current nesting depth (default: 0)
57
+ # @param options [Hash] Attribute options (required, default, as, etc.)
58
+ # @param block [Proc] Block for defining nested attributes (for object/array types)
59
+ def initialize(name, type, *helpers, nesting_level: 0, **options, &block)
60
+ @name = name
61
+ @type = type
62
+ @nesting_level = nesting_level
63
+
64
+ validate_nesting_level!
65
+
66
+ # Separate helpers from non-helper symbols.
67
+ @helpers = extract_helpers(helpers)
68
+
69
+ # Merge helper options with explicit options.
70
+ merged_options = merge_options(@helpers, options)
71
+
72
+ # Normalize all options to advanced mode.
73
+ @options = OptionNormalizer.normalize(merged_options)
74
+
75
+ apply_defaults!
76
+
77
+ # Process nested attributes for object and array types.
78
+ process_nested_attributes(&block) if block_given?
79
+ end
80
+
81
+ # Returns collection of nested attributes for this attribute
82
+ #
83
+ # @return [Collection] Collection of nested attributes
84
+ def collection_of_attributes
85
+ @collection_of_attributes ||= Collection.new
86
+ end
87
+
88
+ # Checks if this attribute has nested attributes
89
+ #
90
+ # @return [Boolean] True if attribute is object/array with nested attributes
91
+ def nested?
92
+ object_or_array? && collection_of_attributes.exists?
93
+ end
94
+
95
+ # Checks if this attribute is an object or array type
96
+ #
97
+ # @return [Boolean] True if type is :object or :array
98
+ def object_or_array?
99
+ object? || array?
100
+ end
101
+
102
+ # Checks if this attribute is an object type
103
+ #
104
+ # @return [Boolean] True if type is :object
105
+ def object?
106
+ @type == :object
107
+ end
108
+
109
+ # Checks if this attribute is an array type
110
+ #
111
+ # @return [Boolean] True if type is :array
112
+ def array?
113
+ @type == :array
114
+ end
115
+
116
+ private
117
+
118
+ # Validates that nesting level doesn't exceed maximum allowed depth
119
+ #
120
+ # @raise [Treaty::Exceptions::NestedAttributes] If nesting exceeds limit
121
+ # @return [void]
122
+ def validate_nesting_level!
123
+ return unless @nesting_level > Treaty::Engine.config.treaty.attribute_nesting_level
124
+
125
+ raise Treaty::Exceptions::NestedAttributes,
126
+ I18n.t(
127
+ "treaty.attributes.errors.nesting_level_exceeded",
128
+ level: @nesting_level,
129
+ max_level: Treaty::Engine.config.treaty.attribute_nesting_level
130
+ )
131
+ end
132
+
133
+ # Extracts helper symbols from arguments
134
+ #
135
+ # @param helpers [Array] Mixed array that may contain helper symbols
136
+ # @return [Array<Symbol>] Filtered array of valid helper symbols
137
+ def extract_helpers(helpers)
138
+ helpers.select do |helper|
139
+ helper.is_a?(Symbol) && HelperMapper.helper?(helper)
140
+ end
141
+ end
142
+
143
+ # Merges helper-derived options with explicit options
144
+ #
145
+ # @param helpers [Array<Symbol>] Helper symbols to convert
146
+ # @param explicit_options [Hash] Explicitly provided options
147
+ # @return [Hash] Merged options hash
148
+ def merge_options(helpers, explicit_options)
149
+ helper_options = HelperMapper.map(helpers)
150
+ helper_options.merge(explicit_options)
151
+ end
152
+
153
+ # Applies default values for options based on context (request/response)
154
+ # Must be implemented in subclasses
155
+ #
156
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
157
+ # @return [void]
158
+ def apply_defaults!
159
+ # Must be implemented in subclasses
160
+ raise Treaty::Exceptions::NotImplemented,
161
+ I18n.t(
162
+ "treaty.attributes.errors.apply_defaults_not_implemented",
163
+ class: self.class
164
+ )
165
+ end
166
+
167
+ # Processes nested attributes block for object/array types
168
+ # Must be implemented in subclasses
169
+ #
170
+ # @param block [Proc] Block containing nested attribute definitions
171
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
172
+ # @return [void]
173
+ def process_nested_attributes
174
+ # Must be implemented in subclasses
175
+ raise Treaty::Exceptions::NotImplemented,
176
+ I18n.t(
177
+ "treaty.attributes.errors.process_nested_not_implemented",
178
+ class: self.class
179
+ )
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ module Builder
7
+ # Base DSL builder for defining attributes in request/response definitions.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Provides the DSL interface for defining attributes within objects.
12
+ # Handles method_missing magic to support type-based method calls.
13
+ #
14
+ # ## Responsibilities
15
+ #
16
+ # 1. **DSL Interface** - Provides clean syntax for attribute definitions
17
+ # 2. **Method Dispatch** - Routes type methods (string, integer, etc.) to attribute creation
18
+ # 3. **Helper Support** - Handles helper symbols in various positions
19
+ # 4. **Nesting Tracking** - Tracks nesting level for nested attributes
20
+ # 5. **Entity Reuse** - Supports use_entity for copying attributes from Entity classes
21
+ #
22
+ # ## DSL Usage
23
+ #
24
+ # The builder enables this clean DSL syntax:
25
+ #
26
+ # ```ruby
27
+ # request do
28
+ # object :user do
29
+ # string :name
30
+ # integer :age, default: 18
31
+ # object :profile do
32
+ # string :bio
33
+ # end
34
+ # end
35
+ # end
36
+ # ```
37
+ #
38
+ # ## Entity Reuse
39
+ #
40
+ # You can use `use_entity` to copy attributes from an Entity class:
41
+ #
42
+ # ```ruby
43
+ # object :author do
44
+ # use_entity(AuthorEntity)
45
+ # end
46
+ # ```
47
+ #
48
+ # Note: `use_entity` must be the only statement in the block.
49
+ #
50
+ # ## Method Dispatch
51
+ #
52
+ # ### Type-based Methods
53
+ # When you call `string :name`, it routes through `method_missing`:
54
+ # 1. `string` becomes the type
55
+ # 2. `:name` becomes the attribute name
56
+ # 3. Calls `attribute(:name, :string, ...)`
57
+ #
58
+ # ### Helper Position Handling
59
+ # Handles helpers in different positions:
60
+ #
61
+ # ```ruby
62
+ # string :required, :name # Helper first, then name
63
+ # string :name, :required # Name first, then helper
64
+ # ```
65
+ #
66
+ # Both resolve to the same attribute definition.
67
+ #
68
+ # ## Nesting
69
+ #
70
+ # Tracks nesting level for:
71
+ # - Validation (enforcing maximum nesting depth)
72
+ # - Error messages (showing context)
73
+ #
74
+ # Maximum nesting level is configured in Treaty::Engine.config.
75
+ #
76
+ # ## Subclass Requirements
77
+ #
78
+ # Subclasses must implement:
79
+ # - `create_attribute` - Creates the appropriate attribute type (Request/Response)
80
+ # - `deep_copy_attribute` - Deep copies an attribute with adjusted nesting level
81
+ #
82
+ # ## Architecture
83
+ #
84
+ # Used by:
85
+ # - Request::Builder - For request attribute definitions
86
+ # - Response::Builder - For response attribute definitions
87
+ # - Entity::Builder - For entity attribute definitions
88
+ class Base
89
+ attr_reader :nesting_level,
90
+ :collection_of_attributes
91
+
92
+ # Creates a new builder instance
93
+ #
94
+ # @param collection_of_attributes [Collection] Collection to add attributes to
95
+ # @param nesting_level [Integer] Current nesting depth
96
+ def initialize(collection_of_attributes, nesting_level)
97
+ @collection_of_attributes = collection_of_attributes
98
+ @nesting_level = nesting_level
99
+ @use_entity_called = false
100
+ @attributes_defined = false
101
+ end
102
+
103
+ # Uses an Entity class to copy its attributes into this builder's collection.
104
+ # Must be the ONLY statement in the block - no other attributes allowed.
105
+ #
106
+ # @param entity_class [Class] Entity class (must be Treaty::Entity::Base subclass)
107
+ # @raise [Treaty::Exceptions::Validation] if entity_class is invalid
108
+ # @raise [Treaty::Exceptions::Validation] if mixed with other attributes
109
+ # @return [void]
110
+ #
111
+ # @example Using an Entity in a nested object
112
+ # object :author do
113
+ # use_entity(AuthorEntity)
114
+ # end
115
+ #
116
+ # @example Using an Entity in a nested array
117
+ # array :items, :optional do
118
+ # use_entity(ItemEntity)
119
+ # end
120
+ def use_entity(entity_class)
121
+ validate_use_entity_preconditions!
122
+ validate_entity_class!(entity_class)
123
+
124
+ @use_entity_called = true
125
+
126
+ copy_attributes_from_entity(entity_class)
127
+ end
128
+
129
+ # Defines an attribute with explicit type
130
+ #
131
+ # @param name [Symbol] The attribute name
132
+ # @param type [Symbol] The attribute type
133
+ # @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
134
+ # @param options [Hash] Attribute options
135
+ # @param block [Proc] Block for nested attributes
136
+ # @return [void]
137
+ def attribute(name, type, *helpers, **options, &block)
138
+ validate_no_use_entity_called!
139
+
140
+ @attributes_defined = true
141
+
142
+ @collection_of_attributes << create_attribute(
143
+ name,
144
+ type,
145
+ *helpers,
146
+ nesting_level: @nesting_level,
147
+ **options,
148
+ &block
149
+ )
150
+ end
151
+
152
+ # Handles DSL methods like `string :name` where method name is the type
153
+ #
154
+ # @param type [Symbol] The attribute type (method name)
155
+ # @param name [Symbol] The attribute name (first argument)
156
+ # @param helpers [Array<Symbol>] Helper symbols
157
+ # @param options [Hash] Attribute options
158
+ # @param block [Proc] Block for nested attributes
159
+ # @return [void]
160
+ def method_missing(type, name, *helpers, **options, &block)
161
+ if name.is_a?(Symbol) && HelperMapper.helper?(name)
162
+ helpers.unshift(name)
163
+ name = helpers.shift
164
+ end
165
+
166
+ attribute(name, type, *helpers, **options, &block)
167
+ end
168
+
169
+ # Checks if method should be handled by method_missing
170
+ #
171
+ # @param name [Symbol] Method name
172
+ # @return [Boolean]
173
+ def respond_to_missing?(name, *)
174
+ super
175
+ end
176
+
177
+ private
178
+
179
+ # Creates an attribute instance (must be implemented in subclasses)
180
+ #
181
+ # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
182
+ # @return [Attribute::Base] Created attribute instance
183
+ def create_attribute(*)
184
+ raise Treaty::Exceptions::NotImplemented,
185
+ I18n.t("treaty.attributes.builder.not_implemented", class: self.class)
186
+ end
187
+
188
+ # Validates that use_entity can be called (no attributes defined before)
189
+ #
190
+ # @raise [Treaty::Exceptions::Validation] if attributes were defined before use_entity
191
+ def validate_use_entity_preconditions!
192
+ return unless @attributes_defined
193
+
194
+ raise Treaty::Exceptions::Validation,
195
+ I18n.t("treaty.attributes.builder.use_entity_after_attributes")
196
+ end
197
+
198
+ # Validates that no use_entity was called before defining attributes
199
+ #
200
+ # @raise [Treaty::Exceptions::Validation] if use_entity was already called
201
+ def validate_no_use_entity_called!
202
+ return unless @use_entity_called
203
+
204
+ raise Treaty::Exceptions::Validation,
205
+ I18n.t("treaty.attributes.builder.attributes_after_use_entity")
206
+ end
207
+
208
+ # Validates that entity_class is a valid Treaty::Entity::Base subclass
209
+ #
210
+ # @param entity_class [Class] Entity class to validate
211
+ # @raise [Treaty::Exceptions::Validation] if entity_class is not valid
212
+ def validate_entity_class!(entity_class)
213
+ return if entity_class.is_a?(Class) && entity_class < Treaty::Entity::Base
214
+
215
+ raise Treaty::Exceptions::Validation,
216
+ I18n.t(
217
+ "treaty.attributes.builder.invalid_entity_class",
218
+ type: entity_class.class,
219
+ value: entity_class
220
+ )
221
+ end
222
+
223
+ # Copies all attributes from entity_class to this builder's collection
224
+ # with adjusted nesting levels.
225
+ #
226
+ # @param entity_class [Class] Source entity class
227
+ def copy_attributes_from_entity(entity_class)
228
+ entity_class.collection_of_attributes.each do |source_attribute|
229
+ copied_attribute = deep_copy_attribute(source_attribute, @nesting_level)
230
+ @collection_of_attributes << copied_attribute
231
+ end
232
+ end
233
+
234
+ # Deep copies an attribute with adjusted nesting level.
235
+ # Must be implemented by subclasses to use proper attribute types.
236
+ #
237
+ # @param source_attribute [Attribute::Base] Attribute to copy
238
+ # @param new_nesting_level [Integer] New nesting level for copied attribute
239
+ # @return [Attribute::Base] Copied attribute with correct type
240
+ def deep_copy_attribute(_source_attribute, _new_nesting_level)
241
+ raise Treaty::Exceptions::NotImplemented,
242
+ I18n.t(
243
+ "treaty.attributes.builder.deep_copy_not_implemented",
244
+ class: self.class
245
+ )
246
+ end
247
+
248
+ # Deep copies options hash, preserving Proc references
249
+ # and recursively handling nested Hash/Array structures.
250
+ #
251
+ # @param options [Hash] Options to copy
252
+ # @return [Hash] Copied options
253
+ def deep_copy_options(options)
254
+ options.transform_values { |value| deep_copy_value(value) }
255
+ end
256
+
257
+ # Deep copies a single value, handling nested structures.
258
+ # Immutable types (Proc, Symbol, Numeric, nil, true, false) are returned as-is.
259
+ # Hash and Array are recursively copied. Strings are duplicated if not frozen.
260
+ #
261
+ # @param value [Object] Value to copy
262
+ # @return [Object] Copied value
263
+ def deep_copy_value(value)
264
+ case value
265
+ when Hash then value.transform_values { |v| deep_copy_value(v) }
266
+ when Array then value.map { |v| deep_copy_value(v) }
267
+ when String then value.frozen? ? value : value.dup
268
+ else value
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Treaty
6
+ module Entity
7
+ module Attribute
8
+ # Collection wrapper for sets of attributes.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # Provides a unified interface for working with collections of attributes.
13
+ # Uses Ruby Set internally for uniqueness but exposes Array-like interface.
14
+ #
15
+ # ## Usage
16
+ #
17
+ # Used internally by:
18
+ # - Request/Response factories (to store attributes)
19
+ # - Attribute::Base (to store nested attributes)
20
+ #
21
+ # ## Methods
22
+ #
23
+ # Delegates common collection methods to internal Set:
24
+ # - `<<` - Add attribute
25
+ # - `each`, `map`, `select`, `reject` - Iteration
26
+ # - `find`, `first` - Access
27
+ # - `size`, `empty?` - Size checks
28
+ # - `to_h` - Convert to hash
29
+ #
30
+ # Custom methods:
31
+ # - `exists?` - Returns true if collection is not empty
32
+ #
33
+ # ## Example
34
+ #
35
+ # collection = Collection.new
36
+ # collection << Attribute::Base.new(:name, :string)
37
+ # collection << Attribute::Base.new(:age, :integer)
38
+ # collection.size # => 2
39
+ # collection.exists? # => true
40
+ class Collection
41
+ extend Forwardable
42
+
43
+ def_delegators :@collection,
44
+ :<<,
45
+ :to_h, :map,
46
+ :each_with_object, :each,
47
+ :select, :reject, :size,
48
+ :find, :first,
49
+ :empty?
50
+
51
+ # Creates a new collection instance
52
+ #
53
+ # @param collection [Set] Initial collection (default: empty Set)
54
+ def initialize(collection = Set.new)
55
+ @collection = collection
56
+ end
57
+
58
+ # Checks if collection has any elements
59
+ #
60
+ # @return [Boolean] True if collection is not empty
61
+ def exists?
62
+ !empty?
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end