treaty 0.16.0 → 0.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc42fa86dd5dc35d49f0a07a189d9ddc59f619e2341905c1ed62193e0d3069b7
4
- data.tar.gz: ffc66f03e0ced6e666bb12db50f405117831b48c26475ab034ab4a16f4c431b1
3
+ metadata.gz: ed95ed37e55c61ca17604d5ef395f93405adbb8259064a08b9857b032d267c93
4
+ data.tar.gz: 48a97168517da5bbca25f73174e5caa666939505193d556b6612ea238f67b98a
5
5
  SHA512:
6
- metadata.gz: 0b31ad204ddb19df8a296007f334d29e2e220fbb771f9c5a3851309e0a9ec1909dae0dbef3608633f6a5203da0b827fb90c75132e83f5ebb3fcc28f36fd33cc9
7
- data.tar.gz: 0c6ec71d1be9155da662498264667a63072c5ce513d1ae0987a54091d76aa35a9fd21b3c722db38a8f4154919a2894f442736c44e23ff88961c9a1aadc841c49
6
+ metadata.gz: 8ae24db235dc2b5127d42e4823d7a103dc6bb85f39bd6ef7ef2ab27cf406bf6adb555177bea649bc3231fb7f8741753c217d976d11b65c10ae5675757aeeca98
7
+ data.tar.gz: 6985a2a682ee06acf5ae091bef6c6c15469ede66d4eebd6bf510973a2e84d16aa3144e82429e9ebbaaa662628eec7054287d708a9917f67e8214987174aa2b03
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.16.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.18.0"`) until the 1.0 release.
17
17
 
18
18
  ## 📚 Documentation
19
19
 
@@ -33,7 +33,7 @@ Treaty provides a complete solution for building versioned APIs in Ruby on Rails
33
33
  - **Type Safety** - Enforce strict type checking for request and response data
34
34
  - **API Versioning** - Manage multiple concurrent API versions effortlessly
35
35
  - **Unified Architecture** - Request blocks, response blocks, and Entity classes share the same validation system
36
- - **Entity Classes (DTOs)** - Define reusable data transfer objects for better code organization
36
+ - **Entity Classes** - Define reusable entity classes for better code organization
37
37
  - **Built-in Validation** - Validate incoming requests and outgoing responses automatically
38
38
  - **Data Transformation** - Transform data seamlessly between different API versions
39
39
  - **Inventory System** - Pass controller-specific data to services efficiently
@@ -83,6 +83,10 @@ en:
83
83
  builder:
84
84
  not_implemented: "%{class} must implement #create_attribute"
85
85
  create_attribute_not_implemented: "Subclass %{class} must implement #create_attribute method"
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}"
88
+ use_entity_after_attributes: "use_entity must be the only statement in the block. Cannot call use_entity after defining other attributes."
89
+ attributes_after_use_entity: "use_entity must be the only statement in the block. Cannot define attributes after calling use_entity."
86
90
 
87
91
  # Attribute-level errors
88
92
  errors:
@@ -16,6 +16,7 @@ module Treaty
16
16
  # 2. **Method Dispatch** - Routes type methods (string, integer, etc.) to attribute creation
17
17
  # 3. **Helper Support** - Handles helper symbols in various positions
18
18
  # 4. **Nesting Tracking** - Tracks nesting level for nested attributes
19
+ # 5. **Entity Reuse** - Supports use_entity for copying attributes from Entity classes
19
20
  #
20
21
  # ## DSL Usage
21
22
  #
@@ -33,6 +34,18 @@ module Treaty
33
34
  # end
34
35
  # ```
35
36
  #
37
+ # ## Entity Reuse
38
+ #
39
+ # You can use `use_entity` to copy attributes from an Entity class:
40
+ #
41
+ # ```ruby
42
+ # object :author do
43
+ # use_entity(AuthorEntity)
44
+ # end
45
+ # ```
46
+ #
47
+ # Note: `use_entity` must be the only statement in the block.
48
+ #
36
49
  # ## Method Dispatch
37
50
  #
38
51
  # ### Type-based Methods
@@ -63,12 +76,14 @@ module Treaty
63
76
  #
64
77
  # Subclasses must implement:
65
78
  # - `create_attribute` - Creates the appropriate attribute type (Request/Response)
79
+ # - `deep_copy_attribute` - Deep copies an attribute with adjusted nesting level
66
80
  #
67
81
  # ## Architecture
68
82
  #
69
83
  # Used by:
70
84
  # - Request::Builder - For request attribute definitions
71
85
  # - Response::Builder - For response attribute definitions
86
+ # - Entity::Builder - For entity attribute definitions
72
87
  class Base
73
88
  attr_reader :nesting_level,
74
89
  :collection_of_attributes
@@ -80,6 +95,34 @@ module Treaty
80
95
  def initialize(collection_of_attributes, nesting_level)
81
96
  @collection_of_attributes = collection_of_attributes
82
97
  @nesting_level = nesting_level
98
+ @use_entity_called = false
99
+ @attributes_defined = false
100
+ end
101
+
102
+ # Uses an Entity class to copy its attributes into this builder's collection.
103
+ # Must be the ONLY statement in the block - no other attributes allowed.
104
+ #
105
+ # @param entity_class [Class] Entity class (must be Treaty::Entity subclass)
106
+ # @raise [Treaty::Exceptions::Validation] if entity_class is invalid
107
+ # @raise [Treaty::Exceptions::Validation] if mixed with other attributes
108
+ # @return [void]
109
+ #
110
+ # @example Using an Entity in a nested object
111
+ # object :author do
112
+ # use_entity(AuthorEntity)
113
+ # end
114
+ #
115
+ # @example Using an Entity in a nested array
116
+ # array :items, :optional do
117
+ # use_entity(ItemEntity)
118
+ # end
119
+ def use_entity(entity_class)
120
+ validate_use_entity_preconditions!
121
+ validate_entity_class!(entity_class)
122
+
123
+ @use_entity_called = true
124
+
125
+ copy_attributes_from_entity(entity_class)
83
126
  end
84
127
 
85
128
  # Defines an attribute with explicit type
@@ -91,6 +134,10 @@ module Treaty
91
134
  # @param block [Proc] Block for nested attributes
92
135
  # @return [void]
93
136
  def attribute(name, type, *helpers, **options, &block)
137
+ validate_no_use_entity_called!
138
+
139
+ @attributes_defined = true
140
+
94
141
  @collection_of_attributes << create_attribute(
95
142
  name,
96
143
  type,
@@ -133,10 +180,93 @@ module Treaty
133
180
  # @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
134
181
  # @return [Attribute::Base] Created attribute instance
135
182
  def create_attribute(*)
136
- # Must be implemented in subclasses
137
183
  raise Treaty::Exceptions::NotImplemented,
138
184
  I18n.t("treaty.attributes.builder.not_implemented", class: self.class)
139
185
  end
186
+
187
+ # Validates that use_entity can be called (no attributes defined before)
188
+ #
189
+ # @raise [Treaty::Exceptions::Validation] if attributes were defined before use_entity
190
+ def validate_use_entity_preconditions!
191
+ return unless @attributes_defined
192
+
193
+ raise Treaty::Exceptions::Validation,
194
+ I18n.t("treaty.attributes.builder.use_entity_after_attributes")
195
+ end
196
+
197
+ # Validates that no use_entity was called before defining attributes
198
+ #
199
+ # @raise [Treaty::Exceptions::Validation] if use_entity was already called
200
+ def validate_no_use_entity_called!
201
+ return unless @use_entity_called
202
+
203
+ raise Treaty::Exceptions::Validation,
204
+ I18n.t("treaty.attributes.builder.attributes_after_use_entity")
205
+ end
206
+
207
+ # Validates that entity_class is a valid Treaty::Entity subclass
208
+ #
209
+ # @param entity_class [Class] Entity class to validate
210
+ # @raise [Treaty::Exceptions::Validation] if entity_class is not valid
211
+ def validate_entity_class!(entity_class)
212
+ return if entity_class.is_a?(Class) && entity_class < Treaty::Entity
213
+
214
+ raise Treaty::Exceptions::Validation,
215
+ I18n.t(
216
+ "treaty.attributes.builder.invalid_entity_class",
217
+ type: entity_class.class,
218
+ value: entity_class
219
+ )
220
+ end
221
+
222
+ # Copies all attributes from entity_class to this builder's collection
223
+ # with adjusted nesting levels.
224
+ #
225
+ # @param entity_class [Class] Source entity class
226
+ def copy_attributes_from_entity(entity_class)
227
+ entity_class.collection_of_attributes.each do |source_attribute|
228
+ copied_attribute = deep_copy_attribute(source_attribute, @nesting_level)
229
+ @collection_of_attributes << copied_attribute
230
+ end
231
+ end
232
+
233
+ # Deep copies an attribute with adjusted nesting level.
234
+ # Must be implemented by subclasses to use proper attribute types.
235
+ #
236
+ # @param source_attribute [Attribute::Base] Attribute to copy
237
+ # @param new_nesting_level [Integer] New nesting level for copied attribute
238
+ # @return [Attribute::Base] Copied attribute with correct type
239
+ def deep_copy_attribute(_source_attribute, _new_nesting_level)
240
+ raise Treaty::Exceptions::NotImplemented,
241
+ I18n.t(
242
+ "treaty.attributes.builder.deep_copy_not_implemented",
243
+ class: self.class
244
+ )
245
+ end
246
+
247
+ # Deep copies options hash, preserving Proc references
248
+ # and recursively handling nested Hash/Array structures.
249
+ #
250
+ # @param options [Hash] Options to copy
251
+ # @return [Hash] Copied options
252
+ def deep_copy_options(options)
253
+ options.transform_values { |value| deep_copy_value(value) }
254
+ end
255
+
256
+ # Deep copies a single value, handling nested structures.
257
+ # Immutable types (Proc, Symbol, Numeric, nil, true, false) are returned as-is.
258
+ # Hash and Array are recursively copied. Strings are duplicated if not frozen.
259
+ #
260
+ # @param value [Object] Value to copy
261
+ # @return [Object] Copied value
262
+ def deep_copy_value(value)
263
+ case value
264
+ when Hash then value.transform_values { |v| deep_copy_value(v) }
265
+ when Array then value.map { |v| deep_copy_value(v) }
266
+ when String then value.frozen? ? value : value.dup
267
+ else value
268
+ end
269
+ end
140
270
  end
141
271
  end
142
272
  end
@@ -17,6 +17,29 @@ module Treaty
17
17
  &block
18
18
  )
19
19
  end
20
+
21
+ # Deep copies an attribute with adjusted nesting level for Entity context.
22
+ #
23
+ # @param source_attribute [Treaty::Attribute::Base] Attribute to copy
24
+ # @param new_nesting_level [Integer] New nesting level
25
+ # @return [Entity::Attribute] Copied attribute
26
+ def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength
27
+ copied = Attribute.new(
28
+ source_attribute.name,
29
+ source_attribute.type,
30
+ nesting_level: new_nesting_level,
31
+ **deep_copy_options(source_attribute.options)
32
+ )
33
+
34
+ return copied unless source_attribute.nested?
35
+
36
+ source_attribute.collection_of_attributes.each do |nested_source|
37
+ nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1)
38
+ copied.collection_of_attributes << nested_copied
39
+ end
40
+
41
+ copied
42
+ end
20
43
  end
21
44
  end
22
45
  end
@@ -17,9 +17,9 @@ module Treaty
17
17
  # integer :views, if: ->(post:) { post[:published_at].present? }
18
18
  #
19
19
  # Complex conditions:
20
- # string :admin_note, if: ->(**attrs) {
21
- # attrs.dig(:user, :role) == "admin" && attrs.dig(:post, :flagged)
22
- # }
20
+ # string :admin_note, if: (lambda do |**attributes|
21
+ # attributes.dig(:user, :role) == "admin" && attributes.dig(:post, :flagged)
22
+ # end)
23
23
  #
24
24
  # ## Use Cases
25
25
  #
@@ -30,7 +30,7 @@ module Treaty
30
30
  # string :id
31
31
  # string :title
32
32
  # datetime :published_at, :optional
33
- # integer :rating, if: ->(**attrs) { attrs.dig(:post, :published_at).present? }
33
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
34
34
  # end
35
35
  # end
36
36
  # # If published_at is nil → rating is excluded from response
@@ -80,7 +80,7 @@ module Treaty
80
80
  #
81
81
  # ```ruby
82
82
  # # For response with { post: { title: "...", published_at: "..." } }
83
- # integer :rating, if: ->(**attrs) { attrs.dig(:post, :published_at).present? }
83
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
84
84
  #
85
85
  # # Alternative: named argument pattern
86
86
  # integer :rating, if: ->(post:) { post[:published_at].present? }
@@ -17,9 +17,9 @@ module Treaty
17
17
  # integer :edit_count, unless: ->(post:) { post[:published_at].present? }
18
18
  #
19
19
  # Complex conditions:
20
- # string :internal_note, unless: ->(**attrs) {
21
- # attrs.dig(:user, :role) == "admin" && attrs.dig(:post, :flagged)
22
- # }
20
+ # string :internal_note, unless: (lambda do |**attributes|
21
+ # attributes.dig(:user, :role) == "admin" && attributes.dig(:post, :flagged)
22
+ # end)
23
23
  #
24
24
  # ## Use Cases
25
25
  #
@@ -30,7 +30,7 @@ module Treaty
30
30
  # string :id
31
31
  # string :title
32
32
  # datetime :published_at, :optional
33
- # integer :draft_views, unless: ->(**attrs) { attrs.dig(:post, :published_at).present? }
33
+ # integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
34
34
  # end
35
35
  # end
36
36
  # # If published_at is nil → draft_views is included in response
@@ -74,12 +74,12 @@ module Treaty
74
74
  #
75
75
  # ```ruby
76
76
  # # These are equivalent:
77
- # integer :rating, if: ->(**attrs) { attrs.dig(:post, :published_at).present? }
78
- # integer :rating, unless: ->(**attrs) { attrs.dig(:post, :published_at).blank? }
77
+ # integer :rating, if: ->(**attributes) { attributes.dig(:post, :published_at).present? }
78
+ # integer :rating, unless: ->(**attributes) { attributes.dig(:post, :published_at).blank? }
79
79
  #
80
80
  # # These are also equivalent:
81
- # integer :draft_views, unless: ->(**attrs) { attrs.dig(:post, :published_at).present? }
82
- # integer :draft_views, if: ->(**attrs) { attrs.dig(:post, :published_at).blank? }
81
+ # integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
82
+ # integer :draft_views, if: ->(**attributes) { attributes.dig(:post, :published_at).blank? }
83
83
  # ```
84
84
  #
85
85
  # ## Error Handling
@@ -96,7 +96,7 @@ module Treaty
96
96
  #
97
97
  # ```ruby
98
98
  # # For response with { post: { title: "...", published_at: "..." } }
99
- # integer :draft_views, unless: ->(**attrs) { attrs.dig(:post, :published_at).present? }
99
+ # integer :draft_views, unless: ->(**attributes) { attributes.dig(:post, :published_at).present? }
100
100
  #
101
101
  # # Alternative: named argument pattern
102
102
  # integer :draft_views, unless: ->(post:) { post[:published_at].present? }
@@ -14,13 +14,13 @@ module Treaty
14
14
  # ## Usage Examples
15
15
  #
16
16
  # Simple mode:
17
- # string :full_name, computed: ->(**attrs) {
18
- # "#{attrs.dig(:user, :first_name)} #{attrs.dig(:user, :last_name)}"
19
- # }
17
+ # string :full_name, computed: (lambda do |**attributes|
18
+ # "#{attributes.dig(:user, :first_name)} #{attributes.dig(:user, :last_name)}"
19
+ # end)
20
20
  #
21
21
  # Advanced mode with custom error message:
22
22
  # string :full_name, computed: {
23
- # is: ->(**attrs) { "#{attrs.dig(:user, :first_name)} #{attrs.dig(:user, :last_name)}" },
23
+ # is: ->(**attributes) { "#{attributes.dig(:user, :first_name)} #{attributes.dig(:user, :last_name)}" },
24
24
  # message: "Failed to compute full name"
25
25
  # }
26
26
  #
@@ -32,9 +32,9 @@ module Treaty
32
32
  # object :user do
33
33
  # string :first_name
34
34
  # string :last_name
35
- # string :full_name, computed: ->(**attrs) {
36
- # "#{attrs.dig(:user, :first_name)} #{attrs.dig(:user, :last_name)}"
37
- # }
35
+ # string :full_name, computed: (lambda do |**attributes|
36
+ # "#{attributes.dig(:user, :first_name)} #{attributes.dig(:user, :last_name)}"
37
+ # end)
38
38
  # end
39
39
  # end
40
40
  # ```
@@ -44,9 +44,9 @@ module Treaty
44
44
  # response 200 do
45
45
  # object :post do
46
46
  # string :content
47
- # integer :word_count, computed: ->(**attrs) {
48
- # attrs.dig(:post, :content).to_s.split.size
49
- # }
47
+ # integer :word_count, computed: (lambda do |**attributes|
48
+ # attributes.dig(:post, :content).to_s.split.size
49
+ # end)
50
50
  # end
51
51
  # end
52
52
  # ```
@@ -57,9 +57,9 @@ module Treaty
57
57
  # object :order do
58
58
  # integer :quantity
59
59
  # integer :unit_price
60
- # integer :total, computed: ->(**attrs) {
61
- # attrs.dig(:order, :quantity).to_i * attrs.dig(:order, :unit_price).to_i
62
- # }
60
+ # integer :total, computed: (lambda do |**attributes|
61
+ # attributes.dig(:order, :quantity).to_i * attributes.dig(:order, :unit_price).to_i
62
+ # end)
63
63
  # end
64
64
  # end
65
65
  # ```
@@ -19,26 +19,27 @@ module Treaty
19
19
  #
20
20
  # ## Registered Options
21
21
  #
22
- # ### Validators
23
- # - `:required` → RequiredValidator
24
- # - `:type` → TypeValidator
25
- # - `:inclusion` → InclusionValidator
26
- # - `:format` → FormatValidator
22
+ # ### Validators (sorted by position)
23
+ # - `:type` → TypeValidator (position: 100)
24
+ # - `:required` → RequiredValidator (position: 200)
25
+ # - `:inclusion` → InclusionValidator (position: 300)
26
+ # - `:format` → FormatValidator (position: 400)
27
27
  #
28
- # ### Modifiers
29
- # - `:as` → AsModifier
30
- # - `:default` → DefaultModifier
31
- # - `:transform` → TransformModifier
32
- # - `:cast` → CastModifier
28
+ # ### Modifiers (sorted by position)
29
+ # - `:transform` → TransformModifier (position: 500)
30
+ # - `:cast` → CastModifier (position: 600)
31
+ # - `:computed` → ComputedModifier (position: 700)
32
+ # - `:default` → DefaultModifier (position: 800)
33
+ # - `:as` → AsModifier (position: 900)
33
34
  #
34
- # ### Conditionals
35
+ # ### Conditionals (no position - handled separately)
35
36
  # - `:if` → IfConditional
36
37
  # - `:unless` → UnlessConditional
37
38
  #
38
39
  # ## Usage
39
40
  #
40
41
  # Registration (done in RegistryInitializer):
41
- # Registry.register(:required, RequiredValidator, category: :validator)
42
+ # Registry.register(:required, RequiredValidator, category: :validator, position: 200)
42
43
  # Registry.register(:if, IfConditional, category: :conditional)
43
44
  #
44
45
  # Retrieval (done in OptionOrchestrator):
@@ -64,11 +65,13 @@ module Treaty
64
65
  #
65
66
  # @param option_name [Symbol] The name of the option (e.g., :required, :as, :default)
66
67
  # @param processor_class [Class] The processor class
67
- # @param category [Symbol] The category (:validator or :modifier)
68
- def register(option_name, processor_class, category:)
68
+ # @param category [Symbol] The category (:validator, :modifier, or :conditional)
69
+ # @param position [Integer, nil] Execution order position (nil for conditionals)
70
+ def register(option_name, processor_class, category:, position: nil)
69
71
  registry[option_name] = {
70
72
  processor_class:,
71
- category:
73
+ category:,
74
+ position:
72
75
  }
73
76
  end
74
77
 
@@ -88,6 +91,14 @@ module Treaty
88
91
  registry.dig(option_name, :category)
89
92
  end
90
93
 
94
+ # Get position for an option
95
+ #
96
+ # @param option_name [Symbol] The name of the option
97
+ # @return [Integer, nil] The execution order position or nil if not set
98
+ def position_for(option_name)
99
+ registry.dig(option_name, :position)
100
+ end
101
+
91
102
  # Check if an option is registered
92
103
  #
93
104
  # @param option_name [Symbol] The name of the option
@@ -17,22 +17,22 @@ module Treaty
17
17
  # 3. **Conditional Registration** - Registers all built-in conditionals
18
18
  # 4. **Auto-Loading** - Executes automatically when file is loaded
19
19
  #
20
- # ## Built-in Validators
20
+ # ## Built-in Validators (sorted by position)
21
21
  #
22
- # - `:required` → RequiredValidator - Validates required/optional attributes
23
- # - `:type` → TypeValidator - Validates value types
24
- # - `:inclusion` → InclusionValidator - Validates value is in allowed set
25
- # - `:format` → FormatValidator - Validates string values match specific formats
22
+ # - `:type` → TypeValidator (position: 100) - Validates value types
23
+ # - `:required` → RequiredValidator (position: 200) - Validates required/optional attributes
24
+ # - `:inclusion` → InclusionValidator (position: 300) - Validates value is in allowed set
25
+ # - `:format` → FormatValidator (position: 400) - Validates string values match specific formats
26
26
  #
27
- # ## Built-in Modifiers
27
+ # ## Built-in Modifiers (sorted by position)
28
28
  #
29
- # - `:computed` → ComputedModifier - Computes values from all raw data (executes first)
30
- # - `:transform` → TransformModifier - Transforms values using custom lambdas
31
- # - `:cast` → CastModifier - Converts values between types automatically
32
- # - `:default` → DefaultModifier - Provides default values
33
- # - `:as` → AsModifier - Renames attributes
29
+ # - `:transform` → TransformModifier (position: 500) - Transforms values using custom lambdas
30
+ # - `:cast` → CastModifier (position: 600) - Converts values between types automatically
31
+ # - `:computed` → ComputedModifier (position: 700) - Computes values from all raw data
32
+ # - `:default` → DefaultModifier (position: 800) - Provides default values
33
+ # - `:as` → AsModifier (position: 900) - Renames attributes
34
34
  #
35
- # ## Built-in Conditionals
35
+ # ## Built-in Conditionals (no position - handled separately)
36
36
  #
37
37
  # - `:if` → IfConditional - Conditionally includes attributes based on runtime data
38
38
  # - `:unless` → UnlessConditional - Conditionally excludes attributes based on runtime data
@@ -76,25 +76,26 @@ module Treaty
76
76
  private
77
77
 
78
78
  # Registers all built-in validators
79
+ # Position determines execution order (lower = earlier)
79
80
  #
80
81
  # @return [void]
81
82
  def register_validators!
82
- Registry.register(:required, Validators::RequiredValidator, category: :validator)
83
- Registry.register(:type, Validators::TypeValidator, category: :validator)
84
- Registry.register(:inclusion, Validators::InclusionValidator, category: :validator)
85
- Registry.register(:format, Validators::FormatValidator, category: :validator)
83
+ Registry.register(:type, Validators::TypeValidator, category: :validator, position: 100)
84
+ Registry.register(:required, Validators::RequiredValidator, category: :validator, position: 200)
85
+ Registry.register(:inclusion, Validators::InclusionValidator, category: :validator, position: 300)
86
+ Registry.register(:format, Validators::FormatValidator, category: :validator, position: 400)
86
87
  end
87
88
 
88
89
  # Registers all built-in modifiers
89
- # Order matters: computed runs first, then transform, cast, default, as
90
+ # Position determines execution order (lower = earlier)
90
91
  #
91
92
  # @return [void]
92
93
  def register_modifiers!
93
- Registry.register(:computed, Modifiers::ComputedModifier, category: :modifier)
94
- Registry.register(:transform, Modifiers::TransformModifier, category: :modifier)
95
- Registry.register(:cast, Modifiers::CastModifier, category: :modifier)
96
- Registry.register(:default, Modifiers::DefaultModifier, category: :modifier)
97
- Registry.register(:as, Modifiers::AsModifier, category: :modifier)
94
+ Registry.register(:transform, Modifiers::TransformModifier, category: :modifier, position: 500)
95
+ Registry.register(:cast, Modifiers::CastModifier, category: :modifier, position: 600)
96
+ Registry.register(:computed, Modifiers::ComputedModifier, category: :modifier, position: 700)
97
+ Registry.register(:default, Modifiers::DefaultModifier, category: :modifier, position: 800)
98
+ Registry.register(:as, Modifiers::AsModifier, category: :modifier, position: 900)
98
99
  end
99
100
 
100
101
  # Registers all built-in conditionals
@@ -87,18 +87,32 @@ module Treaty
87
87
 
88
88
  class << self
89
89
  # Normalizes all options from simple mode to advanced mode
90
+ # and sorts them by position for consistent execution order.
90
91
  #
91
92
  # @param options [Hash] Options hash in simple or advanced mode
92
- # @return [Hash] Normalized options in advanced mode
93
+ # @return [Hash] Normalized options in advanced mode, sorted by position
93
94
  def normalize(options)
94
- options.each_with_object({}) do |(key, value), result|
95
+ normalized = options.each_with_object({}) do |(key, value), result|
95
96
  advanced_key, normalized_value = normalize_option(key, value)
96
97
  result[advanced_key] = normalized_value
97
98
  end
99
+
100
+ sort_by_position(normalized)
98
101
  end
99
102
 
100
103
  private
101
104
 
105
+ # Sorts options by their registered position.
106
+ # Options without position (like conditionals) sort first (position 0).
107
+ #
108
+ # @param options_hash [Hash] Normalized options hash
109
+ # @return [Hash] Options sorted by position
110
+ def sort_by_position(options_hash)
111
+ options_hash.sort_by do |option_name, _|
112
+ Option::Registry.position_for(option_name) || 0
113
+ end.to_h
114
+ end
115
+
102
116
  # Normalizes a single option to advanced mode
103
117
  #
104
118
  # @param key [Symbol] Option key
data/lib/treaty/entity.rb CHANGED
@@ -1,34 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Treaty
4
- # Base class for defining DTO (Data Transfer Object) entities in Treaty.
4
+ # Base class for defining reusable entity classes in Treaty.
5
5
  #
6
6
  # ## Purpose
7
7
  #
8
- # Treaty::Entity provides a base class for creating reusable DTO classes
8
+ # Treaty::Entity provides a base class for creating reusable entity classes
9
9
  # that can be used in both request and response definitions. This allows
10
10
  # for better code organization and reusability of common data structures.
11
11
  #
12
12
  # ## Usage
13
13
  #
14
- # Create a DTO class by inheriting from Treaty::Entity:
14
+ # Create an entity class by inheriting from Treaty::Entity:
15
15
  #
16
16
  # ```ruby
17
- # class PostEntity < Treaty::Entity
18
- # string :id
19
- # string :title
20
- # string :content
21
- # datetime :created_at
17
+ # module Posts
18
+ # module Create
19
+ # class ResponseEntity < Treaty::Entity
20
+ # string :id
21
+ # string :title
22
+ # string :content
23
+ # datetime :created_at
24
+ # end
25
+ # end
22
26
  # end
23
27
  # ```
24
28
  #
25
29
  # Then use it in your treaty definitions:
26
30
  #
27
31
  # ```ruby
28
- # class CreateTreaty < ApplicationTreaty
32
+ # class Posts::CreateTreaty < ApplicationTreaty
29
33
  # version 1 do
30
- # request PostEntity
31
- # response 201, PostEntity
34
+ # request Posts::Create::RequestEntity
35
+ # response 201, Posts::Create::ResponseEntity
32
36
  # end
33
37
  # end
34
38
  # ```
@@ -17,6 +17,29 @@ module Treaty
17
17
  &block
18
18
  )
19
19
  end
20
+
21
+ # Deep copies an attribute with adjusted nesting level for Request context.
22
+ #
23
+ # @param source_attribute [Treaty::Attribute::Base] Attribute to copy
24
+ # @param new_nesting_level [Integer] New nesting level
25
+ # @return [Request::Attribute::Attribute] Copied attribute
26
+ def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength
27
+ copied = Attribute.new(
28
+ source_attribute.name,
29
+ source_attribute.type,
30
+ nesting_level: new_nesting_level,
31
+ **deep_copy_options(source_attribute.options)
32
+ )
33
+
34
+ return copied unless source_attribute.nested?
35
+
36
+ source_attribute.collection_of_attributes.each do |nested_source|
37
+ nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1)
38
+ copied.collection_of_attributes << nested_copied
39
+ end
40
+
41
+ copied
42
+ end
20
43
  end
21
44
  end
22
45
  end
@@ -21,7 +21,7 @@ module Treaty
21
21
  # ## Entity Mode
22
22
  #
23
23
  # ```ruby
24
- # request PostRequestEntity
24
+ # request Posts::Create::RequestEntity
25
25
  # ```
26
26
  class Factory
27
27
  # Uses a provided Entity class
@@ -17,6 +17,29 @@ module Treaty
17
17
  &block
18
18
  )
19
19
  end
20
+
21
+ # Deep copies an attribute with adjusted nesting level for Response context.
22
+ #
23
+ # @param source_attribute [Treaty::Attribute::Base] Attribute to copy
24
+ # @param new_nesting_level [Integer] New nesting level
25
+ # @return [Response::Attribute::Attribute] Copied attribute
26
+ def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength
27
+ copied = Attribute.new(
28
+ source_attribute.name,
29
+ source_attribute.type,
30
+ nesting_level: new_nesting_level,
31
+ **deep_copy_options(source_attribute.options)
32
+ )
33
+
34
+ return copied unless source_attribute.nested?
35
+
36
+ source_attribute.collection_of_attributes.each do |nested_source|
37
+ nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1)
38
+ copied.collection_of_attributes << nested_copied
39
+ end
40
+
41
+ copied
42
+ end
20
43
  end
21
44
  end
22
45
  end
@@ -21,7 +21,7 @@ module Treaty
21
21
  # ## Entity Mode
22
22
  #
23
23
  # ```ruby
24
- # response 200, PostResponseEntity
24
+ # response 200, Posts::Create::ResponseEntity
25
25
  # ```
26
26
  class Factory
27
27
  attr_reader :status
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 16
6
+ MINOR = 18
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: treaty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov