API Reference
Table of contents
- Base Classes
- Configuration
- Parameter DSL
- Type System (Literal::Types)
- Execution Methods
- Lifecycle Hooks
- Partial Application
- Introspection (Class Methods)
- Pattern Matching
- Debugging (TypedOperation::Explainable)
- .explain(*args, **kwargs)
- TypedOperation::Instrumentation.with_output(io, color: nil, &block)
- TypedOperation::Instrumentation.explaining(&block)
- TypedOperation::Instrumentation.tracing?
- TypedOperation::Instrumentation.current_trace
- TypedOperation::Instrumentation.clear_trace!
- TypedOperation::Instrumentation::Trace
- Operation Composition
- TypedOperation::Context
- Result Types
- ActionPolicy Authorization
- Exceptions
- Rails Integration
Complete API reference for TypedOperation.
Base Classes
TypedOperation::Base
Mutable operation class built on Literal::Struct.
class MyOperation < TypedOperation::Base
param :name, String
def perform; name.upcase; end
end
TypedOperation::ImmutableBase
Immutable operation class built on Literal::Data. The instance is frozen after initialization.
class MyOperation < TypedOperation::ImmutableBase
param :name, String
def perform; name.upcase; end
end
Note: ImmutableBase does not support ActionPolicyAuth.
Configuration
TypedOperation.configure
Configure global settings for TypedOperation.
TypedOperation.configure do |config|
config.result_adapter = :dry_monads
end
TypedOperation.configuration
Returns the current Configuration instance.
TypedOperation.configuration.result_adapter
TypedOperation.reset_configuration!
Reset configuration to defaults. Primarily for testing.
TypedOperation.reset_configuration!
Configuration#result_adapter
Get or set the result adapter used for Success() and Failure() helpers.
Options:
:built_in(default) - UsesTypedOperation::Result::SuccessandTypedOperation::Result::Failure:dry_monads- UsesDry::Monads::SuccessandDry::Monads::Failure(requires dry-monads gem)
# Set via configure block
TypedOperation.configure do |config|
config.result_adapter = :dry_monads
end
# Or set directly
TypedOperation.configuration.result_adapter = :built_in
When to use each adapter:
| Adapter | Use When |
|---|---|
:built_in | Default. No external dependencies. Sufficient for most cases. |
:dry_monads | Already using dry-monads in your project. Want ecosystem integration. |
Parameter DSL
param(name, type, **options, &converter)
Define a keyword parameter.
param :email, String # Required
param :role, String, default: "user" # With default
param :bio, optional(String) # Optional (nilable)
param :count, Integer, &:to_i # With coercion
Options:
| Option | Type | Description |
|---|---|---|
positional | Boolean | If true, parameter is positional (default: false) |
default | Any/Proc | Default value or proc for lazy evaluation |
optional | Boolean | If true, wraps type in _Nilable |
reader | Symbol | Visibility: :public, :private, :protected |
Coercion block: Called with the input value before type checking.
positional_param(name, type, **options, &converter)
Define a positional parameter. Equivalent to param with positional: true.
positional_param :filename, String
positional_param :format, String, default: "json"
named_param(name, type, **options, &converter)
Alias for param with positional: false. Explicit keyword parameter.
optional(type)
Wraps a type to accept nil. Returns Literal::Types::NilableType.
param :nickname, optional(String) # Equivalent to _Nilable(String)
Type System (Literal::Types)
Include Literal::Types for type helpers:
class MyOp < TypedOperation::Base
include Literal::Types
param :active, _Boolean
param :tags, _Array(String)
param :meta, _Hash(String, Integer)
param :id, _Union(Integer, String)
param :user, _Nilable(User)
param :status, _Enum("pending", "active")
end
| Type | Description |
|---|---|
_Boolean | true or false |
_Array(T) | Array containing type T |
_Hash(K, V) | Hash with key type K, value type V |
_Union(A, B, ...) | Any of the specified types |
_Nilable(T) | Type T or nil |
_Enum(*values) | One of the specified values |
See literal gem documentation for complete type system.
Execution Methods
Class Methods
.call(*args, **kwargs)
Instantiate and execute the operation. Returns the result of perform.
result = MyOperation.call(name: "Alice")
.new(*args, **kwargs)
Create an operation instance without executing.
op = MyOperation.new(name: "Alice")
op.call # Execute later
.to_proc
Convert operation to a proc that calls .call.
proc = MyOperation.to_proc
proc.call(name: "Alice")
Instance Methods
#call
Execute the operation. Calls #execute_operation internally.
op = MyOperation.new(name: "Alice")
result = op.call
#execute_operation
Internal execution method. Calls hooks and perform:
before_execute_operationperformafter_execute_operation(result)
#to_proc
Convert instance to a proc that calls #call.
Lifecycle Hooks
#prepare
Called after initialization, before execution. Use for validation or setup.
def prepare
raise ArgumentError, "Invalid" unless valid?
end
If prepare raises an exception, the operation does not execute and the exception propagates to the caller.
#before_execute_operation
Called immediately before perform. Always call super.
def before_execute_operation
@start_time = Time.current
super
end
If this hook raises an exception, perform is not called and the exception propagates to the caller.
#after_execute_operation(result)
Called after perform with its return value. Return value becomes final result. Always call super.
def after_execute_operation(result)
log_result(result)
super # Returns result
end
If this hook raises an exception, it propagates to the caller. The perform method has already completed.
#perform
Required. Main business logic. Return value is the operation result.
def perform
User.create!(email: email)
end
Raises InvalidOperationError if not implemented.
Note on error handling: Exceptions from callbacks (prepare, before_execute_operation, after_execute_operation) propagate to the caller. Use Dry::Monads Result types for explicit error handling instead of exceptions.
Partial Application
Class Methods
.with(*args, **kwargs)
Create a partially applied operation. Returns PartiallyApplied or Prepared.
partial = MyOp.with(name: "Alice") # PartiallyApplied or Prepared
.
Alias for .with.
partial = MyOp[name: "Alice"]
.curry
Create a curried operation. Returns Curried.
curried = MyOp.curry
curried.(arg1).(arg2).(arg3)
TypedOperation::PartiallyApplied
Represents an operation missing some required parameters.
#with(*args, **kwargs) / #[]
Add more parameters. Returns PartiallyApplied or Prepared.
partial = partial.with(age: 30)
#call(*args, **kwargs)
Add parameters and execute if prepared. Raises MissingParameterError if still missing params.
partial.call(remaining: "value")
#curry
Convert to Curried for single-argument application.
#prepared?
Returns false.
#operation
Raises MissingParameterError.
#to_proc
Returns proc that calls #call.
#positional_args / #keyword_args
Access stored arguments.
#deconstruct / #deconstruct_keys(keys)
Pattern matching support.
TypedOperation::Prepared
Extends PartiallyApplied. All required parameters provided.
#operation
Returns the operation instance without executing.
op = prepared.operation
op.name # Access parameters
#prepared?
Returns true.
#call
Execute the operation.
#explain
Execute with tracing. Requires Explainable.
TypedOperation::Curried
Wraps an operation for single-argument currying.
#call(arg)
Apply next argument. Returns Curried or executes if all required params provided.
curried.(10).(20).(30) # => result
#to_proc
Returns proc that calls #call.
Introspection (Class Methods)
.positional_parameters
Returns array of positional parameter names.
MyOp.positional_parameters # => [:arg1, :arg2]
.keyword_parameters
Returns array of keyword parameter names.
MyOp.keyword_parameters # => [:name, :email, :role]
.required_positional_parameters
Returns array of required positional parameter names.
.required_keyword_parameters
Returns array of required keyword parameter names.
.optional_positional_parameters
Returns array of optional positional parameter names.
.optional_keyword_parameters
Returns array of optional keyword parameter names.
Pattern Matching
#deconstruct
Array-style pattern matching. Returns positional parameter values.
case operation
in MyOp[value1, value2]
# Match positional values
end
#deconstruct_keys(keys)
Hash-style pattern matching. Returns parameter hash.
case operation
in MyOp[name:, age:]
puts "#{name}, #{age}"
in MyOp[name:, role: "admin"]
puts "Admin: #{name}"
end
Debugging (TypedOperation::Explainable)
Include in your base operation to enable tracing:
class ApplicationOperation < TypedOperation::Base
include TypedOperation::Explainable
end
.explain(*args, **kwargs)
Execute with tracing. Prints execution tree to stdout.
MyOp.explain(param: value)
# Output:
# MyOp [1.23ms] ✓
# ├── NestedOp [0.45ms] ✓
# └── AnotherOp [0.67ms] ✓
To customize output or disable color, use Instrumentation.with_output.
TypedOperation::Instrumentation.with_output(io, color: nil, &block)
Control output and color for tracing within a block.
# Write to file
File.open("trace.log", "w") do |f|
TypedOperation::Instrumentation.with_output(f, color: false) do
MyOp.explain(param: value)
end
end
# Disable color for stdout
TypedOperation::Instrumentation.with_output($stdout, color: false) do
MyOp.explain(param: value)
end
Parameters:
io- IO object (e.g., file, $stdout, $stderr)color:-true,false, ornil(auto-detect)
TypedOperation::Instrumentation.explaining(&block)
Trace multiple operations in a block:
TypedOperation::Instrumentation.explaining do
Op1.call(...)
Op2.call(...)
end
TypedOperation::Instrumentation.tracing?
Returns true if inside a trace context.
TypedOperation::Instrumentation.current_trace
Returns current Trace object or nil.
TypedOperation::Instrumentation.clear_trace!
Clears all trace context from the current thread.
TypedOperation::Instrumentation::Trace
The Trace object returned by current_trace has these properties:
| Property | Type | Description |
|---|---|---|
operation_class | Class | The operation class being traced |
operation_name | String | Class name as string |
params | Hash | Parameters passed to the operation |
start_time | Float | Monotonic clock time when execution started |
end_time | Float | Monotonic clock time when execution finished |
duration_ms | Float | Execution time in milliseconds |
result | Any | Return value of the operation |
exception | Exception | Exception raised, if any |
success? | Boolean | true if no exception was raised |
children | Array | Nested Trace objects for child operations |
# Access trace data programmatically
TypedOperation::Instrumentation.explaining do
result = MyOperation.call(param: value)
trace = TypedOperation::Instrumentation.current_trace
trace.children.each do |child|
puts "#{child.operation_name}: #{child.duration_ms}ms"
end
end
Operation Composition
These methods are available on operations, PartiallyApplied, and Prepared.
Railway-Oriented Programming
Composition chains implement railway-oriented programming with two tracks:
- Success Track: Operations execute sequentially while each returns
Success - Failure Track: First
Failureshort-circuits the chain, skipping remaining operations
ValidateEmail
.then(CreateUser) # Skipped if ValidateEmail fails
.then(SendWelcome) # Skipped if CreateUser fails
.or_else(HandleError) # Only executes on Failure
Context Accumulation
Chains accumulate context across operations. Each operation’s return value is merged into the context hash, which flows to subsequent operations.
class Op1 < TypedOperation::Base
param :input, String
def perform
Success(result1: input.upcase)
end
end
class Op2 < TypedOperation::Base
param :input, String
param :result1, String
def perform
Success(result2: "#{input} + #{result1}")
end
end
chain = Op1.then(Op2)
result = chain.call(input: "hello")
result.value!
# => {input: "hello", result1: "HELLO", result2: "hello + HELLO"}
#then(operation, **extra_kwargs, &block)
Smart sequential composition. Auto-detects whether to spread kwargs or pass as single value based on the next operation’s signature.
How it works:
- Inspects next operation’s parameters
- If operation has keyword params: extracts needed values from context as
**kwargs - If operation has positional params: passes entire context hash as single argument
- Merges result back into context
Constraint: Raises ArgumentError if operation has both positional AND keyword params. Use .transform to adapt.
# With keyword param operation
ValidateUser
.with(email: email)
.then(CreateUser) # Receives extracted kwargs
.call
# With block (receives context hash)
ValidateUser
.then { |ctx| SendWelcome.with(user_id: ctx[:user_id]) }
.call
# Pass extra kwargs to override context values
Op1.then(Op2, role: "admin") # Merges {role: "admin"} into context
Block receives context hash, not individual values:
ValidateUser
.then { |ctx| SendEmail.with(email: ctx[:user][:email]) }
.call
#then_spreads(operation, &block)
Always spreads the context as **kwargs to the next operation. Use when you want explicit kwargs spreading.
op.then_spreads(NextOp) # NextOp.call(**context)
# With block (receives context hash)
op.then_spreads { |ctx| ctx.merge(processed: true) }
Context must be a Hash or respond to #to_h. Raises ArgumentError otherwise.
#then_passes(operation, &block)
Always passes the context as a single positional argument to the next operation. Use for operations expecting a single Hash or Context object.
op.then_passes(NextOp) # NextOp.call(context)
# With block (receives context hash)
op.then_passes { |ctx| {result: ctx[:value] * 2} }
#transform(&block)
Maps over the success value, replacing the context entirely (does not merge). Block receives context hash. Block return is auto-wrapped in Success.
Use for changing the shape of the context or extracting specific values.
# Replace context with transformed value
op.transform { |ctx| {new_key: ctx[:old_key].upcase} }
# Extract single value
op.transform { |ctx| ctx[:user][:email] }
# Not called on Failure (short-circuits)
failing_op.transform { |ctx| raise "never called" }
Difference from .then: .transform replaces context, .then merges into context.
#or_else(fallback, &block)
Provides a fallback when the operation fails. Only called if left side returns Failure. Passes through Success unchanged.
With operation class: Receives original call arguments (allows retry with different implementation).
ChargeCard
.with(amount: 100)
.or_else(ChargeBackupPaymentGateway) # Receives same {amount: 100}
.call
With block: Receives the failure value.
ValidateUser
.or_else { |failure|
Rails.logger.error(failure)
Failure([:handled, failure])
}
.call
Not called on success:
successful_op.or_else { raise "never called" } # Fallback skipped
TypedOperation::Context
Specialized Hash wrapper for passing data through pipelines and chains. Provides dot notation access and common Hash-like methods.
Creating and Accessing
# Create
ctx = TypedOperation::Context.new(user: "alice", role: "admin")
# Read
ctx[:user] # Hash-style
ctx.user # Dot notation
ctx.fetch(:user, "default") # With default
# Write
ctx[:role] = "admin"
ctx.role = "admin"
# Check keys
ctx.key?(:user) # => true
ctx.user? # => true (predicate method)
Common Operations
# Conversion
ctx.to_h # Returns Hash (dup)
# Merging
ctx.merge(email: "user@example.com") # Returns new Context
# Iteration
ctx.keys # => [:user, :role]
ctx.values # => ["alice", "admin"]
ctx.each { |k, v| puts "#{k}: #{v}" }
# Filtering
ctx.select { |k, v| v.is_a?(String) } # Returns new Context
ctx.reject { |k, v| v.nil? }
# Transformation
ctx.transform_values { |v| v.to_s }
ctx.slice(:user, :role)
ctx.except(:internal_field)
# Inspection
ctx.empty? # => false
ctx.size # => 2
Using Context in Operations
class MyOperation < TypedOperation::Base
positional_param :ctx, TypedOperation::Context
def perform
user = ctx.user # Dot notation
role = ctx[:role] # Hash notation
ctx.processed = true
Success(ctx)
end
end
Custom Context Classes
Implement this interface to use your own context class:
class MyContext
def [](key) # Read value
def []=(key, val) # Write value (optional if immutable)
def key?(key) # Check key existence
def to_h # Convert to Hash
def merge(other) # Combine, returns new context
end
Result Types
TypedOperation provides built-in Success and Failure types for explicit error handling. Operations can return these types directly, or use the configured result adapter via the Success() and Failure() helper methods.
TypedOperation::Result::Success
Represents a successful result. Immutable value object.
success = TypedOperation::Result::Success.new(42)
success.success? # => true
success.failure? # => false
success.value! # => 42
success.value # => 42
success.failure # => nil
Methods
| Method | Returns | Description |
|---|---|---|
success? | true | Always returns true |
failure? | false | Always returns false |
value! | Any | Returns the wrapped value |
value | Any | Alias for value! |
failure | nil | Always returns nil for Success |
deconstruct | Array | Pattern matching support (array destructuring) |
deconstruct_keys(keys) | Hash | Pattern matching support (hash destructuring) |
TypedOperation::Result::Failure
Represents a failed result. Immutable value object.
failure = TypedOperation::Result::Failure.new(:not_found)
failure.success? # => false
failure.failure? # => true
failure.error # => :not_found
failure.failure # => :not_found
failure.value! # Raises TypedOperation::UnwrapError
Methods
| Method | Returns | Description |
|---|---|---|
success? | false | Always returns false |
failure? | true | Always returns true |
value! | N/A | Raises UnwrapError |
error | Any | Returns the wrapped error value |
failure | Any | Alias for error |
deconstruct | Array | Pattern matching support (array destructuring) |
deconstruct_keys(keys) | Hash | Pattern matching support (hash destructuring) |
TypedOperation::Result::Mixin
Include this module to get Success() and Failure() helper methods that use the configured result adapter.
class MyOperation < TypedOperation::Base
include TypedOperation::Result::Mixin
def perform
return Failure(:invalid) unless valid?
Success(compute_result)
end
end
The helpers are private methods that delegate to the configured adapter:
Success(value) # Uses TypedOperation.configuration.result_adapter.success(value)
Failure(error) # Uses TypedOperation.configuration.result_adapter.failure(error)
Pattern Matching
Both Success and Failure support Ruby 3+ pattern matching:
case result
in Success(value)
puts "Got: #{value}"
in Failure(error)
puts "Error: #{error}"
end
# With array destructuring
case result
in Failure[:validation_error, details]
puts "Validation failed: #{details}"
end
# With hash destructuring (delegates to wrapped value)
case success_with_hash
in Success[name:, email:]
puts "User: #{name} (#{email})"
end
ActionPolicy Authorization
Include TypedOperation::ActionPolicyAuth for authorization. Only works with Base, not ImmutableBase.
.authorized_via(*params, with:, to:, record:, &block)
Configure authorization. Accepts one or more parameter names that provide authorization context (typically a user or account).
# With policy class
authorized_via :current_user, with: PostPolicy, to: :create?
# With block (creates inline policy)
authorized_via :current_user do
user.admin?
end
# With record to authorize
authorized_via :current_user, with: PostPolicy, to: :destroy?, record: :post
# Multiple context parameters
authorized_via :current_owner, :new_owner, with: TransferPolicy, to: :can_transfer?
# record parameter can refer to any parameter or method
authorized_via :current_user, with: PostPolicy, to: :update?, record: :post_to_edit
Parameters:
| Option | Description |
|---|---|
*params | One or more parameter names providing authorization context. Multiple params can be passed for policies that need multiple actors. |
with: | Policy class to use. Required unless &block provided. |
to: | Policy method to call (e.g., :create?). Defaults to action_type method if not specified. |
record: | Optional. Parameter or method name providing the record to authorize. If omitted, ActionPolicy attempts to infer it from context. |
&block | Inline policy definition (alternative to with:). Creates anonymous policy class. |
Authorization is inherited by subclasses.
.action_type(type = nil)
Set/get action type. Used as default policy method.
action_type :create
# Calls :create? on policy
.verify_authorized!
Require authorization in all subclasses. Raises MissingAuthentication if not configured.
.checks_authorization?
Returns true if authorization is configured.
#on_authorization_failure(error)
Hook called when authorization fails. Define as instance method to add side effects like logging. The authorization error (ActionPolicy::Unauthorized) is always re-raised after this hook executes.
def on_authorization_failure(error)
Rails.logger.warn "Unauthorized: #{current_user&.id} - #{error.message}"
Rails.logger.warn "Policy: #{error.policy}, Rule: #{error.rule}"
end
The error parameter is an ActionPolicy::Unauthorized exception with these properties:
error.message- Human-readable error messageerror.policy- The policy class that failederror.rule- The policy method that returned falseerror.result- ActionPolicy result object with detailed failure reasons
Exceptions
TypedOperation::MissingParameterError
Raised when calling a PartiallyApplied operation or accessing .operation without all required parameters.
TypedOperation::InvalidOperationError
Raised when:
#performis not implemented- Invalid operation configuration
explaincalled withoutExplainable
TypedOperation::MissingAuthentication
Raised when verify_authorized! is set but authorization is not configured.
TypedOperation::UnwrapError
Raised when calling value! on a Failure result.
failure = TypedOperation::Result::Failure.new(:not_found)
failure.value! # Raises UnwrapError: "Cannot unwrap Failure: :not_found"
Literal::TypeError
Raised when a parameter value doesn’t match its type constraint.
ActionPolicy::Unauthorized
Raised when authorization check fails (from ActionPolicy gem).
Rails Integration
Generators
typed_operation:install
Creates ApplicationOperation base class.
bin/rails g typed_operation:install
bin/rails g typed_operation:install --dry_monads
bin/rails g typed_operation:install --action_policy
typed_operation
Creates an operation and test file.
bin/rails g typed_operation CreateUser email:string name:string
Creates:
app/operations/create_user_operation.rbtest/operations/create_user_operation_test.rb