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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -17
  3. data/Rakefile +4 -2
  4. data/config/locales/en.yml +96 -0
  5. data/lib/treaty/attribute/base.rb +174 -0
  6. data/lib/treaty/attribute/builder/base.rb +143 -0
  7. data/lib/treaty/attribute/collection.rb +65 -0
  8. data/lib/treaty/attribute/helper_mapper.rb +72 -0
  9. data/lib/treaty/attribute/option/base.rb +160 -0
  10. data/lib/treaty/attribute/option/modifiers/as_modifier.rb +88 -0
  11. data/lib/treaty/attribute/option/modifiers/default_modifier.rb +103 -0
  12. data/lib/treaty/attribute/option/registry.rb +128 -0
  13. data/lib/treaty/attribute/option/registry_initializer.rb +90 -0
  14. data/lib/treaty/attribute/option/validators/inclusion_validator.rb +80 -0
  15. data/lib/treaty/attribute/option/validators/required_validator.rb +92 -0
  16. data/lib/treaty/attribute/option/validators/type_validator.rb +159 -0
  17. data/lib/treaty/attribute/option_normalizer.rb +151 -0
  18. data/lib/treaty/attribute/option_orchestrator.rb +187 -0
  19. data/lib/treaty/attribute/validation/attribute_validator.rb +144 -0
  20. data/lib/treaty/attribute/validation/base.rb +92 -0
  21. data/lib/treaty/attribute/validation/nested_array_validator.rb +199 -0
  22. data/lib/treaty/attribute/validation/nested_object_validator.rb +103 -0
  23. data/lib/treaty/attribute/validation/nested_transformer.rb +246 -0
  24. data/lib/treaty/attribute/validation/orchestrator/base.rb +194 -0
  25. data/lib/treaty/base.rb +9 -0
  26. data/lib/treaty/configuration.rb +17 -0
  27. data/lib/treaty/context/callable.rb +24 -0
  28. data/lib/treaty/context/dsl.rb +12 -0
  29. data/lib/treaty/context/workspace.rb +28 -0
  30. data/lib/treaty/controller/dsl.rb +38 -0
  31. data/lib/treaty/engine.rb +37 -0
  32. data/lib/treaty/exceptions/base.rb +47 -0
  33. data/lib/treaty/exceptions/class_name.rb +50 -0
  34. data/lib/treaty/exceptions/deprecated.rb +54 -0
  35. data/lib/treaty/exceptions/execution.rb +66 -0
  36. data/lib/treaty/exceptions/method_name.rb +55 -0
  37. data/lib/treaty/exceptions/nested_attributes.rb +65 -0
  38. data/lib/treaty/exceptions/not_implemented.rb +32 -0
  39. data/lib/treaty/exceptions/strategy.rb +63 -0
  40. data/lib/treaty/exceptions/unexpected.rb +70 -0
  41. data/lib/treaty/exceptions/validation.rb +97 -0
  42. data/lib/treaty/info/builder.rb +122 -0
  43. data/lib/treaty/info/dsl.rb +26 -0
  44. data/lib/treaty/info/result.rb +13 -0
  45. data/lib/treaty/request/attribute/attribute.rb +24 -0
  46. data/lib/treaty/request/attribute/builder.rb +22 -0
  47. data/lib/treaty/request/attribute/validation/orchestrator.rb +27 -0
  48. data/lib/treaty/request/attribute/validator.rb +50 -0
  49. data/lib/treaty/request/factory.rb +32 -0
  50. data/lib/treaty/request/scope/collection.rb +21 -0
  51. data/lib/treaty/request/scope/factory.rb +42 -0
  52. data/lib/treaty/response/attribute/attribute.rb +24 -0
  53. data/lib/treaty/response/attribute/builder.rb +22 -0
  54. data/lib/treaty/response/attribute/validation/orchestrator.rb +27 -0
  55. data/lib/treaty/response/attribute/validator.rb +44 -0
  56. data/lib/treaty/response/factory.rb +38 -0
  57. data/lib/treaty/response/scope/collection.rb +21 -0
  58. data/lib/treaty/response/scope/factory.rb +42 -0
  59. data/lib/treaty/result.rb +22 -0
  60. data/lib/treaty/strategy.rb +31 -0
  61. data/lib/treaty/support/loader.rb +24 -0
  62. data/lib/treaty/version.rb +8 -1
  63. data/lib/treaty/versions/collection.rb +15 -0
  64. data/lib/treaty/versions/dsl.rb +30 -0
  65. data/lib/treaty/versions/execution/request.rb +147 -0
  66. data/lib/treaty/versions/executor.rb +14 -0
  67. data/lib/treaty/versions/factory.rb +92 -0
  68. data/lib/treaty/versions/resolver.rb +69 -0
  69. data/lib/treaty/versions/semantic.rb +22 -0
  70. data/lib/treaty/versions/workspace.rb +40 -0
  71. data/lib/treaty.rb +3 -3
  72. metadata +200 -27
  73. data/.standard.yml +0 -3
  74. data/CHANGELOG.md +0 -5
  75. data/CODE_OF_CONDUCT.md +0 -84
  76. data/LICENSE.txt +0 -21
  77. data/sig/treaty.rbs +0 -4
  78. data/treaty.gemspec +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72d96fdd6fd04921e3317e4cb6da530b650b59b7f3ea49df6f9a0eed48074c3c
4
- data.tar.gz: 45838415475fbfabe6570fc29cea16d283edea99aa52f6c09ac1e37cc030c627
3
+ metadata.gz: 2697d0627eb77c0671a3b71a5aacefc47acd90339214badcc68ab30ad61b96b0
4
+ data.tar.gz: 32dfce4473730165c99715bbc83d2c822148fb6392bc47a098bcd7771131b748
5
5
  SHA512:
6
- metadata.gz: b26072eccaa4620c6e5797eb7fc549ba9b4765bb1f2cc56f6bc669cf2c84cc63533e6fb2a1588b89d629245964dc67d20d7031467551c6cfcce74a3149c0f098
7
- data.tar.gz: 90f4a34188088c711d318b1c2a9696e18219a34c9def18b4230f02303b0ed54cda24818d1dbb49568dd6c8ff482c4e65b744a93c656fddeb8b7db97d70cc1a6e
6
+ metadata.gz: b6681d643573b27f56102619b50ef8f48e332b834e580f4d0dc164f0c5e298f221da2877db78d2c88853857f682b5f183b58c86e0b2d72c540a7462670d150d7
7
+ data.tar.gz: 70f69a8794ff010e115fdb80c81fc954ed1bd2881509cbcbf9d714957714c298775291e03f2268633d4fc8e293820a3a3cdd93cc2cb249b3beb51ffd64029037
data/README.md CHANGED
@@ -1,31 +1,120 @@
1
- # Treaty
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
- A gem to use pact-verifier via FFI in order to support the latest pact features.
6
+ <div align="center">
4
7
 
5
- Should be a replacement for `pact-ruby`, but most likely is very much incompatible
6
- due to large specification differences.
8
+ [![Gem Version](https://img.shields.io/gem/v/treaty.svg)](https://rubygems.org/gems/treaty)
9
+ [![Release Date](https://img.shields.io/github/release-date/servactory/treaty)](https://github.com/servactory/servactory/releases)
10
+ [![Gem Downloads](https://img.shields.io/gem/dt/treaty.svg)](https://rubygems.org/gems/treaty)
11
+ ![Ruby Version](https://img.shields.io/badge/Ruby-3.2%2B-red)
7
12
 
8
- :warning: This is just me hacking around with `ffi` and the pact verifier. Do not use this project.
9
- ## Installation
13
+ </div>
10
14
 
11
- Install the gem and add to the application's Gemfile by executing:
15
+ ## 📚 Documentation
12
16
 
13
- $ bundle add treaty
17
+ Explore comprehensive guides and documentation at [docs](./docs):
14
18
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
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
- $ gem install treaty
26
+ ## 💡 Why Treaty?
18
27
 
19
- ## Usage
28
+ Treaty provides a complete solution for building versioned APIs in Ruby on Rails:
20
29
 
21
- ## Development
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
- ## Contributing
38
+ ## 🚀 Quick Start
24
39
 
25
- ## License
40
+ ### Installation
26
41
 
27
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
42
+ Add Treaty to your Gemfile:
28
43
 
29
- ## Code of Conduct
44
+ ```ruby
45
+ gem "treaty"
46
+ ```
30
47
 
31
- Everyone interacting in the Treaty project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/treaty/blob/main/CODE_OF_CONDUCT.md).
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
@@ -5,6 +5,8 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "standard/rake"
8
+ require "rubocop/rake_task"
9
9
 
10
- task default: %i[spec standard]
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -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