The Inventory system allows you to provide additional data from your controllers to services through the treaty execution pipeline. This enables you to pass controller-specific data, such as current user information, session data, or dynamically computed values, to your services without including them in the request parameters.
Use the provide method within a block passed to the treaty method to define inventory items:
class PostsController < ApplicationController
treaty :index do
provide :current_user # Shorthand: uses :current_user as source
provide :current_user, from: :current_user_method # Explicit source
provide :request_id, from: -> { request.uuid } # Lambda source
provide :static_value, from: "Welcome" # Direct value
end
endThe from: parameter is optional and accepts three types of sources. When omitted, the inventory name itself is used as the source.
treaty :index do
provide :posts, from: :load_posts
end
private
def load_posts
Post.where(user: current_user).limit(10)
endWhen the treaty executes, it will call load_posts on the controller instance and pass the result to the service.
treaty :index do
# Simple lambda
provide :meta, from: -> { { count: 10 } }
# Lambda with context (controller)
provide :posts, from: -> { load_posts }
# Lambda accessing request
provide :request_id, from: -> { request.uuid }
endLambdas are evaluated in the controller's context, so you can access controller methods and instance variables.
treaty :index do
provide :welcome_message, from: "Welcome to our API"
provide :api_version, from: 3
provide :feature_flags, from: { new_ui: true, beta: false }
endDirect values are passed through unchanged.
When the from: parameter is omitted, the inventory name is used as the source:
treaty :index do
# These are equivalent:
provide :current_user, from: :current_user
provide :current_user # Shorthand
# These are equivalent:
provide :posts, from: :posts
provide :posts # Shorthand
end
private
def current_user
@current_user ||= User.find(session[:user_id])
end
def posts
Post.where(user: current_user).published
endThis shorthand is particularly useful when the inventory name matches the controller method name, making the code more concise and readable.
The inventory system uses lazy evaluation to optimize performance. Inventory items are not evaluated until they are actually accessed in your service.
How it works:
-
When a treaty executes, a
Treaty::Action::Executor::Inventoryinstance is created with:- The inventory collection (definitions from your
providecalls) - The controller context (for method calls and proc evaluation)
- The inventory collection (definitions from your
-
Items are evaluated only when accessed via method calls:
# This triggers evaluation of the :posts inventory item posts = inventory.posts # This triggers evaluation of the :current_user inventory item user = inventory.current_user
-
Once evaluated, results are cached for the duration of the request:
posts1 = inventory.posts # Evaluates and caches posts2 = inventory.posts # Returns cached value, no re-evaluation
-
Calling
inventory.to_hevaluates all inventory items at once and returns a hash.
Benefits:
- Performance: Only evaluates data that is actually used
- Efficiency: Expensive operations (database queries, API calls) are skipped if not needed
- Caching: Each item is evaluated once per request, preventing redundant work
Example:
# Controller defines 3 inventory items
treaty :index do
provide :current_user # Might call database
provide :posts, from: :load_posts # Might call database
provide :meta, from: -> { build_meta } # Computation
end
# Service only uses current_user
def call
user = inputs.inventory.current_user # Only this is evaluated
# posts and meta are never evaluated - performance win!
endInventory is passed to services as a Treaty::Action::Executor::Inventory instance, which provides both method-based and hash-based access to inventory items.
The inventory object supports method calls for accessing items:
# In service
def call
posts = inventory.posts # Method call
user = inventory.current_user # Method call
meta = inventory.meta # Method call
endIf an inventory item is not found, Treaty::Exceptions::Inventory is raised with a helpful error message listing available items.
For backward compatibility or dynamic access, you can convert inventory to a hash:
# In service
def call
data = inventory.to_h # Convert to hash
posts = data[:posts] # Hash access
user = data[:current_user] # Hash access
endFor Servactory services, declare an inventory input to receive the inventory data:
class Posts::IndexService < ApplicationService::Base
input :params, type: Hash
input :inventory, required: false
output :data, type: Hash
private
def call
# Use method-based access
posts = inputs.inventory&.posts || Post.all
outputs.data = {
posts: posts.map(&:to_h),
meta: build_meta(posts)
}
end
endThe inventory input receives a Treaty::Action::Executor::Inventory instance providing method-based access to evaluated inventory items.
For Proc executors, inventory is passed as a keyword argument:
version 1 do
delegate_to(lambda do |inventory:, params:|
# Use method-based access
posts = inventory.posts
{ posts: posts, meta: { count: posts.size } }
end)
endFor regular Ruby classes, inventory is passed as a keyword argument to the specified method:
class Posts::IndexService
def self.call(inventory:, params:)
# Use method-based access
posts = inventory.posts
{ posts: posts.map(&:to_h) }
end
endclass PostsController < ApplicationController
treaty :index do
provide :current_user # Shorthand: calls current_user method
provide :posts, from: :load_posts # Explicit: calls load_posts method
provide :meta, from: -> { build_meta }
end
private
def current_user
@current_user ||= User.find(session[:user_id])
end
def load_posts
Post.where(published: true).limit(10)
end
def build_meta
{ timestamp: Time.current, api_version: 3 }
end
endmodule Posts
class IndexTreaty < ApplicationTreaty
version 3, default: true do
request do
object :filters, :optional do
string :title, :optional
string :tag, :optional
end
end
response 200 do
array :posts do
string :id
string :title
string :content
end
object :meta do
integer :count
string :timestamp
end
end
delegate_to Posts::IndexService
end
end
endmodule Posts
class IndexService < ApplicationService::Base
input :params, type: Hash
input :inventory, required: false
output :data, type: Hash
private
def call
# Access pre-loaded data from inventory using method calls
posts = inputs.inventory.posts
current_user = inputs.inventory.current_user
meta = inputs.inventory.meta
# Apply filters from params if needed
posts = apply_filters(posts, inputs.params[:filters]) if inputs.params[:filters]
outputs.data = {
posts: posts.map { |post| serialize_post(post) },
meta: meta.merge(count: posts.size)
}
end
def apply_filters(posts, filters)
posts = posts.where(title: filters[:title]) if filters[:title]
posts = posts.tagged_with(filters[:tag]) if filters[:tag]
posts
end
def serialize_post(post)
{ id: post.id, title: post.title, content: post.content }
end
end
endInventory items are evaluated only when accessed, providing significant performance benefits:
- Expensive operations (database queries, API calls) are skipped if not needed
- Each item is evaluated once and cached for the request duration
- Services can define many inventory sources without performance penalty
Keep controller-specific logic (like loading current user, checking permissions) in the controller, while keeping services focused on business logic.
Pre-load expensive data once in the controller and reuse it across multiple service calls or in different parts of your treaty processing.
Services can be tested independently by passing inventory directly:
RSpec.describe Posts::IndexService do
let(:posts) { create_list(:post, 5) }
let(:user) { create(:user) }
it "processes posts from inventory" do
result = described_class.call!(
params: {},
inventory: { posts: posts, current_user: user }
)
expect(result.data[:posts].size).to eq(5)
end
endMix and match different data sources without cluttering your request parameters.
# Good
provide :current_user # Shorthand when name matches method
provide :current_user, from: :current_user # Explicit, same result
provide :filtered_posts, from: :load_filtered_posts
# Avoid
provide :data, from: :get_data
provide :x, from: :y# Good - simple method call
provide :posts, from: :load_posts
# Good - simple lambda
provide :meta, from: -> { { count: 10 } }
# Avoid - complex logic in lambda
provide :data, from: -> do
# 20 lines of complex logic here
# This should be a method instead
endWhen using inventory in Servactory services, you must declare it as an input. The inventory is always passed to services when it exists, so services should be prepared to receive it:
# Make inventory optional if service works both with and without it
input :inventory, required: false
# Or make it required if service always needs inventory
input :inventoryNote: The inventory is a Treaty::Action::Executor::Inventory instance. Servactory's type checking is flexible and will accept it.
If a Servactory service receives inventory but hasn't declared the input, Servactory will raise Servactory::Exceptions::Input error.
# Expects inventory to provide:
# - current_user (User) - The authenticated user via inventory.current_user
# - posts (Array<Post>) - Pre-loaded posts via inventory.posts
class Posts::IndexService < ApplicationService::Base
input :params, type: Hash
input :inventory, required: false
private
def call
user = inputs.inventory.current_user
posts = inputs.inventory.posts
# ...
end
end# FORBIDDEN - This will be evaluated at load time
treaty :index do
provide :posts, from: load_posts # Error!
end
# CORRECT - Use symbol or lambda
treaty :index do
provide :posts, from: :load_posts # OK
provide :posts, from: -> { load_posts } # OK
end# FORBIDDEN - Explicitly passing nil is not allowed
treaty :index do
provide :posts, from: nil # Error!
end
# ALLOWED - Omitting from parameter uses inventory name as source
treaty :index do
provide :posts # OK: equivalent to provide :posts, from: :posts
endUnknown method 'unknown_method' in treaty block for action 'index'.
Only 'provide' method is supported. Use: provide :name, from: :source OR provide :name
Inventory name must be a Symbol, got "posts".
Use: provide :name, from: :source OR provide :name
Inventory source cannot be nil.
Provide a Symbol (method name), Proc/Lambda, or direct value
treaty :index do
if admin_user?
provide :all_posts, from: -> { Post.all }
else
provide :published_posts, from: -> { Post.published }
end
endtreaty :index do
provide :user, from: :current_user
provide :user_posts, from: -> { current_user.posts }
provide :post_count, from: -> { current_user.posts.count }
endtreaty :index do
# Shorthand - calls current_user method
provide :current_user
# Controller method with explicit source
provide :user, from: :current_user
# Lambda
provide :session_id, from: -> { session.id }
# Direct value
provide :app_name, from: "MyApp"
# Complex lambda
provide :permissions, from: -> do
current_user.roles.map(&:permissions).flatten.uniq
end
end- Core Concepts - Understanding Treaty architecture
- Defining Contracts - Creating treaties
- Examples - Real-world usage patterns
- API Reference - Complete API documentation