treaty 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ec3d8b8448c1cb6634404040ed78864beb81a317b705a14d6f3684caaaf5952
4
- data.tar.gz: a21afccd9a25da7531ff561ed4baae2cc0011ed8c9c5b09b7ceb1370b79ba353
3
+ metadata.gz: c10e0241b816115758a4383e524fc0f9cfb6738e95f4fddff5a1b32eb114c719
4
+ data.tar.gz: 4a5bfe17af7e6efb87975770aed1a3a67fb817aeee13cdbd05c88c187407efe0
5
5
  SHA512:
6
- metadata.gz: 0a8281e2551007b9c6b514b3d8d664d484b27e757733dd6a94d191bfdf8cce1d50a5b4f3bdee8fb8e4cadb51b1b90802697f0cd06a929c8cf39482f6aef6c5c3
7
- data.tar.gz: 1855561545ee10307c8094377ed28a132a6a9ac8f9347801f9f1d80e7f6adbb6fddc812b34e2b7960336d8558d84e9d48691aeea525c6357962b66e2a0a56e7e
6
+ metadata.gz: 12b23780c22b9987d317323e723c81d5be07ff2099e74a6cf90c4eab5bb4d0d74a6f0c8b05f11f7800daf16c02d5dd7c317c2d7df770ad99c1638933e94b6297
7
+ data.tar.gz: 4074fa86260a0f9e79adeaf6b41a61f70f5e568e7cbd6592b4eb8c19c270c954753db941be3490ce40944d1edc8cf9445caf75ef1de0b18e65e7939ad6ffd191
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.9.0"`) until the 1.0 release.
16
+ > **Development Status**: Treaty is currently under active development in the 0.x version series. Breaking changes may occur between minor versions (0.x) as we refine the API and add new features. The library will stabilize with the 1.0 release. We recommend pinning to specific patch versions in your Gemfile (e.g., `gem "treaty", "~> 0.12.0"`) until the 1.0 release.
17
17
 
18
18
  ## 📚 Documentation
19
19
 
@@ -36,6 +36,7 @@ Treaty provides a complete solution for building versioned APIs in Ruby on Rails
36
36
  - **Entity Classes (DTOs)** - Define reusable data transfer objects for better code organization
37
37
  - **Built-in Validation** - Validate incoming requests and outgoing responses automatically
38
38
  - **Data Transformation** - Transform data seamlessly between different API versions
39
+ - **Inventory System** - Pass controller-specific data to services efficiently
39
40
  - **Deprecation Management** - Mark versions as deprecated with flexible conditions
40
41
  - **Internationalization** - Full I18n support for multilingual error messages
41
42
  - **Well-documented** - Comprehensive guides and examples for every feature
@@ -64,8 +65,6 @@ Create your first API contract in `app/treaties/posts/create_treaty.rb`:
64
65
  module Posts
65
66
  class CreateTreaty < ApplicationTreaty
66
67
  version 1, default: true do
67
- strategy Treaty::Strategy::ADAPTER
68
-
69
68
  request do
70
69
  object :post do
71
70
  string :title
@@ -102,6 +101,18 @@ class PostsController < ApplicationController
102
101
  # 3. Validates service response according to response definition
103
102
  # 4. Returns transformed data to client
104
103
  treaty :create
104
+
105
+ # Optional: Provide additional data from controller to service
106
+ treaty :index do
107
+ provide :current_user
108
+ provide :posts, from: :load_posts
109
+ end
110
+
111
+ private
112
+
113
+ def load_posts
114
+ Post.published.limit(10)
115
+ end
105
116
  end
106
117
  ```
107
118
 
@@ -91,14 +91,10 @@ en:
91
91
  # Version factory
92
92
  factory:
93
93
  invalid_default_option: "Default option for version must be true, false, or a Proc, got: %{type}"
94
- unknown_method: "Unknown method '%{method}' in version definition. Available methods: summary, strategy, deprecated, request, response, delegate_to"
94
+ unknown_method: "Unknown method '%{method}' in version definition. Available methods: summary, deprecated, request, response, delegate_to"
95
95
  default_deprecated_conflict: "Version %{version} cannot be both default and deprecated. A default version must be active and usable. Either remove 'default: true' or remove the 'deprecated' declaration."
96
96
  multiple_defaults: "Cannot have multiple versions marked as default. Only one version can be the default. Please review your treaty definition and ensure only one version has 'default: true'."
97
97
 
98
- # Strategy validation
99
- strategy:
100
- unknown: "Unknown strategy: %{strategy}"
101
-
102
98
  # ============================================================================
103
99
  # Execution: Service and executor invocation
104
100
  # ============================================================================
@@ -120,3 +116,23 @@ en:
120
116
  # ============================================================================
121
117
  controller:
122
118
  treaty_class_not_found: "%{class_name}"
119
+
120
+ # ============================================================================
121
+ # Inventory: Controller data provisioning system
122
+ # ============================================================================
123
+ inventory:
124
+ # Factory DSL errors
125
+ unknown_method: "Unknown method '%{method}' in treaty block for action '%{action}'. Only 'provide' method is supported. Use: provide :name, from: :source OR provide :name"
126
+ name_must_be_symbol: "Inventory name must be a Symbol, got %{name}. Use: provide :name, from: :source OR provide :name"
127
+
128
+ # Inventory validation errors
129
+ invalid_name: "Inventory name must be a non-empty Symbol, got %{name}"
130
+ source_required: "Inventory source cannot be nil. Provide a Symbol (method name), Proc/Lambda, or direct value"
131
+ evaluation_error: "Failed to evaluate inventory item '%{name}': %{error}"
132
+
133
+ # ============================================================================
134
+ # Executor: Inventory executor wrapper
135
+ # ============================================================================
136
+ executor:
137
+ inventory:
138
+ item_not_found: "Inventory item '%{name}' not found. Available items: %{available}"
@@ -144,14 +144,14 @@ module Treaty
144
144
  # Resolves custom message with lambda support
145
145
  # If message is a lambda, calls it with provided named arguments
146
146
  #
147
- # @param context [Hash] Named arguments to pass to lambda
147
+ # @param attributes [Hash] Named arguments to pass to lambda
148
148
  # @return [String, nil] Resolved message string or nil
149
- def resolve_custom_message(**context)
149
+ def resolve_custom_message(**attributes)
150
150
  message = custom_message
151
151
  return nil if message.nil?
152
152
 
153
153
  if message.respond_to?(:call)
154
- message.call(**context)
154
+ message.call(**attributes)
155
155
  else
156
156
  message
157
157
  end
@@ -3,18 +3,17 @@
3
3
  module Treaty
4
4
  module Attribute
5
5
  module Validation
6
- # Base class for validation strategies (adapter vs non-adapter).
6
+ # Base class for request and response validation.
7
7
  #
8
8
  # ## Purpose
9
9
  #
10
- # Provides common interface for validation strategies used in Treaty.
11
- # Subclasses implement specific validation logic for different strategies.
10
+ # Provides common interface for validation used in Treaty.
11
+ # Subclasses implement specific validation logic for requests and responses.
12
12
  #
13
13
  # ## Responsibilities
14
14
  #
15
- # 1. **Strategy Interface** - Defines common validation interface
15
+ # 1. **Validation Interface** - Defines common validation interface
16
16
  # 2. **Factory Pattern** - Provides class-level validate! method
17
- # 3. **Strategy Detection** - Checks if adapter strategy is active
18
17
  #
19
18
  # ## Subclasses
20
19
  #
@@ -29,14 +28,6 @@ module Treaty
29
28
  # Example usage:
30
29
  # Request::Validation.validate!(version_factory: factory, data: params)
31
30
  #
32
- # ## Strategy Pattern
33
- #
34
- # The validation system supports two strategies:
35
- # - **Adapter Strategy** - Adapts between different API versions
36
- # - **Standard Strategy** - Direct version handling
37
- #
38
- # This base class provides `adapter_strategy?` helper to check current strategy.
39
- #
40
31
  # ## Factory Method
41
32
  #
42
33
  # The `self.validate!(...)` class method provides a convenient factory pattern:
@@ -49,7 +40,7 @@ module Treaty
49
40
  # ## Architecture
50
41
  #
51
42
  # Works with:
52
- # - VersionFactory - Provides version and strategy information
43
+ # - VersionFactory - Provides version information
53
44
  # - Orchestrator::Base - Performs actual validation and transformation
54
45
  class Base
55
46
  # Class-level factory method for validation
@@ -63,7 +54,7 @@ module Treaty
63
54
 
64
55
  # Creates a new validation instance
65
56
  #
66
- # @param version_factory [VersionFactory] Factory containing version and strategy
57
+ # @param version_factory [VersionFactory] Factory containing version information
67
58
  def initialize(version_factory:)
68
59
  @version_factory = version_factory
69
60
  end
@@ -77,15 +68,6 @@ module Treaty
77
68
  raise Treaty::Exceptions::Validation,
78
69
  I18n.t("treaty.attributes.validators.nested.orchestrator.collection_not_implemented")
79
70
  end
80
-
81
- private
82
-
83
- # Checks if adapter strategy is active
84
- #
85
- # @return [Boolean] True if using adapter strategy
86
- def adapter_strategy?
87
- @version_factory.strategy_instance.adapter?
88
- end
89
71
  end
90
72
  end
91
73
  end
@@ -9,7 +9,7 @@ module Treaty
9
9
  attr_reader :attribute_nesting_level
10
10
 
11
11
  def initialize
12
- @version = ->(context) { context }
12
+ @version = ->(controller) { controller }
13
13
 
14
14
  @attribute_nesting_level = 5
15
15
  end
@@ -3,17 +3,19 @@
3
3
  module Treaty
4
4
  module Context
5
5
  module Callable
6
- def call!(version:, params:)
7
- context = send(:new)
6
+ def call!(version:, params:, context: nil, inventory: nil)
7
+ treaty_instance = send(:new)
8
8
 
9
- _call!(context, version:, params:)
9
+ _call!(treaty_instance, context:, inventory:, version:, params:)
10
10
  end
11
11
 
12
12
  private
13
13
 
14
- def _call!(context, version:, params:)
15
- context.send(
14
+ def _call!(treaty_instance, context:, inventory:, version:, params:)
15
+ treaty_instance.send(
16
16
  :_call!,
17
+ context:,
18
+ inventory:,
17
19
  version:,
18
20
  params:,
19
21
  collection_of_versions:
@@ -6,11 +6,15 @@ module Treaty
6
6
  private
7
7
 
8
8
  def _call!(
9
+ context:,
10
+ inventory:,
9
11
  version:,
10
12
  params:,
11
13
  collection_of_versions:
12
14
  )
13
15
  call!(
16
+ context:,
17
+ inventory:,
14
18
  version:,
15
19
  params:,
16
20
  collection_of_versions:
@@ -11,11 +11,23 @@ module Treaty
11
11
  module ClassMethods
12
12
  private
13
13
 
14
- def treaty(action_name)
14
+ def treaty(action_name, &block) # rubocop:disable Metrics/MethodLength
15
+ # Capture block in a local variable before using in define_method.
16
+ # This is necessary because define_method creates a new closure,
17
+ # and the block parameter might not be accessible without explicit capture.
18
+ inventory_block = block
19
+
15
20
  define_method(action_name) do
16
- treaty = treaty_class.call!(version: treaty_version, params:)
21
+ inventory_collection = treaty_build_inventory_for(action_name, inventory_block)
22
+
23
+ treaty_result = treaty_class.call!(
24
+ context: self,
25
+ inventory: inventory_collection,
26
+ version: treaty_version,
27
+ params:
28
+ )
17
29
 
18
- render json: treaty.data, status: treaty.status
30
+ render json: treaty_result.data, status: treaty_result.status
19
31
  end
20
32
  end
21
33
  end
@@ -39,6 +51,16 @@ module Treaty
39
51
  def treaty_version
40
52
  Treaty::Engine.config.treaty.version.call(self)
41
53
  end
54
+
55
+ private
56
+
57
+ def treaty_build_inventory_for(action_name, block)
58
+ return nil unless block
59
+
60
+ factory = Treaty::Inventory::Factory.new(action_name)
61
+ factory.instance_eval(&block)
62
+ factory.collection
63
+ end
42
64
  end
43
65
  end
44
66
  end
@@ -37,7 +37,6 @@ module Treaty
37
37
  # - Deprecated - API version deprecation
38
38
  # - SpecifiedVersionNotFound - No version specified and no default configured
39
39
  # - VersionNotFound - Requested version doesn't exist
40
- # - Strategy - Invalid strategy specification
41
40
  # - ClassName - Treaty class not found
42
41
  # - MethodName - Unknown method in DSL
43
42
  # - NestedAttributes - Nesting depth exceeded
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Exceptions
5
+ # Raised when inventory definition or processing fails
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Indicates errors during inventory DSL processing, including invalid method calls,
10
+ # missing parameters, and invalid source types. Provides consistent error handling
11
+ # for the inventory system.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # Raised in various inventory scenarios:
16
+ #
17
+ # ### Invalid DSL Method
18
+ # ```ruby
19
+ # # When calling unknown method in treaty block
20
+ # treaty :index do
21
+ # unknown_method :something
22
+ # end
23
+ # # => Treaty::Exceptions::Inventory: Unknown method 'unknown_method' in treaty block for action 'index'
24
+ # ```
25
+ #
26
+ # ### Missing Required Parameters
27
+ # ```ruby
28
+ # # When 'from' parameter is missing
29
+ # treaty :index do
30
+ # provide :posts
31
+ # end
32
+ # # => Treaty::Exceptions::Inventory: Inventory 'posts' requires 'from' parameter
33
+ #
34
+ # # When inventory name is not a symbol
35
+ # treaty :index do
36
+ # provide "posts", from: :load_posts
37
+ # end
38
+ # # => Treaty::Exceptions::Inventory: Inventory name must be a Symbol, got "posts"
39
+ # ```
40
+ #
41
+ # ### Invalid Source
42
+ # ```ruby
43
+ # # When source is nil
44
+ # treaty :index do
45
+ # provide :posts, from: nil
46
+ # end
47
+ # # => Treaty::Exceptions::Inventory: Inventory source cannot be nil
48
+ # ```
49
+ #
50
+ # ## Integration
51
+ #
52
+ # Can be rescued by application controllers:
53
+ #
54
+ # ```ruby
55
+ # rescue_from Treaty::Exceptions::Inventory, with: :render_inventory_error
56
+ #
57
+ # def render_inventory_error(exception)
58
+ # render json: { error: exception.message }, status: :internal_server_error
59
+ # end
60
+ # ```
61
+ #
62
+ # ## Valid Sources
63
+ #
64
+ # - Symbol: Method name to call on controller (e.g., `:load_posts`)
65
+ # - Proc/Lambda: Callable object (e.g., `-> { Post.all }`)
66
+ # - Direct value: String, number, or any other value (e.g., `"Welcome"`)
67
+ class Inventory < Base
68
+ end
69
+ end
70
+ end
@@ -15,7 +15,6 @@ module Treaty
15
15
  #
16
16
  # ```ruby
17
17
  # version 1 do
18
- # strategy Treaty::Strategy::ADAPTER # Valid
19
18
  # deprecated true # Valid
20
19
  # summary "Version 1" # Valid
21
20
  #
@@ -38,7 +37,6 @@ module Treaty
38
37
  # ## Valid DSL Methods
39
38
  #
40
39
  # Within a version block, these methods are valid:
41
- # - strategy(code) - Set version strategy (DIRECT/ADAPTER)
42
40
  # - deprecated(condition) - Mark version as deprecated
43
41
  # - summary(text) - Add version description
44
42
  # - request(&block) - Define request schema
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Executor
5
+ # Inventory wrapper that provides method-based access to inventory items.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Wraps inventory collection and controller context, providing lazy evaluation
10
+ # of inventory items through method calls. This encapsulates all inventory logic
11
+ # within the class and provides a clean API for services.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # # Created internally by Treaty
17
+ # inventory = Treaty::Executor::Inventory.new(inventory_collection, controller_context)
18
+ #
19
+ # # Access via method calls - evaluates lazily
20
+ # inventory.posts # => Calls controller method or evaluates proc
21
+ # inventory.current_user # => Returns evaluated value
22
+ #
23
+ # # Raises exception for missing items
24
+ # inventory.missing_item # => Treaty::Exceptions::Inventory
25
+ #
26
+ # # Convert to hash - evaluates all items
27
+ # inventory.to_h # => { posts: [...], current_user: ... }
28
+ # ```
29
+ #
30
+ # ## Architecture
31
+ #
32
+ # The class encapsulates:
33
+ # - Inventory collection (from controller's treaty block)
34
+ # - Controller context (for method calls and proc evaluation)
35
+ # - Lazy evaluation logic (items evaluated on access)
36
+ #
37
+ # ## Error Handling
38
+ #
39
+ # If an inventory item is not found, raises `Treaty::Exceptions::Inventory` with
40
+ # an I18n-translated error message listing available items.
41
+ class Inventory
42
+ # Creates a new inventory instance
43
+ #
44
+ # @param inventory [Treaty::Inventory::Collection] Collection of inventory items
45
+ # @param context [Object] Controller instance for evaluation
46
+ def initialize(inventory, context)
47
+ @inventory = inventory
48
+ @context = context
49
+ @evaluated_cache = {}
50
+ end
51
+
52
+ # Provides method-based access to inventory items with lazy evaluation
53
+ #
54
+ # @param method_name [Symbol] The inventory item name
55
+ # @param _args [Array] Arguments (not used, for compatibility)
56
+ # @return [Object] The evaluated inventory item value
57
+ # @raise [Treaty::Exceptions::Inventory] If item not found
58
+ def method_missing(method_name, *_args)
59
+ # Check cache first
60
+ return @evaluated_cache[method_name] if @evaluated_cache.key?(method_name)
61
+
62
+ # Find inventory item
63
+ item = find_inventory_item(method_name)
64
+
65
+ # Evaluate and cache
66
+ @evaluated_cache[method_name] = item.evaluate(@context)
67
+ end
68
+
69
+ # Checks if inventory responds to a method
70
+ #
71
+ # @param method_name [Symbol] The method name to check
72
+ # @param include_private [Boolean] Whether to include private methods
73
+ # @return [Boolean] True if inventory has the item
74
+ def respond_to_missing?(method_name, include_private = false)
75
+ return false if @inventory.nil?
76
+
77
+ @inventory.names.include?(method_name) || super
78
+ end
79
+
80
+ # Converts inventory to hash, evaluating all items
81
+ #
82
+ # @return [Hash] Hash of all evaluated inventory items
83
+ def to_h
84
+ return {} if @inventory.nil?
85
+
86
+ @inventory.evaluate(@context)
87
+ end
88
+
89
+ # Returns string representation
90
+ #
91
+ # @return [String] Inventory description
92
+ def inspect
93
+ items = @inventory&.names || []
94
+ "#<Treaty::Executor::Inventory items=#{items.inspect}>"
95
+ end
96
+
97
+ private
98
+
99
+ # Finds inventory item by name
100
+ #
101
+ # @param name [Symbol] Inventory item name
102
+ # @return [Treaty::Inventory::Inventory] The inventory item
103
+ # @raise [Treaty::Exceptions::Inventory] If not found or inventory is nil
104
+ def find_inventory_item(name)
105
+ # Use find method for cleaner search
106
+ item = @inventory&.find { |item| item.name == name }
107
+
108
+ return item if item
109
+
110
+ # Item not found - list available items
111
+ available = @inventory&.names || []
112
+
113
+ raise Treaty::Exceptions::Inventory,
114
+ I18n.t(
115
+ "treaty.executor.inventory.item_not_found",
116
+ name:,
117
+ available: available.join(", ")
118
+ )
119
+ end
120
+ end
121
+ end
122
+ end
@@ -36,7 +36,6 @@ module Treaty
36
36
  segments: gem_version.segments,
37
37
  default: version.default_result,
38
38
  summary: version.summary_text,
39
- strategy: version.strategy_instance.code,
40
39
  deprecated: version.deprecated_result,
41
40
  executor: build_executor_with(version),
42
41
  request: build_request_with(version),
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Inventory
5
+ # Collection wrapper for sets of inventory items.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Provides a unified interface for working with collections of inventory items.
10
+ # Uses Ruby Set internally for uniqueness but exposes Array-like interface.
11
+ #
12
+ # ## Usage
13
+ #
14
+ # Used internally by:
15
+ # - Inventory::Factory (to store inventory items during DSL processing)
16
+ #
17
+ # ## Methods
18
+ #
19
+ # Delegates common collection methods to internal Set:
20
+ # - `<<` - Add inventory item
21
+ # - `empty?` - Check if collection is empty
22
+ #
23
+ # Custom methods:
24
+ # - `exists?` - Returns true if collection is not empty
25
+ # - `evaluate` - Evaluates all inventory items with context
26
+ #
27
+ # ## Example
28
+ #
29
+ # collection = Collection.new
30
+ # collection << Inventory.new(name: :posts, source: :load_posts)
31
+ # collection << Inventory.new(name: :meta, source: -> { { count: 10 } })
32
+ # collection.size # => 2
33
+ # collection.exists? # => true
34
+ class Collection
35
+ extend Forwardable
36
+
37
+ def_delegators :@collection, :<<, :each_with_object, :find, :empty?
38
+
39
+ # Creates a new collection instance
40
+ #
41
+ # @param collection [Set] Initial collection (default: empty Set)
42
+ def initialize(collection = Set.new)
43
+ @collection = collection
44
+ end
45
+
46
+ # Checks if collection has any elements
47
+ #
48
+ # @return [Boolean] True if collection is not empty
49
+ def exists?
50
+ !empty?
51
+ end
52
+
53
+ # Returns array of all inventory item names
54
+ #
55
+ # @return [Array<Symbol>] Array of inventory item names
56
+ def names
57
+ @collection.each_with_object([]) { |item, names| names << item.name }
58
+ end
59
+
60
+ # Evaluates all inventory items with the given context
61
+ #
62
+ # @param context [Object] The controller instance to call methods on
63
+ # @return [Hash{Symbol => Object}] Hash of inventory name => resolved value
64
+ def evaluate(context)
65
+ @collection.each_with_object({}) do |inventory_item, hash|
66
+ hash[inventory_item.name] = inventory_item.evaluate(context)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Inventory
5
+ # Factory for building inventory collections via DSL.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Provides the `provide` DSL method for defining inventory items in controller blocks.
10
+ # Captures calls like `provide :posts, from: :load_posts` and builds a collection.
11
+ #
12
+ # ## Usage
13
+ #
14
+ # Used internally by Controller::DSL when processing treaty blocks:
15
+ #
16
+ # ```ruby
17
+ # treaty :index do
18
+ # provide :posts, from: :load_posts # Explicit source
19
+ # provide :meta, from: -> { { count: 10 } } # Lambda source
20
+ # provide :current_user # Shorthand: uses :current_user as source
21
+ # end
22
+ # ```
23
+ #
24
+ # ## Valid Sources
25
+ #
26
+ # - Symbol: Method name to call on controller (e.g., `:load_posts`)
27
+ # - Proc/Lambda: Callable object (e.g., `-> { Post.all }`)
28
+ # - Direct value: String, number, or any other value (e.g., `"Welcome"`)
29
+ # - Omitted: Uses inventory name as source (e.g., `provide :posts` → `from: :posts`)
30
+ #
31
+ # ## Invalid Sources
32
+ #
33
+ # - Direct method calls without symbol/proc (e.g., `from: load_posts`)
34
+ # - Explicit nil values (e.g., `from: nil`)
35
+ class Factory
36
+ attr_reader :collection
37
+
38
+ def initialize(action_name)
39
+ @action_name = action_name
40
+ @collection = Collection.new
41
+ end
42
+
43
+ # Handles the `provide` DSL method via method_missing
44
+ #
45
+ # @param method_name [Symbol] Should be :provide
46
+ # @param args [Array] First argument is the inventory name
47
+ # @param options [Hash] Optional :from key with source (defaults to inventory name)
48
+ # @return [Collection] The collection being built
49
+ # @raise [Treaty::Exceptions::Inventory] For invalid method or missing parameters
50
+ def method_missing(method_name, *args, **options, &_block) # rubocop:disable Metrics/MethodLength
51
+ # Only handle 'provide' method
52
+ unless method_name == :provide
53
+ raise Treaty::Exceptions::Inventory,
54
+ I18n.t(
55
+ "treaty.inventory.unknown_method",
56
+ method: method_name,
57
+ action: @action_name
58
+ )
59
+ end
60
+
61
+ # Extract inventory name
62
+ inventory_name = args.first
63
+
64
+ unless inventory_name.is_a?(Symbol)
65
+ raise Treaty::Exceptions::Inventory,
66
+ I18n.t(
67
+ "treaty.inventory.name_must_be_symbol",
68
+ name: inventory_name.inspect
69
+ )
70
+ end
71
+
72
+ # Extract source from options (default to inventory name if not provided)
73
+ source = if options.key?(:from)
74
+ options.fetch(:from)
75
+ else
76
+ inventory_name
77
+ end
78
+
79
+ # Create and add inventory item to collection
80
+ @collection << Inventory.new(name: inventory_name, source:)
81
+
82
+ # Return collection for potential chaining
83
+ @collection
84
+ end
85
+
86
+ def respond_to_missing?(method_name, *)
87
+ method_name == :provide || super
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Treaty
4
+ module Inventory
5
+ # Represents a single inventory item that provides data to the treaty execution.
6
+ #
7
+ # An inventory item has a name and a source. The source can be:
8
+ # - Symbol: A method name to call on the controller (e.g., :load_posts)
9
+ # - Proc/Lambda: A callable object (e.g., -> { Post.all })
10
+ # - Direct value: Any other value to pass directly (e.g., "text", 42)
11
+ #
12
+ # ## Usage
13
+ #
14
+ # ```ruby
15
+ # # In controller
16
+ # treaty :index do
17
+ # provide :posts, from: :load_posts
18
+ # provide :meta, from: -> { { count: 10 } }
19
+ # provide :title, from: "Welcome"
20
+ # end
21
+ # ```
22
+ class Inventory
23
+ attr_reader :name, :source
24
+
25
+ def initialize(name:, source:)
26
+ validate_name!(name)
27
+ validate_source!(source)
28
+
29
+ @name = name
30
+ @source = source
31
+ end
32
+
33
+ # Evaluates the inventory source with the given context
34
+ #
35
+ # @param context [Object] The controller instance to call methods on
36
+ # @return [Object] The resolved value
37
+ # @raise [Treaty::Exceptions::Inventory] If evaluation fails
38
+ def evaluate(context) # rubocop:disable Metrics/MethodLength
39
+ case source
40
+ when Symbol
41
+ evaluate_symbol(context)
42
+ when Proc
43
+ evaluate_proc(context)
44
+ else
45
+ source
46
+ end
47
+ rescue StandardError => e
48
+ raise Treaty::Exceptions::Inventory,
49
+ I18n.t(
50
+ "treaty.inventory.evaluation_error",
51
+ name: @name,
52
+ error: e.message
53
+ )
54
+ end
55
+
56
+ private
57
+
58
+ # Evaluates Symbol source by calling method on context
59
+ #
60
+ # @param context [Object] The controller instance
61
+ # @return [Object] The method return value
62
+ def evaluate_symbol(context)
63
+ context.send(source)
64
+ end
65
+
66
+ # Evaluates Proc source within controller context
67
+ #
68
+ # @param context [Object] The controller instance
69
+ # @return [Object] The proc return value
70
+ def evaluate_proc(context)
71
+ # Execute proc in controller context to access instance variables and methods
72
+ context.instance_exec(&source)
73
+ end
74
+
75
+ def validate_name!(name)
76
+ return if name.is_a?(Symbol) && !name.to_s.empty?
77
+
78
+ raise Treaty::Exceptions::Inventory,
79
+ I18n.t("treaty.inventory.invalid_name", name: name.inspect)
80
+ end
81
+
82
+ def validate_source!(source)
83
+ # Source must be Symbol, Proc, or any other direct value
84
+ # We don't allow nil as it's likely a mistake
85
+ return unless source.nil?
86
+
87
+ raise Treaty::Exceptions::Inventory,
88
+ I18n.t("treaty.inventory.source_required")
89
+ end
90
+ end
91
+ end
92
+ end
@@ -34,11 +34,10 @@ module Treaty
34
34
  end
35
35
  end
36
36
 
37
- def validate_request_attributes! # rubocop:disable Metrics/MethodLength
38
- return request_data unless adapter_strategy?
37
+ def validate_request_attributes!
39
38
  return request_data unless request_attributes_exist?
40
39
 
41
- # For adapter strategy with attributes defined:
40
+ # Validate request attributes with orchestrator:
42
41
  orchestrator_class = Class.new(Treaty::Attribute::Validation::Orchestrator::Base) do
43
42
  define_method(:collection_of_attributes) do
44
43
  @version_factory.request_factory.collection_of_attributes
@@ -51,10 +50,6 @@ module Treaty
51
50
  )
52
51
  end
53
52
 
54
- def adapter_strategy?
55
- !@version_factory.strategy_instance.direct?
56
- end
57
-
58
53
  def request_attributes_exist?
59
54
  return false if @version_factory.request_factory&.collection_of_attributes&.empty?
60
55
 
@@ -29,7 +29,7 @@ module Treaty
29
29
  def validate_response_attributes!
30
30
  return @response_data unless response_attributes_exist?
31
31
 
32
- # Create orchestrator for both DIRECT and ADAPTER strategies
32
+ # Create orchestrator for response validation
33
33
  # Orchestrator filters data by attributes and performs transformation
34
34
  orchestrator_class = Class.new(Treaty::Attribute::Validation::Orchestrator::Base) do
35
35
  define_method(:collection_of_attributes) do
@@ -43,10 +43,6 @@ module Treaty
43
43
  )
44
44
  end
45
45
 
46
- def adapter_strategy?
47
- !@version_factory.strategy_instance.direct?
48
- end
49
-
50
46
  def response_attributes_exist?
51
47
  return false if @version_factory.response_factory&.collection_of_attributes&.empty?
52
48
 
@@ -3,7 +3,7 @@
3
3
  module Treaty
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 11
6
+ MINOR = 12
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
@@ -8,7 +8,9 @@ module Treaty
8
8
  new(...).execute!
9
9
  end
10
10
 
11
- def initialize(version_factory:, validated_params:)
11
+ def initialize(version_factory:, validated_params:, inventory: nil, context: nil)
12
+ @inventory = inventory
13
+ @context = context
12
14
  @version_factory = version_factory
13
15
  @validated_params = validated_params
14
16
  end
@@ -89,15 +91,26 @@ module Treaty
89
91
  ########################################################################
90
92
  ########################################################################
91
93
 
94
+ # Creates inventory wrapper with lazy evaluation
95
+ #
96
+ # @return [Treaty::Executor::Inventory] Inventory wrapper with method-based access
97
+ def evaluated_inventory
98
+ @evaluated_inventory ||= Treaty::Executor::Inventory.new(@inventory, @context)
99
+ end
100
+
101
+ ########################################################################
102
+
92
103
  def execute_proc
93
- executor.call(params: @validated_params)
104
+ # For Proc executors, pass inventory if collection exists
105
+ executor.call(**build_call_params)
94
106
  rescue StandardError => e
95
107
  raise Treaty::Exceptions::Execution,
96
108
  I18n.t("treaty.execution.proc_error", message: e.message)
97
109
  end
98
110
 
99
111
  def execute_servactory # rubocop:disable Metrics/MethodLength
100
- executor.call!(params: @validated_params)
112
+ # For Servactory services, pass inventory if collection exists
113
+ executor.call!(**build_call_params)
101
114
  rescue Servactory::Exceptions::Input => e
102
115
  raise Treaty::Exceptions::Execution,
103
116
  I18n.t("treaty.execution.servactory_input_error", message: e.message)
@@ -124,7 +137,8 @@ module Treaty
124
137
  )
125
138
  end
126
139
 
127
- executor.public_send(method_name, params: @validated_params)
140
+ # For regular classes, pass inventory if collection exists
141
+ executor.public_send(method_name, **build_call_params)
128
142
  rescue StandardError => e
129
143
  raise Treaty::Exceptions::Execution,
130
144
  I18n.t("treaty.execution.regular_service_error", message: e.message)
@@ -134,6 +148,17 @@ module Treaty
134
148
  ########################################################################
135
149
  ########################################################################
136
150
 
151
+ # Builds call parameters hash with inventory if it exists
152
+ #
153
+ # @return [Hash] Parameters hash with :params and optionally :inventory
154
+ def build_call_params
155
+ if @inventory&.exists?
156
+ { params: @validated_params, inventory: evaluated_inventory }
157
+ else
158
+ { params: @validated_params }
159
+ end
160
+ end
161
+
137
162
  def raise_executor_missing_error!
138
163
  raise Treaty::Exceptions::Execution,
139
164
  I18n.t(
@@ -6,7 +6,6 @@ module Treaty
6
6
  attr_reader :version,
7
7
  :default_result,
8
8
  :summary_text,
9
- :strategy_instance,
10
9
  :deprecated_result,
11
10
  :executor,
12
11
  :request_factory,
@@ -16,7 +15,6 @@ module Treaty
16
15
  @version = Semantic.new(version)
17
16
  @default_result = default.is_a?(Proc) ? default.call : default
18
17
  @summary_text = nil
19
- @strategy_instance = Strategy.new(Strategy::ADAPTER) # without .validate!
20
18
  @deprecated_result = false
21
19
  @executor = nil
22
20
 
@@ -35,10 +33,6 @@ module Treaty
35
33
  @summary_text = text
36
34
  end
37
35
 
38
- def strategy(code)
39
- @strategy_instance = Strategy.new(code).validate!
40
- end
41
-
42
36
  def deprecated(condition = nil)
43
37
  result =
44
38
  if condition.is_a?(Proc)
@@ -5,7 +5,7 @@ module Treaty
5
5
  module Workspace
6
6
  private
7
7
 
8
- def call!(version:, params:, **) # rubocop:disable Metrics/MethodLength
8
+ def call!(context:, inventory:, version:, params:, **) # rubocop:disable Metrics/MethodLength
9
9
  super
10
10
 
11
11
  version_factory = Resolver.resolve!(
@@ -19,6 +19,8 @@ module Treaty
19
19
  )
20
20
 
21
21
  executor_result = Execution::Request.execute!(
22
+ context:,
23
+ inventory:,
22
24
  version_factory:,
23
25
  validated_params:
24
26
  )
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: treaty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov
@@ -183,22 +183,26 @@ files:
183
183
  - lib/treaty/exceptions/class_name.rb
184
184
  - lib/treaty/exceptions/deprecated.rb
185
185
  - lib/treaty/exceptions/execution.rb
186
+ - lib/treaty/exceptions/inventory.rb
186
187
  - lib/treaty/exceptions/method_name.rb
187
188
  - lib/treaty/exceptions/nested_attributes.rb
188
189
  - lib/treaty/exceptions/not_implemented.rb
189
190
  - lib/treaty/exceptions/specified_version_not_found.rb
190
- - lib/treaty/exceptions/strategy.rb
191
191
  - lib/treaty/exceptions/unexpected.rb
192
192
  - lib/treaty/exceptions/validation.rb
193
193
  - lib/treaty/exceptions/version_default_deprecated_conflict.rb
194
194
  - lib/treaty/exceptions/version_multiple_defaults.rb
195
195
  - lib/treaty/exceptions/version_not_found.rb
196
+ - lib/treaty/executor/inventory.rb
196
197
  - lib/treaty/info/entity/builder.rb
197
198
  - lib/treaty/info/entity/dsl.rb
198
199
  - lib/treaty/info/entity/result.rb
199
200
  - lib/treaty/info/rest/builder.rb
200
201
  - lib/treaty/info/rest/dsl.rb
201
202
  - lib/treaty/info/rest/result.rb
203
+ - lib/treaty/inventory/collection.rb
204
+ - lib/treaty/inventory/factory.rb
205
+ - lib/treaty/inventory/inventory.rb
202
206
  - lib/treaty/request/attribute/attribute.rb
203
207
  - lib/treaty/request/attribute/builder.rb
204
208
  - lib/treaty/request/entity.rb
@@ -210,7 +214,6 @@ files:
210
214
  - lib/treaty/response/factory.rb
211
215
  - lib/treaty/response/validator.rb
212
216
  - lib/treaty/result.rb
213
- - lib/treaty/strategy.rb
214
217
  - lib/treaty/support/loader.rb
215
218
  - lib/treaty/version.rb
216
219
  - lib/treaty/versions/collection.rb
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Treaty
4
- module Exceptions
5
- # Raised when an unknown or invalid strategy is specified
6
- #
7
- # ## Purpose
8
- #
9
- # Ensures only valid strategy types are used in version definitions.
10
- # Prevents typos and invalid strategy configurations.
11
- #
12
- # ## Usage
13
- #
14
- # Raised when specifying an invalid strategy in version definition:
15
- #
16
- # ```ruby
17
- # version 1 do
18
- # strategy Treaty::Strategy::ADAPTER # Valid
19
- # strategy Treaty::Strategy::DIRECT # Valid
20
- #
21
- # strategy :invalid_strategy # Raises Treaty::Exceptions::Strategy
22
- # end
23
- # ```
24
- #
25
- # ## Valid Strategies
26
- #
27
- # Only two strategies are supported:
28
- #
29
- # - `Treaty::Strategy::DIRECT` - Direct pass-through mode
30
- # - No transformation between service and response
31
- # - Service output becomes response output directly
32
- # - Faster but less flexible
33
- #
34
- # - `Treaty::Strategy::ADAPTER` - Adapter mode (default)
35
- # - Transforms service output to match response schema
36
- # - Validates and adapts data structure
37
- # - More flexible, recommended for most cases
38
- #
39
- # ## Integration
40
- #
41
- # Can be rescued by application controllers:
42
- #
43
- # ```ruby
44
- # rescue_from Treaty::Exceptions::Strategy, with: :render_strategy_error
45
- #
46
- # def render_strategy_error(exception)
47
- # render json: { error: exception.message }, status: :internal_server_error
48
- # end
49
- # ```
50
- #
51
- # ## Default Behavior
52
- #
53
- # If no strategy is specified, ADAPTER is used by default:
54
- #
55
- # ```ruby
56
- # version 1 do
57
- # # strategy defaults to Treaty::Strategy::ADAPTER
58
- # end
59
- # ```
60
- class Strategy < Base
61
- end
62
- end
63
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Treaty
4
- class Strategy
5
- DIRECT = :direct
6
- ADAPTER = :adapter
7
-
8
- LIST = [DIRECT, ADAPTER].freeze
9
-
10
- attr_reader :code
11
-
12
- def initialize(code)
13
- @code = code
14
- end
15
-
16
- def validate!
17
- return self if LIST.include?(@code)
18
-
19
- raise Treaty::Exceptions::Strategy,
20
- I18n.t("treaty.versioning.strategy.unknown", strategy: @code)
21
- end
22
-
23
- def direct?
24
- @code == DIRECT
25
- end
26
-
27
- def adapter?
28
- @code == ADAPTER
29
- end
30
- end
31
- end