EasyOp Docs Getting Started

Getting Started

Installation

Add EasyOp to your Gemfile:

gem "easyop"

Then run:

bundle install

EasyOp has no required runtime dependencies — it works in plain Ruby, Sinatra, or Rails.

Your First Operation

An operation is any Ruby class that includes Easyop::Operation and defines a call method.

class DoubleNumber
  include Easyop::Operation

  def call
    ctx.fail!(error: "input must be a number") unless ctx.number.is_a?(Numeric)
    ctx.result = ctx.number * 2
  end
end

Call it and inspect the result:

result = DoubleNumber.call(number: 21)
result.success?   # => true
result.result     # => 42

result = DoubleNumber.call(number: "oops")
result.failure?   # => true
result.error      # => "input must be a number"

Every operation:

  • Receives input via ctx — the shared data bag
  • Writes output back to ctx
  • Calls ctx.fail! to halt and mark failure
  • Returns ctx — the result object

.call vs .call!

EasyOp provides two entry points:

MethodOn successOn ctx.fail!On unhandled exception
.call(attrs) Returns ctx Returns ctx (ctx.failure? == true) Re-raises after setting ctx.error
.call!(attrs) Returns ctx Raises Easyop::Ctx::Failure Re-raises (unhandled)
# Safe variant — check ctx.failure? afterwards
result = DoubleNumber.call(number: "bad")
result.failure?  # => true
result.error     # => "input must be a number"

# Bang variant — rescue the exception
begin
  result = DoubleNumber.call!(number: "bad")
rescue Easyop::Ctx::Failure => e
  e.ctx.error   # => "input must be a number"
  e.message     # => "Operation failed: input must be a number"
end
Tip Use .call in controllers where you want to handle failure inline. Use .call! in service objects or pipelines where the caller is responsible for error handling.

Configuration

Configure EasyOp once, typically in an initializer:

# config/initializers/easyop.rb
Easyop.configure do |c|
  # Schema type validation mode:
  #   false (default) — emit a warning on type mismatch, continue
  #   true            — call ctx.fail! on type mismatch
  c.strict_types = false

  # Type adapter for schema validation:
  #   :native       (default) — Ruby is_a? checks
  #   :none         — skip type checking entirely
  c.type_adapter = :native
end

Resetting Configuration

In tests, reset configuration before each example to avoid state leakage:

# spec/spec_helper.rb
RSpec.configure do |config|
  config.before(:each) do
    Easyop.reset_config!
  end
end

Easyop.reset_config! replaces the current config with a fresh Configuration instance using all defaults.

Adding Hooks

Use before, after, and around to add lifecycle callbacks:

class NormalizeEmail
  include Easyop::Operation

  before :strip_whitespace
  after  :log_result
  around :with_timing

  def call
    ctx.normalized = ctx.email.downcase
  end

  private

  def strip_whitespace
    ctx.email = ctx.email.to_s.strip
  end

  def log_result
    Rails.logger.info "Normalized: #{ctx.normalized}" if ctx.success?
  end

  def with_timing
    t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    yield
    ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
    Rails.logger.info "NormalizeEmail took #{ms}ms"
  end
end

result = NormalizeEmail.call(email: "  ALICE@EXAMPLE.COM  ")
result.normalized  # => "alice@example.com"

Typed Schema

Declare expected inputs and outputs with the params / result DSL:

class RegisterUser
  include Easyop::Operation

  params do
    required :email, String
    required :age,   Integer
    optional :plan,  String,   default: "free"
    optional :admin, :boolean, default: false
  end

  result do
    required :user, User
  end

  def call
    ctx.user = User.create!(ctx.slice(:email, :age, :plan))
  end
end

result = RegisterUser.call(email: "alice@example.com", age: 30)
result.success?  # => true
result.plan      # => "free"  (default applied)

result = RegisterUser.call(email: "bob@example.com")
result.failure?  # => true
result.error     # => "Missing required params field: age"

Next Steps

EasyOp v0.1.0 · MIT License · GitHub