treaty 0.11.0 → 0.13.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.
@@ -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 = 13
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)