Comprehensive examples demonstrating TypedOperation features.

Complete Operation Example

class ShelveBookOperation < ::TypedOperation::Base
  # Parameters can be specified with `positional_param`/`named_param` or directly with the
  # underlying `param` method.
  
  # Note that you may also like to simply alias the param methods to your own preferred names:
  # `positional`/`named` or `arg`/`key` for example.
  
  # A positional parameter (positional argument passed to the operation when creating it).
  positional_param :title, String
  # Or if you prefer:
  #   `param :title, String, positional: true`
  
  # A named parameter (keyword argument passed to the operation when creating it).
  named_param :description, String
  # Or if you prefer:
  #   `param :description, String`

  # `param` creates named parameters by default
  param :author_id, Integer, &:to_i
  param :isbn, String
  
  # Optional parameters are specified by wrapping the type constraint in the `optional` method, or using the `optional:` option
  param :shelf_code, optional(Integer)
  # Or if you prefer:
  #   `named_param :shelf_code, Integer, optional: true`

  param :category, String, default: "unknown".freeze

  # optional hook called when the operation is initialized, and after the parameters have been set
  def prepare
    raise ArgumentError, "ISBN is invalid" unless valid_isbn?
  end

  # optionally hook in before execution ... and call super to allow subclasses to hook in too
  def before_execute_operation
    # ...
    super
  end

  # The 'work' of the operation, this is the main body of the operation and must be implemented
  def perform
    "Put away '#{title}' by author ID #{author_id}#{shelf_code ? " on shelf #{shelf_code}" : "" }"
  end

  # optionally hook in after execution ... and call super to allow subclasses to hook in too
  def after_execute_operation(result)
    # ...
    super
  end
  
  private

  def valid_isbn?
    # ...
    true
  end
end

shelve = ShelveBookOperation.new("The Hobbit", description: "A book about a hobbit", author_id: "1", isbn: "978-0261103283")
# => #<ShelveBookOperation:0x0000000108b3e490 @attributes={:title=>"The Hobbit", :description=>"A book about a hobbit", :author_id=>1, :isbn=>"978-0261103283", :shelf_code=>nil, :category=>"unknown"}, ...

shelve.call
# => "Put away 'The Hobbit' by author ID 1"

shelve = ShelveBookOperation.with("The Silmarillion", description: "A book about the history of Middle-earth", shelf_code: 1)
# => #<TypedOperation::PartiallyApplied:0x0000000103e6f560 ...

shelve.call(author_id: "1", isbn: "978-0261102736")
# => "Put away 'The Silmarillion' by author ID 1 on shelf 1"

curried = shelve.curry
# => #<TypedOperation::Curried:0x0000000108d98a10 ...

curried.(1).("978-0261102736")
# => "Put away 'The Silmarillion' by author ID 1 on shelf 1"

shelve.call(author_id: "1", isbn: false)
# => Raises an error because isbn is invalid
# :in `initialize': Expected `false` to be of type: `String`. (Literal::TypeError)

Partially applying parameters

Operations can also be partially applied and curried:

class TestOperation < ::TypedOperation::Base
  param :foo, String, positional: true
  param :bar, String
  param :baz, String, &:to_s

  def perform = "It worked! (#{foo}, #{bar}, #{baz})"
end

# Invoking the operation directly
TestOperation.("1", bar: "2", baz: 3)
# => "It worked! (1, 2, 3)"

# Partial application of parameters
partially_applied = TestOperation.with("1").with(bar: "2")
# => #<TypedOperation::PartiallyApplied:0x0000000110270248 @keyword_args={:bar=>"2"}, @operation_class=TestOperation, @positional_args=["1"]>

# You can partially apply more than one parameter at a time, and chain calls to `.with`.
# With all the required parameters set, the operation is 'prepared' and can be instantiated and called
prepared = TestOperation.with("1", bar: "2").with(baz: 3)
# => #<TypedOperation::Prepared:0x0000000110a9df38 @keyword_args={:bar=>"2", :baz=>3}, @operation_class=TestOperation, @positional_args=["1"]>

# A 'prepared' operation can instantiated & called
prepared.call
# => "It worked! (1, 2, 3)"

# You can provide additional parameters when calling call on a partially applied operation
partially_applied.call(baz: 3)
# => "It worked! (1, 2, 3)"

# Partial application can be done using `.with or `.[]`
TestOperation.with("1")[bar: "2", baz: 3].call
# => "It worked! (1, 2, 3)"

# Currying an operation, note that *all required* parameters must be provided an argument in order  
TestOperation.curry.("1").("2").(3)
# => "It worked! (1, 2, 3)"

# You can also curry from an already partially applied operation, so you can set optional named parameters first.
# Note currying won't let you set optional positional parameters.
partially_applied = TestOperation.with("1")
partially_applied.curry.("2").(3)
# => "It worked! (1, 2, 3)"

# > TestOperation.with("1").with(bar: "2").call
# => Raises an error because it is PartiallyApplied and so can't be called (it is missing required args)
#      "Cannot call PartiallyApplied operation TestOperation, are you expecting it to be Prepared? (TypedOperation::MissingParameterError)"

TestOperation.with("1").with(bar: "2").with(baz: 3).operation
# same as > TestOperation.new("1", bar: "2", baz: 3)
# => <TestOperation:0x000000014a0048a8 ...>

# > TestOperation.with(foo: "1").with(bar: "2").operation
# => Raises an error because it is PartiallyApplied so operation can't be instantiated
#      "Cannot instantiate Operation TestOperation, as it is only partially applied. (TypedOperation::MissingParameterError)"

Dummy App Examples

The following examples are from the test dummy Rails application included in the gem’s source code. You can find these files in test/dummy/app/operations/ and run them in the dummy app context.

ApplicationOperation (Base Class)

A minimal base class using built-in Result types:

# test/dummy/app/operations/application_operation.rb
class ApplicationOperation < TypedOperation::Base
  include TypedOperation::Result::Mixin
end

Simple Operation with Result Types

A standalone operation demonstrating Success/Failure returns:

# test/dummy/app/operations/products/create_operation.rb
module Products
  class CreateOperation < ApplicationOperation
    param :name, String
    param :price, Numeric
    param :pen_type, String
    param :stock_quantity, Integer, default: 0

    def perform
      product = Product.new(name:, price:, pen_type:, stock_quantity:)

      if product.save
        Success(product:)
      else
        Failure(errors: product.errors.full_messages)
      end
    end
  end
end

Pipeline with Multiple Operations

A complete order processing pipeline demonstrating:

  • Named steps
  • Conditional steps (if: option)
  • Transform blocks
  • Multiple operations working together
# test/dummy/app/operations/orders/process_order_pipeline.rb
module Orders
  ProcessOrderPipeline = TypedOperation::Pipeline.build do
    step :validate, ValidateOrderParams
    step :check_stock, CheckProductStock, if: ->(ctx) { !ctx[:skip_stock_check] }
    transform do |ctx|
      ctx.merge(total: ctx[:product].price * ctx[:quantity])
    end
    step :create_order, CreateOrderRecord
  end
end

Step 1: ValidateOrderParams

Validates input and fetches associated records:

# test/dummy/app/operations/orders/validate_order_params.rb
module Orders
  class ValidateOrderParams < ApplicationOperation
    param :user_id, Integer
    param :product_id, Integer
    param :quantity, Integer
    param :skip_stock_check, _Boolean, default: false

    def perform
      user = User.find_by(id: user_id)
      return Failure(error: :user_not_found) unless user

      product = Product.find_by(id: product_id)
      return Failure(error: :product_not_found) unless product

      return Failure(error: :invalid_quantity) unless quantity > 0

      Success(user:, product:, quantity:, skip_stock_check:)
    end
  end
end

Step 2: CheckProductStock

Verifies sufficient inventory (conditionally skipped):

# test/dummy/app/operations/orders/check_product_stock.rb
module Orders
  class CheckProductStock < ApplicationOperation
    param :product, Product
    param :quantity, Integer
    param :user, User
    param :skip_stock_check, _Boolean, default: false

    def perform
      if product.stock_quantity >= quantity
        Success(user:, product:, quantity:, skip_stock_check:)
      else
        Failure(error: :insufficient_stock, available: product.stock_quantity)
      end
    end
  end
end

Step 3: CreateOrderRecord

Creates the order record and reserves stock:

# test/dummy/app/operations/orders/create_order_record.rb
module Orders
  class CreateOrderRecord < ApplicationOperation
    param :user, User
    param :product, Product
    param :quantity, Integer
    param :total, Numeric, default: nil
    param :skip_stock_check, _Boolean, default: false

    def perform
      calculated_total = total || (product.price * quantity)

      order = Order.new(
        user:,
        product:,
        quantity:,
        total: calculated_total,
        status: "pending"
      )

      if order.save
        product.reserve_stock!(quantity)
        Success(user:, product:, quantity:, order:)
      else
        Failure(error: :order_creation_failed, messages: order.errors.full_messages)
      end
    end
  end
end

Using the Pipeline

# Process an order
result = Orders::ProcessOrderPipeline.call(
  user_id: 1,
  product_id: 42,
  quantity: 2
)

case result
in Success[order:]
  puts "Order created: #{order.id}"
in Failure[error: :insufficient_stock, available:]
  puts "Only #{available} items in stock"
in Failure[error:]
  puts "Failed: #{error}"
end

# Skip stock check (e.g., for backorders)
result = Orders::ProcessOrderPipeline.call(
  user_id: 1,
  product_id: 42,
  quantity: 100,
  skip_stock_check: true
)