treaty 0.0.1 → 0.1.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 +19 -18
- data/Rakefile +4 -2
- data/lib/treaty/attribute/base.rb +172 -0
- data/lib/treaty/attribute/builder/base.rb +142 -0
- data/lib/treaty/attribute/collection.rb +65 -0
- data/lib/treaty/attribute/helper_mapper.rb +72 -0
- data/lib/treaty/attribute/option/base.rb +159 -0
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +87 -0
- data/lib/treaty/attribute/option/modifiers/default_modifier.rb +103 -0
- data/lib/treaty/attribute/option/registry.rb +128 -0
- data/lib/treaty/attribute/option/registry_initializer.rb +90 -0
- data/lib/treaty/attribute/option/validators/inclusion_validator.rb +80 -0
- data/lib/treaty/attribute/option/validators/required_validator.rb +94 -0
- data/lib/treaty/attribute/option/validators/type_validator.rb +153 -0
- data/lib/treaty/attribute/option_normalizer.rb +150 -0
- data/lib/treaty/attribute/option_orchestrator.rb +186 -0
- data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
- data/lib/treaty/attribute/validation/base.rb +93 -0
- data/lib/treaty/attribute/validation/nested_array_validator.rb +194 -0
- data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
- data/lib/treaty/attribute/validation/nested_transformer.rb +240 -0
- data/lib/treaty/attribute/validation/orchestrator/base.rb +196 -0
- data/lib/treaty/base.rb +9 -0
- data/lib/treaty/configuration.rb +17 -0
- data/lib/treaty/context/callable.rb +24 -0
- data/lib/treaty/context/dsl.rb +12 -0
- data/lib/treaty/context/workspace.rb +28 -0
- data/lib/treaty/controller/dsl.rb +38 -0
- data/lib/treaty/engine.rb +37 -0
- data/lib/treaty/exceptions/base.rb +8 -0
- data/lib/treaty/exceptions/class_name.rb +11 -0
- data/lib/treaty/exceptions/deprecated.rb +8 -0
- data/lib/treaty/exceptions/execution.rb +8 -0
- data/lib/treaty/exceptions/method_name.rb +8 -0
- data/lib/treaty/exceptions/nested_attributes.rb +8 -0
- data/lib/treaty/exceptions/strategy.rb +8 -0
- data/lib/treaty/exceptions/unexpected.rb +8 -0
- data/lib/treaty/exceptions/validation.rb +8 -0
- data/lib/treaty/info/builder.rb +122 -0
- data/lib/treaty/info/dsl.rb +26 -0
- data/lib/treaty/info/result.rb +13 -0
- data/lib/treaty/request/attribute/attribute.rb +24 -0
- data/lib/treaty/request/attribute/builder.rb +22 -0
- data/lib/treaty/request/attribute/validation/orchestrator.rb +27 -0
- data/lib/treaty/request/attribute/validator.rb +50 -0
- data/lib/treaty/request/factory.rb +32 -0
- data/lib/treaty/request/scope/collection.rb +21 -0
- data/lib/treaty/request/scope/factory.rb +42 -0
- data/lib/treaty/response/attribute/attribute.rb +24 -0
- data/lib/treaty/response/attribute/builder.rb +22 -0
- data/lib/treaty/response/attribute/validation/orchestrator.rb +27 -0
- data/lib/treaty/response/attribute/validator.rb +44 -0
- data/lib/treaty/response/factory.rb +38 -0
- data/lib/treaty/response/scope/collection.rb +21 -0
- data/lib/treaty/response/scope/factory.rb +42 -0
- data/lib/treaty/result.rb +22 -0
- data/lib/treaty/strategy.rb +31 -0
- data/lib/treaty/support/loader.rb +24 -0
- data/lib/treaty/version.rb +8 -1
- data/lib/treaty/versions/collection.rb +15 -0
- data/lib/treaty/versions/dsl.rb +30 -0
- data/lib/treaty/versions/execution/request.rb +151 -0
- data/lib/treaty/versions/executor.rb +14 -0
- data/lib/treaty/versions/factory.rb +93 -0
- data/lib/treaty/versions/resolver.rb +72 -0
- data/lib/treaty/versions/semantic.rb +22 -0
- data/lib/treaty/versions/workspace.rb +40 -0
- data/lib/treaty.rb +3 -3
- metadata +184 -27
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -84
- data/LICENSE.txt +0 -21
- data/sig/treaty.rbs +0 -4
- data/treaty.gemspec +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d99db2724f9dd4fd84a7f315d1acf070dcf70a7f3669dab9980334dc2c179076
|
|
4
|
+
data.tar.gz: d02611a96da26130b8504818fabd9adbc10fa72b8984fc8330e7b63aec79c7ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 492777e146c0042d9c78df8e9783a17d4aa705c629705425d7cd0db0ce2eaf5d8a2cf58b817c95fe1bc9025739044eb89959e91b04eb7e8dd205c6da9ab68251
|
|
7
|
+
data.tar.gz: 6133706b11654af81a88b32489d88d78f86a66df79a80a072c29fde23e2ea6130e0bee4ffe401def62f52a80ce293954228dd746bc57cf01d38e17833179f350
|
data/README.md
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
# Treaty
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This project is currently under development.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
due to large specification differences.
|
|
6
|
+
## Quick Start
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
## Installation
|
|
8
|
+
### Installation
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
```ruby
|
|
11
|
+
gem "treaty"
|
|
12
|
+
```
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
## Documentation
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
Complete documentation is available in the [docs](./docs) directory:
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
- [Getting Started](./docs/getting-started.md) - installation and first steps
|
|
19
|
+
- [Core Concepts](./docs/core-concepts.md) - fundamental concepts
|
|
20
|
+
- [API Reference](./docs/api-reference.md) - complete API documentation
|
|
21
|
+
- [Examples](./docs/examples.md) - practical examples
|
|
22
|
+
- [Full Documentation Index](./docs/README.md) - all documentation topics
|
|
22
23
|
|
|
23
24
|
## Contributing
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
This project is intended to be a safe, welcoming space for collaboration.
|
|
27
|
+
Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
28
|
+
We recommend reading the [contributing guide](./CONTRIBUTING.md) as well.
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Code of Conduct
|
|
30
|
+
## License
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Treaty is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
# Base class for all attribute definitions in Treaty DSL.
|
|
6
|
+
#
|
|
7
|
+
# ## Purpose
|
|
8
|
+
#
|
|
9
|
+
# Represents a single attribute defined in request/response scopes.
|
|
10
|
+
# Handles:
|
|
11
|
+
# - Attribute metadata (name, type, nesting level)
|
|
12
|
+
# - Helper mode to simple mode conversion
|
|
13
|
+
# - Simple mode to advanced mode normalization
|
|
14
|
+
# - Nested attributes (for object and array types)
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Attributes are created through DSL methods:
|
|
19
|
+
# string :title, :required
|
|
20
|
+
# integer :age, default: 18
|
|
21
|
+
# object :author do
|
|
22
|
+
# string :name
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# ## Processing Flow
|
|
26
|
+
#
|
|
27
|
+
# 1. Extract helpers from arguments (`:required`, `:optional`)
|
|
28
|
+
# 2. Convert helpers to simple mode options
|
|
29
|
+
# 3. Merge with explicit options
|
|
30
|
+
# 4. Normalize all options to advanced mode
|
|
31
|
+
# 5. Apply defaults (required: true for request, false for response)
|
|
32
|
+
# 6. Process nested attributes if block given
|
|
33
|
+
#
|
|
34
|
+
# ## Nested Attributes
|
|
35
|
+
#
|
|
36
|
+
# Object and array types can have nested attributes:
|
|
37
|
+
# - `object` - nested attributes as direct children
|
|
38
|
+
# - `array` - nested attributes define array element structure
|
|
39
|
+
#
|
|
40
|
+
# Special scope `:_self` is used for simple arrays:
|
|
41
|
+
# array :tags do
|
|
42
|
+
# string :_self # Array of strings
|
|
43
|
+
# end
|
|
44
|
+
class Base
|
|
45
|
+
attr_reader :name,
|
|
46
|
+
:type,
|
|
47
|
+
:options,
|
|
48
|
+
:nesting_level
|
|
49
|
+
|
|
50
|
+
# Creates a new attribute instance
|
|
51
|
+
#
|
|
52
|
+
# @param name [Symbol] The attribute name
|
|
53
|
+
# @param type [Symbol] The attribute type (:string, :integer, :object, :array, etc.)
|
|
54
|
+
# @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
|
|
55
|
+
# @param nesting_level [Integer] Current nesting depth (default: 0)
|
|
56
|
+
# @param options [Hash] Attribute options (required, default, as, etc.)
|
|
57
|
+
# @param block [Proc] Block for defining nested attributes (for object/array types)
|
|
58
|
+
def initialize(name, type, *helpers, nesting_level: 0, **options, &block)
|
|
59
|
+
@name = name
|
|
60
|
+
@type = type
|
|
61
|
+
@nesting_level = nesting_level
|
|
62
|
+
|
|
63
|
+
validate_nesting_level!
|
|
64
|
+
|
|
65
|
+
# Separate helpers from non-helper symbols.
|
|
66
|
+
@helpers = extract_helpers(helpers)
|
|
67
|
+
|
|
68
|
+
# Merge helper options with explicit options.
|
|
69
|
+
merged_options = merge_options(@helpers, options)
|
|
70
|
+
|
|
71
|
+
# Normalize all options to advanced mode.
|
|
72
|
+
@options = OptionNormalizer.normalize(merged_options)
|
|
73
|
+
|
|
74
|
+
apply_defaults!
|
|
75
|
+
|
|
76
|
+
# Process nested attributes for object and array types.
|
|
77
|
+
process_nested_attributes(&block) if block_given?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns collection of nested attributes for this attribute
|
|
81
|
+
#
|
|
82
|
+
# @return [Collection] Collection of nested attributes
|
|
83
|
+
def collection_of_attributes
|
|
84
|
+
@collection_of_attributes ||= Collection.new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Checks if this attribute has nested attributes
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] True if attribute is object/array with nested attributes
|
|
90
|
+
def nested?
|
|
91
|
+
object_or_array? && collection_of_attributes.exists?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Checks if this attribute is an object or array type
|
|
95
|
+
#
|
|
96
|
+
# @return [Boolean] True if type is :object or :array
|
|
97
|
+
def object_or_array?
|
|
98
|
+
object? || array?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Checks if this attribute is an object type
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] True if type is :object
|
|
104
|
+
def object?
|
|
105
|
+
@type == :object
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Checks if this attribute is an array type
|
|
109
|
+
#
|
|
110
|
+
# @return [Boolean] True if type is :array
|
|
111
|
+
def array?
|
|
112
|
+
@type == :array
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Validates that nesting level doesn't exceed maximum allowed depth
|
|
118
|
+
#
|
|
119
|
+
# @raise [Treaty::Exceptions::NestedAttributes] If nesting exceeds limit
|
|
120
|
+
# @return [void]
|
|
121
|
+
def validate_nesting_level!
|
|
122
|
+
return unless @nesting_level > Treaty::Engine.config.treaty.attribute_nesting_level
|
|
123
|
+
|
|
124
|
+
# TODO: It is necessary to implement a translation system (I18n).
|
|
125
|
+
raise Treaty::Exceptions::NestedAttributes,
|
|
126
|
+
"Nesting level #{@nesting_level} exceeds maximum allowed level of " \
|
|
127
|
+
"#{Treaty::Engine.config.treaty.attribute_nesting_level}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Extracts helper symbols from arguments
|
|
131
|
+
#
|
|
132
|
+
# @param helpers [Array] Mixed array that may contain helper symbols
|
|
133
|
+
# @return [Array<Symbol>] Filtered array of valid helper symbols
|
|
134
|
+
def extract_helpers(helpers)
|
|
135
|
+
helpers.select do |helper|
|
|
136
|
+
helper.is_a?(Symbol) && HelperMapper.helper?(helper)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Merges helper-derived options with explicit options
|
|
141
|
+
#
|
|
142
|
+
# @param helpers [Array<Symbol>] Helper symbols to convert
|
|
143
|
+
# @param explicit_options [Hash] Explicitly provided options
|
|
144
|
+
# @return [Hash] Merged options hash
|
|
145
|
+
def merge_options(helpers, explicit_options)
|
|
146
|
+
helper_options = HelperMapper.map(helpers)
|
|
147
|
+
helper_options.merge(explicit_options)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Applies default values for options based on context (request/response)
|
|
151
|
+
# Must be implemented in subclasses
|
|
152
|
+
#
|
|
153
|
+
# @raise [NotImplementedError] If subclass doesn't implement
|
|
154
|
+
# @return [void]
|
|
155
|
+
def apply_defaults!
|
|
156
|
+
# Must be implemented in subclasses
|
|
157
|
+
raise NotImplementedError, "#{self.class} must implement #apply_defaults!"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Processes nested attributes block for object/array types
|
|
161
|
+
# Must be implemented in subclasses
|
|
162
|
+
#
|
|
163
|
+
# @param block [Proc] Block containing nested attribute definitions
|
|
164
|
+
# @raise [NotImplementedError] If subclass doesn't implement
|
|
165
|
+
# @return [void]
|
|
166
|
+
def process_nested_attributes(&block)
|
|
167
|
+
# Must be implemented in subclasses
|
|
168
|
+
raise NotImplementedError, "#{self.class} must implement #process_nested_attributes"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Builder
|
|
6
|
+
# Base DSL builder for defining attributes in request/response scopes.
|
|
7
|
+
#
|
|
8
|
+
# ## Purpose
|
|
9
|
+
#
|
|
10
|
+
# Provides the DSL interface for defining attributes within scopes.
|
|
11
|
+
# Handles method_missing magic to support type-based method calls.
|
|
12
|
+
#
|
|
13
|
+
# ## Responsibilities
|
|
14
|
+
#
|
|
15
|
+
# 1. **DSL Interface** - Provides clean syntax for attribute definitions
|
|
16
|
+
# 2. **Method Dispatch** - Routes type methods (string, integer, etc.) to attribute creation
|
|
17
|
+
# 3. **Helper Support** - Handles helper symbols in various positions
|
|
18
|
+
# 4. **Nesting Tracking** - Tracks nesting level for nested attributes
|
|
19
|
+
#
|
|
20
|
+
# ## DSL Usage
|
|
21
|
+
#
|
|
22
|
+
# The builder enables this clean DSL syntax:
|
|
23
|
+
#
|
|
24
|
+
# ```ruby
|
|
25
|
+
# request do
|
|
26
|
+
# scope :user do
|
|
27
|
+
# string :name, :required
|
|
28
|
+
# integer :age, default: 18
|
|
29
|
+
# object :profile do
|
|
30
|
+
# string :bio
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
# ```
|
|
35
|
+
#
|
|
36
|
+
# ## Method Dispatch
|
|
37
|
+
#
|
|
38
|
+
# ### Type-based Methods
|
|
39
|
+
# When you call `string :name`, it routes through `method_missing`:
|
|
40
|
+
# 1. `string` becomes the type
|
|
41
|
+
# 2. `:name` becomes the attribute name
|
|
42
|
+
# 3. Calls `attribute(:name, :string, ...)`
|
|
43
|
+
#
|
|
44
|
+
# ### Helper Position Handling
|
|
45
|
+
# Handles helpers in different positions:
|
|
46
|
+
#
|
|
47
|
+
# ```ruby
|
|
48
|
+
# string :required, :name # Helper first, then name
|
|
49
|
+
# string :name, :required # Name first, then helper
|
|
50
|
+
# ```
|
|
51
|
+
#
|
|
52
|
+
# Both resolve to the same attribute definition.
|
|
53
|
+
#
|
|
54
|
+
# ## Nesting
|
|
55
|
+
#
|
|
56
|
+
# Tracks nesting level for:
|
|
57
|
+
# - Validation (enforcing maximum nesting depth)
|
|
58
|
+
# - Error messages (showing context)
|
|
59
|
+
#
|
|
60
|
+
# Maximum nesting level is configured in Treaty::Engine.config.
|
|
61
|
+
#
|
|
62
|
+
# ## Subclass Requirements
|
|
63
|
+
#
|
|
64
|
+
# Subclasses must implement:
|
|
65
|
+
# - `create_attribute` - Creates the appropriate attribute type (Request/Response)
|
|
66
|
+
#
|
|
67
|
+
# ## Architecture
|
|
68
|
+
#
|
|
69
|
+
# Used by:
|
|
70
|
+
# - Request::Builder - For request attribute definitions
|
|
71
|
+
# - Response::Builder - For response attribute definitions
|
|
72
|
+
class Base
|
|
73
|
+
attr_reader :nesting_level,
|
|
74
|
+
:collection_of_attributes
|
|
75
|
+
|
|
76
|
+
# Creates a new builder instance
|
|
77
|
+
#
|
|
78
|
+
# @param collection_of_attributes [Collection] Collection to add attributes to
|
|
79
|
+
# @param nesting_level [Integer] Current nesting depth
|
|
80
|
+
def initialize(collection_of_attributes, nesting_level)
|
|
81
|
+
@collection_of_attributes = collection_of_attributes
|
|
82
|
+
@nesting_level = nesting_level
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Defines an attribute with explicit type
|
|
86
|
+
#
|
|
87
|
+
# @param name [Symbol] The attribute name
|
|
88
|
+
# @param type [Symbol] The attribute type
|
|
89
|
+
# @param helpers [Array<Symbol>] Helper symbols (:required, :optional)
|
|
90
|
+
# @param options [Hash] Attribute options
|
|
91
|
+
# @param block [Proc] Block for nested attributes
|
|
92
|
+
# @return [void]
|
|
93
|
+
def attribute(name, type, *helpers, **options, &block)
|
|
94
|
+
@collection_of_attributes << create_attribute(
|
|
95
|
+
name,
|
|
96
|
+
type,
|
|
97
|
+
*helpers,
|
|
98
|
+
nesting_level: @nesting_level,
|
|
99
|
+
**options,
|
|
100
|
+
&block
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Handles DSL methods like `string :name` where method name is the type
|
|
105
|
+
#
|
|
106
|
+
# @param type [Symbol] The attribute type (method name)
|
|
107
|
+
# @param name [Symbol] The attribute name (first argument)
|
|
108
|
+
# @param helpers [Array<Symbol>] Helper symbols
|
|
109
|
+
# @param options [Hash] Attribute options
|
|
110
|
+
# @param block [Proc] Block for nested attributes
|
|
111
|
+
# @return [void]
|
|
112
|
+
def method_missing(type, name, *helpers, **options, &block)
|
|
113
|
+
if name.is_a?(Symbol) && HelperMapper.helper?(name)
|
|
114
|
+
helpers.unshift(name)
|
|
115
|
+
name = helpers.shift
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
attribute(name, type, *helpers, **options, &block)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Checks if method should be handled by method_missing
|
|
122
|
+
#
|
|
123
|
+
# @param name [Symbol] Method name
|
|
124
|
+
# @return [Boolean]
|
|
125
|
+
def respond_to_missing?(name, *)
|
|
126
|
+
super
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Creates an attribute instance (must be implemented in subclasses)
|
|
132
|
+
#
|
|
133
|
+
# @raise [NotImplementedError] If subclass doesn't implement
|
|
134
|
+
# @return [Attribute::Base] Created attribute instance
|
|
135
|
+
def create_attribute(*)
|
|
136
|
+
# Must be implemented in subclasses
|
|
137
|
+
raise NotImplementedError, "#{self.class} must implement #create_attribute"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Treaty
|
|
6
|
+
module Attribute
|
|
7
|
+
# Collection wrapper for sets of attributes.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Provides a unified interface for working with collections of attributes.
|
|
12
|
+
# Uses Ruby Set internally for uniqueness but exposes Array-like interface.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# Used internally by:
|
|
17
|
+
# - Scope factories (to store attributes in a scope)
|
|
18
|
+
# - Attribute::Base (to store nested attributes)
|
|
19
|
+
#
|
|
20
|
+
# ## Methods
|
|
21
|
+
#
|
|
22
|
+
# Delegates common collection methods to internal Set:
|
|
23
|
+
# - `<<` - Add attribute
|
|
24
|
+
# - `each`, `map`, `select`, `reject` - Iteration
|
|
25
|
+
# - `find`, `first` - Access
|
|
26
|
+
# - `size`, `empty?` - Size checks
|
|
27
|
+
# - `to_h` - Convert to hash
|
|
28
|
+
#
|
|
29
|
+
# Custom methods:
|
|
30
|
+
# - `exists?` - Returns true if collection is not empty
|
|
31
|
+
#
|
|
32
|
+
# ## Example
|
|
33
|
+
#
|
|
34
|
+
# collection = Collection.new
|
|
35
|
+
# collection << Attribute::Base.new(:name, :string)
|
|
36
|
+
# collection << Attribute::Base.new(:age, :integer)
|
|
37
|
+
# collection.size # => 2
|
|
38
|
+
# collection.exists? # => true
|
|
39
|
+
class Collection
|
|
40
|
+
extend Forwardable
|
|
41
|
+
|
|
42
|
+
def_delegators :@collection,
|
|
43
|
+
:<<,
|
|
44
|
+
:to_h, :map,
|
|
45
|
+
:each_with_object, :each,
|
|
46
|
+
:select, :reject, :size,
|
|
47
|
+
:find, :first,
|
|
48
|
+
:empty?
|
|
49
|
+
|
|
50
|
+
# Creates a new collection instance
|
|
51
|
+
#
|
|
52
|
+
# @param collection [Set] Initial collection (default: empty Set)
|
|
53
|
+
def initialize(collection = Set.new)
|
|
54
|
+
@collection = collection
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Checks if collection has any elements
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] True if collection is not empty
|
|
60
|
+
def exists?
|
|
61
|
+
!empty?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
# Maps DSL helper symbols to their simple mode option equivalents.
|
|
6
|
+
#
|
|
7
|
+
# ## Purpose
|
|
8
|
+
#
|
|
9
|
+
# Helpers provide the most concise syntax for common options.
|
|
10
|
+
# They are syntactic sugar that gets converted to simple mode options.
|
|
11
|
+
#
|
|
12
|
+
# ## Available Helpers
|
|
13
|
+
#
|
|
14
|
+
# - `:required` → `required: true`
|
|
15
|
+
# - `:optional` → `required: false`
|
|
16
|
+
#
|
|
17
|
+
# ## Usage Examples
|
|
18
|
+
#
|
|
19
|
+
# Helper mode (most concise):
|
|
20
|
+
# string :title, :required
|
|
21
|
+
# string :bio, :optional
|
|
22
|
+
#
|
|
23
|
+
# Equivalent to simple mode:
|
|
24
|
+
# string :title, required: true
|
|
25
|
+
# string :bio, required: false
|
|
26
|
+
#
|
|
27
|
+
# ## Processing Flow
|
|
28
|
+
#
|
|
29
|
+
# 1. Helper mode: `string :title, :required`
|
|
30
|
+
# 2. HelperMapper: `:required` → `required: true`
|
|
31
|
+
# 3. OptionNormalizer: `required: true` → `{ is: true, message: nil }`
|
|
32
|
+
# 4. Final: Advanced mode used internally
|
|
33
|
+
#
|
|
34
|
+
# ## Adding New Helpers
|
|
35
|
+
#
|
|
36
|
+
# To add a new helper:
|
|
37
|
+
# ```ruby
|
|
38
|
+
# HELPER_MAPPINGS = {
|
|
39
|
+
# required: { required: true },
|
|
40
|
+
# optional: { required: false },
|
|
41
|
+
# my_helper: { my_option: :smth } # New helper example
|
|
42
|
+
# }.freeze
|
|
43
|
+
# ```
|
|
44
|
+
class HelperMapper
|
|
45
|
+
HELPER_MAPPINGS = {
|
|
46
|
+
required: { required: true },
|
|
47
|
+
optional: { required: false }
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
# Maps helper symbols to their simple mode equivalents
|
|
52
|
+
#
|
|
53
|
+
# @param helpers [Array<Symbol>] Array of helper symbols
|
|
54
|
+
# @return [Hash] Simple mode options hash
|
|
55
|
+
def map(helpers)
|
|
56
|
+
helpers.each_with_object({}) do |helper, result|
|
|
57
|
+
mapping = HELPER_MAPPINGS.fetch(helper)
|
|
58
|
+
result.merge!(mapping) if mapping.present?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Checks if a symbol is a registered helper
|
|
63
|
+
#
|
|
64
|
+
# @param symbol [Symbol] Symbol to check
|
|
65
|
+
# @return [Boolean] True if symbol is a helper
|
|
66
|
+
def helper?(symbol)
|
|
67
|
+
HELPER_MAPPINGS.key?(symbol)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Treaty
|
|
4
|
+
module Attribute
|
|
5
|
+
module Option
|
|
6
|
+
# Base class for all option processors (validators and modifiers).
|
|
7
|
+
#
|
|
8
|
+
# ## Option Modes
|
|
9
|
+
#
|
|
10
|
+
# Treaty supports two modes for defining options:
|
|
11
|
+
#
|
|
12
|
+
# 1. **Simple Mode** - Concise syntax for common cases:
|
|
13
|
+
# - `required: true`
|
|
14
|
+
# - `as: :value`
|
|
15
|
+
# - `default: 12`
|
|
16
|
+
# - `in: %w[twitter linkedin]`
|
|
17
|
+
#
|
|
18
|
+
# 2. **Advanced Mode** - Extended syntax with custom messages:
|
|
19
|
+
# - `required: { is: true, message: "Custom error" }`
|
|
20
|
+
# - `as: { is: :value, message: nil }`
|
|
21
|
+
# - `inclusion: { in: %w[...], message: "Must be one of..." }`
|
|
22
|
+
#
|
|
23
|
+
# ## Helpers
|
|
24
|
+
#
|
|
25
|
+
# Helpers are shortcuts in DSL that map to simple mode options:
|
|
26
|
+
# - `:required` → `required: true`
|
|
27
|
+
# - `:optional` → `required: false`
|
|
28
|
+
#
|
|
29
|
+
# ## Advanced Mode Keys
|
|
30
|
+
#
|
|
31
|
+
# Each option in advanced mode has a value key:
|
|
32
|
+
# - Default key: `:is` (used by most options)
|
|
33
|
+
# - Special key: `:in` (used by inclusion validator)
|
|
34
|
+
#
|
|
35
|
+
# The value key is defined by overriding `value_key` method in subclasses.
|
|
36
|
+
#
|
|
37
|
+
# ## Processing Phases
|
|
38
|
+
#
|
|
39
|
+
# Each option processor can participate in three phases:
|
|
40
|
+
# - Phase 1: Schema validation (validate DSL definition correctness)
|
|
41
|
+
# - Phase 2: Value validation (validate runtime data values)
|
|
42
|
+
# - Phase 3: Value transformation (transform values: defaults, renaming, etc.)
|
|
43
|
+
class Base
|
|
44
|
+
# Creates a new option processor instance
|
|
45
|
+
#
|
|
46
|
+
# @param attribute_name [Symbol] The name of the attribute
|
|
47
|
+
# @param attribute_type [Symbol] The type of the attribute
|
|
48
|
+
# @param option_schema [Object] The option schema (simple or advanced mode)
|
|
49
|
+
def initialize(attribute_name:, attribute_type:, option_schema:)
|
|
50
|
+
@attribute_name = attribute_name
|
|
51
|
+
@attribute_type = attribute_type
|
|
52
|
+
@option_schema = option_schema
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Phase 1: Validates schema (DSL definition)
|
|
56
|
+
# Override in subclasses if validation is needed
|
|
57
|
+
#
|
|
58
|
+
# @raise [Treaty::Exceptions::Validation] If schema is invalid
|
|
59
|
+
# @return [void]
|
|
60
|
+
def validate_schema!
|
|
61
|
+
# No-op by default
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Phase 2: Validates value (runtime data)
|
|
65
|
+
# Override in subclasses if validation is needed
|
|
66
|
+
#
|
|
67
|
+
# @param value [Object] The value to validate
|
|
68
|
+
# @raise [Treaty::Exceptions::Validation] If value is invalid
|
|
69
|
+
# @return [void]
|
|
70
|
+
def validate_value!(value)
|
|
71
|
+
# No-op by default
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Phase 3: Transforms value
|
|
75
|
+
# Returns transformed value or original if no transformation needed
|
|
76
|
+
# Override in subclasses if transformation is needed
|
|
77
|
+
#
|
|
78
|
+
# @param value [Object] The value to transform
|
|
79
|
+
# @return [Object] Transformed value
|
|
80
|
+
def transform_value(value)
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Indicates if this option processor transforms attribute names
|
|
85
|
+
# Override in subclasses if needed (e.g., AsModifier)
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] True if this processor transforms names
|
|
88
|
+
def transforms_name?
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the target name for the attribute if this processor transforms names
|
|
93
|
+
# Override in subclasses if needed (e.g., AsModifier)
|
|
94
|
+
#
|
|
95
|
+
# @return [Symbol] The target attribute name
|
|
96
|
+
def target_name
|
|
97
|
+
@attribute_name
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
protected
|
|
101
|
+
|
|
102
|
+
# Returns the value key for this option in advanced mode
|
|
103
|
+
# Default is :is, but can be overridden (e.g., :in for inclusion)
|
|
104
|
+
#
|
|
105
|
+
# @return [Symbol] The key used to store the value in advanced mode
|
|
106
|
+
def value_key
|
|
107
|
+
:is
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Checks if option is enabled
|
|
111
|
+
# Handles both simple mode (boolean) and advanced mode (hash with value key)
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean] Whether the option is enabled
|
|
114
|
+
def option_enabled?
|
|
115
|
+
return false if @option_schema.nil?
|
|
116
|
+
return @option_schema if @option_schema.is_a?(TrueClass) || @option_schema.is_a?(FalseClass)
|
|
117
|
+
|
|
118
|
+
@option_schema.fetch(value_key, false)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Extracts the actual value from normalized schema
|
|
122
|
+
# Works with both simple mode and advanced mode
|
|
123
|
+
#
|
|
124
|
+
# In simple mode: returns the value directly
|
|
125
|
+
# In advanced mode: extracts value using the appropriate key (is/in)
|
|
126
|
+
#
|
|
127
|
+
# @return [Object] The actual value from the option schema
|
|
128
|
+
def option_value
|
|
129
|
+
return @option_schema unless @option_schema.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
@option_schema.fetch(value_key, nil)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Gets custom error message from advanced mode schema
|
|
135
|
+
#
|
|
136
|
+
# @return [String, nil] Custom error message or nil for default message
|
|
137
|
+
def custom_message
|
|
138
|
+
return nil unless @option_schema.is_a?(Hash)
|
|
139
|
+
|
|
140
|
+
@option_schema.fetch(:message, nil)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Checks if schema is in advanced mode
|
|
144
|
+
#
|
|
145
|
+
# @return [Boolean] True if schema is in advanced mode (hash with value key)
|
|
146
|
+
def advanced_mode?
|
|
147
|
+
@option_schema.is_a?(Hash) && @option_schema.key?(value_key)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Checks if schema is in simple mode
|
|
151
|
+
#
|
|
152
|
+
# @return [Boolean] True if schema is in simple mode (not a hash or no value key)
|
|
153
|
+
def simple_mode?
|
|
154
|
+
!advanced_mode?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|