API Reference

Table of contents

  1. Base Classes
    1. TypedOperation::Base
    2. TypedOperation::ImmutableBase
  2. Configuration
    1. TypedOperation.configure
    2. TypedOperation.configuration
    3. TypedOperation.reset_configuration!
    4. Configuration#result_adapter
  3. Parameter DSL
    1. param(name, type, **options, &converter)
    2. positional_param(name, type, **options, &converter)
    3. named_param(name, type, **options, &converter)
    4. optional(type)
  4. Type System (Literal::Types)
  5. Execution Methods
    1. Class Methods
      1. .call(*args, **kwargs)
      2. .new(*args, **kwargs)
      3. .to_proc
    2. Instance Methods
      1. #call
      2. #execute_operation
      3. #to_proc
  6. Lifecycle Hooks
    1. #prepare
    2. #before_execute_operation
    3. #after_execute_operation(result)
    4. #perform
  7. Partial Application
    1. Class Methods
      1. .with(*args, **kwargs)
      2. .
      3. .curry
    2. TypedOperation::PartiallyApplied
      1. #with(*args, **kwargs) / #[]
      2. #call(*args, **kwargs)
      3. #curry
      4. #prepared?
      5. #operation
      6. #to_proc
      7. #positional_args / #keyword_args
      8. #deconstruct / #deconstruct_keys(keys)
    3. TypedOperation::Prepared
      1. #operation
      2. #prepared?
      3. #call
      4. #explain
    4. TypedOperation::Curried
      1. #call(arg)
      2. #to_proc
  8. Introspection (Class Methods)
    1. .positional_parameters
    2. .keyword_parameters
    3. .required_positional_parameters
    4. .required_keyword_parameters
    5. .optional_positional_parameters
    6. .optional_keyword_parameters
  9. Pattern Matching
    1. #deconstruct
    2. #deconstruct_keys(keys)
  10. Debugging (TypedOperation::Explainable)
    1. .explain(*args, **kwargs)
    2. TypedOperation::Instrumentation.with_output(io, color: nil, &block)
    3. TypedOperation::Instrumentation.explaining(&block)
    4. TypedOperation::Instrumentation.tracing?
    5. TypedOperation::Instrumentation.current_trace
    6. TypedOperation::Instrumentation.clear_trace!
    7. TypedOperation::Instrumentation::Trace
  11. Operation Composition
    1. Railway-Oriented Programming
    2. Context Accumulation
    3. #then(operation, **extra_kwargs, &block)
    4. #then_spreads(operation, &block)
    5. #then_passes(operation, &block)
    6. #transform(&block)
    7. #or_else(fallback, &block)
  12. TypedOperation::Context
    1. Creating and Accessing
    2. Common Operations
    3. Using Context in Operations
    4. Custom Context Classes
  13. Result Types
    1. TypedOperation::Result::Success
      1. Methods
    2. TypedOperation::Result::Failure
      1. Methods
    3. TypedOperation::Result::Mixin
    4. Pattern Matching
  14. ActionPolicy Authorization
    1. .authorized_via(*params, with:, to:, record:, &block)
    2. .action_type(type = nil)
    3. .verify_authorized!
    4. .checks_authorization?
    5. #on_authorization_failure(error)
  15. Exceptions
    1. TypedOperation::MissingParameterError
    2. TypedOperation::InvalidOperationError
    3. TypedOperation::MissingAuthentication
    4. TypedOperation::UnwrapError
    5. Literal::TypeError
    6. ActionPolicy::Unauthorized
  16. Rails Integration
    1. Generators
      1. typed_operation:install
      2. typed_operation

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) - Uses TypedOperation::Result::Success and TypedOperation::Result::Failure
  • :dry_monads - Uses Dry::Monads::Success and Dry::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:

  1. before_execute_operation
  2. perform
  3. after_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, or nil (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 Failure short-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 message
  • error.policy - The policy class that failed
  • error.rule - The policy method that returned false
  • error.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:

  • #perform is not implemented
  • Invalid operation configuration
  • explain called without Explainable

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.rb
  • test/operations/create_user_operation_test.rb