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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/config/locales/en.yml +3 -3
- data/lib/treaty/engine.rb +1 -1
- data/lib/treaty/{attribute/entity → entity/attribute}/attribute.rb +4 -4
- data/lib/treaty/entity/attribute/base.rb +184 -0
- data/lib/treaty/entity/attribute/builder/base.rb +275 -0
- data/lib/treaty/entity/attribute/collection.rb +67 -0
- data/lib/treaty/entity/attribute/dsl.rb +92 -0
- data/lib/treaty/entity/attribute/helper_mapper.rb +74 -0
- data/lib/treaty/entity/attribute/option/base.rb +190 -0
- data/lib/treaty/entity/attribute/option/conditionals/base.rb +92 -0
- data/lib/treaty/entity/attribute/option/conditionals/if_conditional.rb +136 -0
- data/lib/treaty/entity/attribute/option/conditionals/unless_conditional.rb +153 -0
- data/lib/treaty/entity/attribute/option/modifiers/as_modifier.rb +93 -0
- data/lib/treaty/entity/attribute/option/modifiers/cast_modifier.rb +285 -0
- data/lib/treaty/entity/attribute/option/modifiers/computed_modifier.rb +128 -0
- data/lib/treaty/entity/attribute/option/modifiers/default_modifier.rb +105 -0
- data/lib/treaty/entity/attribute/option/modifiers/transform_modifier.rb +114 -0
- data/lib/treaty/entity/attribute/option/registry.rb +157 -0
- data/lib/treaty/entity/attribute/option/registry_initializer.rb +117 -0
- data/lib/treaty/entity/attribute/option/validators/format_validator.rb +222 -0
- data/lib/treaty/entity/attribute/option/validators/inclusion_validator.rb +94 -0
- data/lib/treaty/entity/attribute/option/validators/required_validator.rb +100 -0
- data/lib/treaty/entity/attribute/option/validators/type_validator.rb +219 -0
- data/lib/treaty/entity/attribute/option_normalizer.rb +168 -0
- data/lib/treaty/entity/attribute/option_orchestrator.rb +192 -0
- data/lib/treaty/entity/attribute/validation/attribute_validator.rb +147 -0
- data/lib/treaty/entity/attribute/validation/base.rb +76 -0
- data/lib/treaty/entity/attribute/validation/nested_array_validator.rb +207 -0
- data/lib/treaty/entity/attribute/validation/nested_object_validator.rb +105 -0
- data/lib/treaty/entity/attribute/validation/nested_transformer.rb +432 -0
- data/lib/treaty/entity/attribute/validation/orchestrator/base.rb +262 -0
- data/lib/treaty/entity/base.rb +90 -0
- data/lib/treaty/entity/builder.rb +44 -0
- data/lib/treaty/{info/entity → entity/info}/builder.rb +8 -8
- data/lib/treaty/{info/entity → entity/info}/dsl.rb +2 -2
- data/lib/treaty/{info/entity → entity/info}/result.rb +2 -2
- data/lib/treaty/entity.rb +7 -79
- data/lib/treaty/request/attribute/attribute.rb +1 -1
- data/lib/treaty/request/attribute/builder.rb +2 -2
- data/lib/treaty/request/entity.rb +1 -1
- data/lib/treaty/request/factory.rb +5 -5
- data/lib/treaty/request/validator.rb +1 -1
- data/lib/treaty/response/attribute/attribute.rb +1 -1
- data/lib/treaty/response/attribute/builder.rb +2 -2
- data/lib/treaty/response/entity.rb +1 -1
- data/lib/treaty/response/factory.rb +5 -5
- data/lib/treaty/response/validator.rb +1 -1
- data/lib/treaty/version.rb +1 -1
- metadata +35 -34
- data/lib/treaty/attribute/base.rb +0 -182
- data/lib/treaty/attribute/builder/base.rb +0 -273
- data/lib/treaty/attribute/collection.rb +0 -65
- data/lib/treaty/attribute/dsl.rb +0 -90
- data/lib/treaty/attribute/entity/builder.rb +0 -46
- data/lib/treaty/attribute/helper_mapper.rb +0 -72
- data/lib/treaty/attribute/option/base.rb +0 -188
- data/lib/treaty/attribute/option/conditionals/base.rb +0 -90
- data/lib/treaty/attribute/option/conditionals/if_conditional.rb +0 -134
- data/lib/treaty/attribute/option/conditionals/unless_conditional.rb +0 -151
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +0 -91
- data/lib/treaty/attribute/option/modifiers/cast_modifier.rb +0 -283
- data/lib/treaty/attribute/option/modifiers/computed_modifier.rb +0 -126
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +0 -103
- data/lib/treaty/attribute/option/modifiers/transform_modifier.rb +0 -112
- data/lib/treaty/attribute/option/registry.rb +0 -155
- data/lib/treaty/attribute/option/registry_initializer.rb +0 -115
- data/lib/treaty/attribute/option/validators/format_validator.rb +0 -220
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +0 -92
- data/lib/treaty/attribute/option/validators/required_validator.rb +0 -98
- data/lib/treaty/attribute/option/validators/type_validator.rb +0 -217
- data/lib/treaty/attribute/option_normalizer.rb +0 -166
- data/lib/treaty/attribute/option_orchestrator.rb +0 -190
- data/lib/treaty/attribute/validation/attribute_validator.rb +0 -145
- data/lib/treaty/attribute/validation/base.rb +0 -74
- data/lib/treaty/attribute/validation/nested_array_validator.rb +0 -205
- data/lib/treaty/attribute/validation/nested_object_validator.rb +0 -103
- data/lib/treaty/attribute/validation/nested_transformer.rb +0 -430
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7b03c7d0eed4f560087ed9662f711e50ec947a78357d687ea2be29ddd9b7872
|
|
4
|
+
data.tar.gz: c9b3b121e50d75518e4db1a0b3c388f36334cce4b9995aefb5b24e3101e5d984
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
|
5
|
-
module
|
|
4
|
+
module Entity
|
|
5
|
+
module Attribute
|
|
6
6
|
# Entity-specific attribute that defaults to required: true
|
|
7
|
-
class Attribute <
|
|
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
|