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,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 [Treaty::Entity::Attribute::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 [Treaty::Entity::Attribute::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
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ # DSL module for defining attributes in Entity-like classes.
7
+ #
8
+ # This module provides the class-level DSL for defining attributes.
9
+ # It can be included in any class that needs attribute definition capabilities.
10
+ #
11
+ # ## Usage
12
+ #
13
+ # ```ruby
14
+ # class MyEntity
15
+ # include Treaty::Entity::Attribute::DSL
16
+ #
17
+ # string :name
18
+ # integer :age
19
+ # end
20
+ # ```
21
+ module DSL
22
+ def self.included(base)
23
+ base.extend(ClassMethods)
24
+ end
25
+
26
+ module ClassMethods
27
+ # Defines an attribute with explicit type
28
+ #
29
+ # @param name [Symbol] The attribute name
30
+ # @param type [Symbol] The attribute type
31
+ # @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
32
+ # @param options [Hash] Attribute options
33
+ # @param block [Proc] Block for nested attributes
34
+ # @return [void]
35
+ def attribute(name, type, *helpers, **options, &block)
36
+ collection_of_attributes << create_attribute(
37
+ name,
38
+ type,
39
+ *helpers,
40
+ nesting_level: 0,
41
+ **options,
42
+ &block
43
+ )
44
+ end
45
+
46
+ # Returns collection of attributes for this class
47
+ #
48
+ # @return [Treaty::Entity::Attribute::Collection] Collection of attributes
49
+ def collection_of_attributes
50
+ @collection_of_attributes ||= Treaty::Entity::Attribute::Collection.new
51
+ end
52
+
53
+ # Handles DSL methods like `string :name` where method name is the type
54
+ #
55
+ # @param type [Symbol] The attribute type (method name)
56
+ # @param name [Symbol] The attribute name (first argument)
57
+ # @param helpers [Array<Symbol>] Helper symbols
58
+ # @param options [Hash] Attribute options
59
+ # @param block [Proc] Block for nested attributes
60
+ # @return [void]
61
+ def method_missing(type, *helpers, **options, &block)
62
+ name = helpers.shift
63
+
64
+ # If no attribute name provided, this is not an attribute definition
65
+ # Pass to super to handle it properly (e.g., for methods like 'info', 'call!', etc.)
66
+ return super if name.nil?
67
+
68
+ attribute(name, type, *helpers, **options, &block)
69
+ end
70
+
71
+ def respond_to_missing?(name, *)
72
+ super
73
+ end
74
+
75
+ private
76
+
77
+ # Creates an attribute instance (must be implemented by including class)
78
+ #
79
+ # @raise [Treaty::Exceptions::NotImplemented] If not implemented
80
+ # @return [Attribute::Base] Created attribute instance
81
+ def create_attribute(*)
82
+ raise Treaty::Exceptions::NotImplemented,
83
+ I18n.t(
84
+ "treaty.attributes.dsl.create_attribute_not_implemented",
85
+ class: self
86
+ )
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Entity
5
+ module Attribute
6
+ # Maps DSL helper symbols to their simple mode option equivalents.
7
+ #
8
+ # ## Purpose
9
+ #
10
+ # Helpers provide the most concise syntax for common options.
11
+ # They are syntactic sugar that gets converted to simple mode options.
12
+ #
13
+ # ## Available Helpers
14
+ #
15
+ # - `:required` → `required: true`
16
+ # - `:optional` → `required: false`
17
+ #
18
+ # ## Usage Examples
19
+ #
20
+ # Helper mode (most concise):
21
+ # string :title, :required
22
+ # string :bio, :optional
23
+ #
24
+ # Equivalent to simple mode:
25
+ # string :title, required: true
26
+ # string :bio, required: false
27
+ #
28
+ # ## Processing Flow
29
+ #
30
+ # 1. Helper mode: `string :title, :required`
31
+ # 2. HelperMapper: `:required` → `required: true`
32
+ # 3. OptionNormalizer: `required: true` → `{ is: true, message: nil }`
33
+ # 4. Final: Advanced mode used internally
34
+ #
35
+ # ## Adding New Helpers
36
+ #
37
+ # To add a new helper:
38
+ # ```ruby
39
+ # HELPER_MAPPINGS = {
40
+ # required: { required: true },
41
+ # optional: { required: false },
42
+ # my_helper: { my_option: :smth } # New helper example
43
+ # }.freeze
44
+ # ```
45
+ class HelperMapper
46
+ HELPER_MAPPINGS = {
47
+ required: { required: true },
48
+ optional: { required: false }
49
+ }.freeze
50
+
51
+ class << self
52
+ # Maps helper symbols to their simple mode equivalents
53
+ #
54
+ # @param helpers [Array<Symbol>] Array of helper symbols
55
+ # @return [Hash] Simple mode options hash
56
+ def map(helpers)
57
+ helpers.each_with_object({}) do |helper, result|
58
+ mapping = HELPER_MAPPINGS.fetch(helper)
59
+ result.merge!(mapping) if mapping.present?
60
+ end
61
+ end
62
+
63
+ # Checks if a symbol is a registered helper
64
+ #
65
+ # @param symbol [Symbol] Symbol to check
66
+ # @return [Boolean] True if symbol is a helper
67
+ def helper?(symbol)
68
+ HELPER_MAPPINGS.key?(symbol)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end