EasyOp Docs Overview

EasyOp โ€” Joyful, composable business logic for Ruby

Wrap every piece of business logic in a small, testable operation object. Compose operations into flows. Never entangle controllers or models with complex logic again.

Quick Example

An operation is any Ruby class that includes Easyop::Operation and defines a call method. The shared context ctx is the input carrier and result object.

class AuthenticateUser
  include Easyop::Operation

  def call
    user = User.authenticate(ctx.email, ctx.password)
    ctx.fail!(error: "Invalid credentials") unless user
    ctx.user = user
  end
end

result = AuthenticateUser.call(email: "alice@example.com", password: "hunter2")
result.success?   # => true
result.user       # => #<User id=1>

result = AuthenticateUser.call(email: "bob@example.com", password: "wrong")
result.failure?   # => true
result.error      # => "Invalid credentials"

Features

โš™๏ธ

Operation

Pure Ruby objects with a call method, typed schemas, hooks, and error handling. No framework required.

๐Ÿ”—

Flow

Compose operations in sequence sharing one context. Automatic rollback on failure. Conditional skip_if steps.

🔧

Hooks

before, after, and around hooks with inheritance. Works without ActiveSupport.

๐Ÿ“‹

Schema

Optional typed params / result declarations. Catches missing or wrong-type inputs before call runs.

๐Ÿ”Œ

Plugins

Instrumentation, Recording, Async, Transactional โ€” opt-in, inheritable, composable.

๐Ÿงช

Testing

Operations are plain Ruby objects. Call them directly in specs โ€” no stubs, no HTTP, no framework needed.

How It Works

Every operation receives a ctx โ€” a Hash-backed object with method-style attribute access. Your code reads inputs from ctx and writes outputs back to it. When something goes wrong, call ctx.fail! to halt and mark the operation as failed.

The caller receives ctx as the result and inspects ctx.success? or ctx.failure?. No exceptions are raised unless you use the bang variant .call!.

# .call  โ€” returns ctx, never raises on ctx.fail!
result = MyOp.call(input: "data")
result.success?   # => true or false
result.some_attr  # => whatever the op set

# .call! โ€” returns ctx on success, raises Easyop::Ctx::Failure on fail!
begin
  result = MyOp.call!(input: "data")
rescue Easyop::Ctx::Failure => e
  e.ctx.error   # => the error message
  e.message     # => "Operation failed: the error message"
end

Flow: Composing Operations

A Flow runs operations in sequence, sharing one ctx. Any failure halts the chain and triggers rollback of completed steps.

class ProcessCheckout
  include Easyop::Flow

  flow ValidateCart,
       ApplyCoupon,      # declared skip_if โ€” skipped when no coupon_code
       ChargePayment,    # defines rollback โ€” refunds on failure
       CreateOrder,
       SendConfirmation
end

result = ProcessCheckout.call(user: current_user, cart: current_cart)
result.success?  # => true
result.order     # => #<Order id=42>

Module Map

ModuleDescription
Easyop::OperationCore mixin โ€” include in any class
Easyop::FlowSequential operation chain with rollback
Easyop::FlowBuilderBuilder returned by FlowClass.prepare
Easyop::CtxShared context + result object
Easyop::Ctx::FailureException raised by ctx.fail!
Easyop::Hooksbefore/after/around hook system
Easyop::Rescuablerescue_from DSL
Easyop::Skipskip_if for conditional flow steps
Easyop::SchemaTyped params/result DSL
Easyop::Plugins::InstrumentationActiveSupport::Notifications events
Easyop::Plugins::RecordingPersists executions to ActiveRecord
Easyop::Plugins::AsyncBackground jobs via ActiveJob
Easyop::Plugins::TransactionalDB transaction wrapper
EasyOp v0.1.0 · MIT License · GitHub