treaty 0.0.1 → 0.2.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 +106 -17
- data/Rakefile +4 -2
- data/config/locales/en.yml +96 -0
- data/lib/treaty/attribute/base.rb +174 -0
- data/lib/treaty/attribute/builder/base.rb +143 -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 +160 -0
- data/lib/treaty/attribute/option/modifiers/as_modifier.rb +88 -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 +92 -0
- data/lib/treaty/attribute/option/validators/type_validator.rb +159 -0
- data/lib/treaty/attribute/option_normalizer.rb +151 -0
- data/lib/treaty/attribute/option_orchestrator.rb +187 -0
- data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
- data/lib/treaty/attribute/validation/base.rb +92 -0
- data/lib/treaty/attribute/validation/nested_array_validator.rb +199 -0
- data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
- data/lib/treaty/attribute/validation/nested_transformer.rb +246 -0
- data/lib/treaty/attribute/validation/orchestrator/base.rb +194 -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 +47 -0
- data/lib/treaty/exceptions/class_name.rb +50 -0
- data/lib/treaty/exceptions/deprecated.rb +54 -0
- data/lib/treaty/exceptions/execution.rb +66 -0
- data/lib/treaty/exceptions/method_name.rb +55 -0
- data/lib/treaty/exceptions/nested_attributes.rb +65 -0
- data/lib/treaty/exceptions/not_implemented.rb +32 -0
- data/lib/treaty/exceptions/strategy.rb +63 -0
- data/lib/treaty/exceptions/unexpected.rb +70 -0
- data/lib/treaty/exceptions/validation.rb +97 -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 +147 -0
- data/lib/treaty/versions/executor.rb +14 -0
- data/lib/treaty/versions/factory.rb +92 -0
- data/lib/treaty/versions/resolver.rb +69 -0
- data/lib/treaty/versions/semantic.rb +22 -0
- data/lib/treaty/versions/workspace.rb +40 -0
- data/lib/treaty.rb +3 -3
- metadata +200 -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: 2697d0627eb77c0671a3b71a5aacefc47acd90339214badcc68ab30ad61b96b0
|
|
4
|
+
data.tar.gz: 32dfce4473730165c99715bbc83d2c822148fb6392bc47a098bcd7771131b748
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6681d643573b27f56102619b50ef8f48e332b834e580f4d0dc164f0c5e298f221da2877db78d2c88853857f682b5f183b58c86e0b2d72c540a7462670d150d7
|
|
7
|
+
data.tar.gz: 70f69a8794ff010e115fdb80c81fc954ed1bd2881509cbcbf9d714957714c298775291e03f2268633d4fc8e293820a3a3cdd93cc2cb249b3beb51ffd64029037
|
data/README.md
CHANGED
|
@@ -1,31 +1,120 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Treaty</h1>
|
|
3
|
+
<p>A Ruby library for defining and managing REST API contracts with versioning support.</p>
|
|
4
|
+
</div>
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
<div align="center">
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
[](https://rubygems.org/gems/treaty)
|
|
9
|
+
[](https://github.com/servactory/servactory/releases)
|
|
10
|
+
[](https://rubygems.org/gems/treaty)
|
|
11
|
+

|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
## Installation
|
|
13
|
+
</div>
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
## 📚 Documentation
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
Explore comprehensive guides and documentation at [docs](./docs):
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
- [Getting Started](./docs/getting-started.md) - installation and configuration
|
|
20
|
+
- [Core Concepts](./docs/core-concepts.md) - understand fundamental concepts
|
|
21
|
+
- [API Reference](./docs/api-reference.md) - complete API documentation
|
|
22
|
+
- [Examples](./docs/examples.md) - practical real-world examples
|
|
23
|
+
- [Internationalization](./docs/internationalization.md) - I18n and multilingual support
|
|
24
|
+
- [Full Documentation Index](./docs/README.md) - all documentation topics
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
## 💡 Why Treaty?
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
Treaty provides a complete solution for building versioned APIs in Ruby on Rails:
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
- **Type Safety** - Enforce strict type checking for request and response data
|
|
31
|
+
- **API Versioning** - Manage multiple concurrent API versions effortlessly
|
|
32
|
+
- **Built-in Validation** - Validate incoming requests and outgoing responses automatically
|
|
33
|
+
- **Data Transformation** - Transform data seamlessly between different API versions
|
|
34
|
+
- **Deprecation Management** - Mark versions as deprecated with flexible conditions
|
|
35
|
+
- **Internationalization** - Full I18n support for multilingual error messages
|
|
36
|
+
- **Well-documented** - Comprehensive guides and examples for every feature
|
|
22
37
|
|
|
23
|
-
##
|
|
38
|
+
## 🚀 Quick Start
|
|
24
39
|
|
|
25
|
-
|
|
40
|
+
### Installation
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
Add Treaty to your Gemfile:
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
```ruby
|
|
45
|
+
gem "treaty"
|
|
46
|
+
```
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
Run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Define Treaty
|
|
55
|
+
|
|
56
|
+
Create your first API contract in `app/treaties/posts/create_treaty.rb`:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
module Posts
|
|
60
|
+
class CreateTreaty < ApplicationTreaty
|
|
61
|
+
version 1, default: true do
|
|
62
|
+
strategy Treaty::Strategy::ADAPTER
|
|
63
|
+
|
|
64
|
+
request do
|
|
65
|
+
scope :post do
|
|
66
|
+
string :title, :required
|
|
67
|
+
string :content, :required
|
|
68
|
+
string :summary, :optional
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
response 201 do
|
|
73
|
+
scope :post do
|
|
74
|
+
string :id
|
|
75
|
+
string :title
|
|
76
|
+
string :content
|
|
77
|
+
string :summary
|
|
78
|
+
datetime :created_at
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
delegate_to Posts::CreateService
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Use in Controller
|
|
89
|
+
|
|
90
|
+
Define the treaty in your controller `app/controllers/posts_controller.rb`:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
class PostsController < ApplicationController
|
|
94
|
+
# Treaty automatically:
|
|
95
|
+
# 1. Validates incoming parameters according to request definition
|
|
96
|
+
# 2. Calls Posts::CreateService with validated data
|
|
97
|
+
# 3. Validates service response according to response definition
|
|
98
|
+
# 4. Returns transformed data to client
|
|
99
|
+
treaty :create
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 🤝 Contributing
|
|
104
|
+
|
|
105
|
+
We welcome contributions! You can help by:
|
|
106
|
+
|
|
107
|
+
- Reporting bugs and suggesting features
|
|
108
|
+
- Writing code and improving documentation
|
|
109
|
+
- Reviewing pull requests
|
|
110
|
+
- Sharing your experience with Treaty
|
|
111
|
+
|
|
112
|
+
Please read our [Contributing Guide](./CONTRIBUTING.md) before submitting a pull request.
|
|
113
|
+
|
|
114
|
+
## 🙏 Acknowledgments
|
|
115
|
+
|
|
116
|
+
Thank you to all [contributors](https://github.com/servactory/treaty/graphs/contributors) who have helped make Treaty better!
|
|
117
|
+
|
|
118
|
+
## 📄 License
|
|
119
|
+
|
|
120
|
+
Treaty is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
en:
|
|
2
|
+
treaty:
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Attributes: Definition, validation, and processing
|
|
5
|
+
# ============================================================================
|
|
6
|
+
attributes:
|
|
7
|
+
# Attribute value validators
|
|
8
|
+
validators:
|
|
9
|
+
required:
|
|
10
|
+
blank: "Attribute '%{attribute}' is required but was not provided or is empty"
|
|
11
|
+
|
|
12
|
+
type:
|
|
13
|
+
unknown_type: "Unknown type '%{type}' for attribute '%{attribute}'. Allowed types: %{allowed}"
|
|
14
|
+
mismatch:
|
|
15
|
+
integer: "Attribute '%{attribute}' must be an Integer, got %{actual}"
|
|
16
|
+
string: "Attribute '%{attribute}' must be a String, got %{actual}"
|
|
17
|
+
object: "Attribute '%{attribute}' must be a Hash (object), got %{actual}"
|
|
18
|
+
array: "Attribute '%{attribute}' must be an Array, got %{actual}"
|
|
19
|
+
datetime: "Attribute '%{attribute}' must be a DateTime/Time/Date, got %{actual}"
|
|
20
|
+
|
|
21
|
+
inclusion:
|
|
22
|
+
invalid_schema: "Option 'inclusion' for attribute '%{attribute}' must have a non-empty array of allowed values"
|
|
23
|
+
not_included: "Attribute '%{attribute}' must be one of: %{allowed}. Got: '%{value}'"
|
|
24
|
+
|
|
25
|
+
# Nested structures validation
|
|
26
|
+
nested:
|
|
27
|
+
# Orchestrator errors
|
|
28
|
+
orchestrator:
|
|
29
|
+
collection_not_implemented: "Subclass must implement the collection_of_scopes method"
|
|
30
|
+
scope_data_not_implemented: "Subclass must implement the scope_data_for method"
|
|
31
|
+
|
|
32
|
+
# Array validation errors
|
|
33
|
+
array:
|
|
34
|
+
element_validation_error: "Error in array '%{attribute}' at index %{index}: Element must match one of the defined types. Errors: %{errors}"
|
|
35
|
+
element_type_error: "Error in array '%{attribute}' at index %{index}: Expected Hash but got %{actual}"
|
|
36
|
+
attribute_error: "Error in array '%{attribute}' at index %{index}: %{message}"
|
|
37
|
+
|
|
38
|
+
# Attribute options
|
|
39
|
+
options:
|
|
40
|
+
unknown: "Unknown options for attribute '%{attribute}': %{unknown}. Known options: %{known}"
|
|
41
|
+
|
|
42
|
+
# Attribute modifiers
|
|
43
|
+
modifiers:
|
|
44
|
+
as:
|
|
45
|
+
invalid_type: "Option 'as' for attribute '%{attribute}' must be a Symbol. Got: %{type}"
|
|
46
|
+
|
|
47
|
+
# Attribute builder DSL
|
|
48
|
+
builder:
|
|
49
|
+
not_implemented: "%{class} must implement #create_attribute"
|
|
50
|
+
|
|
51
|
+
# Attribute-level errors
|
|
52
|
+
errors:
|
|
53
|
+
nesting_level_exceeded: "Nesting level %{level} exceeds maximum allowed level of %{max_level}"
|
|
54
|
+
apply_defaults_not_implemented: "%{class} must implement #apply_defaults!"
|
|
55
|
+
process_nested_not_implemented: "%{class} must implement #process_nested_attributes"
|
|
56
|
+
|
|
57
|
+
# ============================================================================
|
|
58
|
+
# Versioning: API version management and resolution
|
|
59
|
+
# ============================================================================
|
|
60
|
+
versioning:
|
|
61
|
+
# Version resolver
|
|
62
|
+
resolver:
|
|
63
|
+
current_version_required: "Current version is required for validation"
|
|
64
|
+
version_not_found: "Version %{version} not found in treaty definition"
|
|
65
|
+
version_deprecated: "Version %{version} is deprecated and cannot be used"
|
|
66
|
+
|
|
67
|
+
# Version factory
|
|
68
|
+
factory:
|
|
69
|
+
invalid_default_option: "Default option for version must be true, false, or a Proc, got: %{type}"
|
|
70
|
+
unknown_method: "Unknown method: %{method}"
|
|
71
|
+
|
|
72
|
+
# Strategy validation
|
|
73
|
+
strategy:
|
|
74
|
+
unknown: "Unknown strategy: %{strategy}"
|
|
75
|
+
|
|
76
|
+
# ============================================================================
|
|
77
|
+
# Execution: Service and executor invocation
|
|
78
|
+
# ============================================================================
|
|
79
|
+
execution:
|
|
80
|
+
executor_missing: "Executor is not defined for version %{version}"
|
|
81
|
+
executor_empty: "Executor cannot be an empty string"
|
|
82
|
+
executor_not_found: "Executor class `%{class_name}` not found"
|
|
83
|
+
executor_invalid_type: "Invalid executor type: %{type}. Expected Proc, Class, String, or Symbol"
|
|
84
|
+
method_not_found: "Method '%{method}' not found in class '%{class_name}'"
|
|
85
|
+
proc_error: "%{message}"
|
|
86
|
+
servactory_input_error: "%{message}"
|
|
87
|
+
servactory_internal_error: "%{message}"
|
|
88
|
+
servactory_output_error: "%{message}"
|
|
89
|
+
servactory_failure_error: "%{message}"
|
|
90
|
+
regular_service_error: "%{message}"
|
|
91
|
+
|
|
92
|
+
# ============================================================================
|
|
93
|
+
# Controller DSL: Rails controller integration
|
|
94
|
+
# ============================================================================
|
|
95
|
+
controller:
|
|
96
|
+
treaty_class_not_found: "%{class_name}"
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
raise Treaty::Exceptions::NestedAttributes,
|
|
125
|
+
I18n.t("treaty.attributes.errors.nesting_level_exceeded",
|
|
126
|
+
level: @nesting_level,
|
|
127
|
+
max_level: 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 [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
|
|
154
|
+
# @return [void]
|
|
155
|
+
def apply_defaults!
|
|
156
|
+
# Must be implemented in subclasses
|
|
157
|
+
raise Treaty::Exceptions::NotImplemented,
|
|
158
|
+
I18n.t("treaty.attributes.errors.apply_defaults_not_implemented", class: self.class)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Processes nested attributes block for object/array types
|
|
162
|
+
# Must be implemented in subclasses
|
|
163
|
+
#
|
|
164
|
+
# @param block [Proc] Block containing nested attribute definitions
|
|
165
|
+
# @raise [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
|
|
166
|
+
# @return [void]
|
|
167
|
+
def process_nested_attributes
|
|
168
|
+
# Must be implemented in subclasses
|
|
169
|
+
raise Treaty::Exceptions::NotImplemented,
|
|
170
|
+
I18n.t("treaty.attributes.errors.process_nested_not_implemented", class: self.class)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
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 [Treaty::Exceptions::NotImplemented] If subclass doesn't implement
|
|
134
|
+
# @return [Attribute::Base] Created attribute instance
|
|
135
|
+
def create_attribute(*)
|
|
136
|
+
# Must be implemented in subclasses
|
|
137
|
+
raise Treaty::Exceptions::NotImplemented,
|
|
138
|
+
I18n.t("treaty.attributes.builder.not_implemented", class: self.class)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
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
|