EasyOp Docs Ctx API

Ctx API Reference

Easyop::Ctx is the shared data bag that flows through every operation. It is passed in from the caller, written to by the operation, and returned as the result. It replaces OpenStruct with a plain-Hash backend for speed and clarity.

Quick Reference

MethodDescription
ctx[key] / ctx.keyRead attribute (nil for missing keys)
ctx[key] = v / ctx.key = vWrite attribute
ctx.key?Predicate: !!ctx[:key] (never raises)
ctx.key?(sym)Explicit existence check (true/false)
ctx.merge!(hash)Bulk-set attributes; returns self
ctx.to_hReturns a plain Hash copy of all attributes
ctx.slice(:a, :b)Returns plain Hash with only those keys
ctx.fail!(attrs = {})Merge attrs, mark failed, raise Ctx::Failure
ctx.success? / ctx.ok?True unless fail! was called
ctx.failure? / ctx.failed?True after fail!
ctx.error / ctx.error=Shortcut for ctx[:error]
ctx.errors / ctx.errors=Shortcut for ctx[:errors] — default {}
ctx.on_success { |c| }Yield and return self if successful
ctx.on_failure { |c| }Yield and return self if failed
ctx.called!(instance)Register for rollback (called by Flow)
ctx.rollback!Roll back registered instances in reverse; idempotent
ctx.deconstruct_keys(keys)Pattern matching support
ctx.inspectHuman-readable string

Construction

# Normally created by .call / .call! — you don't instantiate manually
ctx = Easyop::Ctx.new(email: "alice@example.com", age: 30)

# Ctx.build is used by Operation internals:
# - Wraps a plain Hash in a new Ctx
# - Returns an existing Ctx unchanged
ctx2 = Easyop::Ctx.build(email: "bob@example.com")  # => new Ctx
ctx3 = Easyop::Ctx.build(ctx2)                       # => ctx2 unchanged

Reading Attributes

ctx = Easyop::Ctx.new(email: "alice@example.com", admin: false)

# Hash-style
ctx[:email]       # => "alice@example.com"
ctx[:missing]     # => nil (never raises)

# Method-style (via method_missing)
ctx.email         # => "alice@example.com"

# Predicate: !!ctx[:key]
ctx.admin?        # => false  (falsey but set)
ctx.missing?      # => false  (not set)

# Existence check: was this key explicitly set?
ctx.key?(:email)  # => true
ctx.key?(:admin)  # => true   (set to false, but present)
ctx.key?(:other)  # => false  (never set)

# Plain hash copy
ctx.to_h          # => { email: "alice@example.com", admin: false }
Note: key? vs predicate ctx.admin? returns !!ctx[:admin] — it tests truthiness, not existence. A key set to false or nil will return false from the predicate. Use ctx.key?(:admin) when you need to know whether the key was explicitly set.

Writing Attributes

# Single key
ctx[:user]  = user_object
ctx.user    = user_object     # same thing

# Bulk set
ctx.merge!(user: user, token: "abc123", plan: "pro")
# Returns self — chainable

Extracting a Subset

ctx = Easyop::Ctx.new(name: "Alice", email: "alice@example.com", password: "secret", role: "admin")

ctx.slice(:name, :email)
# => { name: "Alice", email: "alice@example.com" }
# Missing keys are silently omitted — no KeyError

ctx.slice(:name, :nonexistent)
# => { name: "Alice" }

Status Methods

ctx = Easyop::Ctx.new

# Before any fail!
ctx.success?   # => true
ctx.ok?        # => true   (alias for success?)
ctx.failure?   # => false
ctx.failed?    # => false  (alias for failure?)

# After fail!
ctx.fail! rescue nil
ctx.success?   # => false
ctx.failure?   # => true

fail!

# Mark failed with no message
ctx.fail!
# => raises Easyop::Ctx::Failure

# Merge attributes first, then fail
ctx.fail!(error: "Invalid credentials")
# => sets ctx.error, then raises

# Structured failure
ctx.fail!(
  error:  "Validation failed",
  errors: { email: "is blank", age: "must be positive" }
)
# => sets ctx.error and ctx.errors, then raises

# .call swallows Ctx::Failure — caller checks ctx.failure?
# .call! re-raises Ctx::Failure — caller rescues it

Error Helpers

# ctx.error / ctx.error= are shortcuts for ctx[:error] / ctx[:error]=
ctx.error          # => nil (before any fail!)
ctx.error = "Oops" # => sets ctx[:error]
ctx.error          # => "Oops"

# ctx.errors / ctx.errors= are shortcuts for ctx[:errors]
ctx.errors          # => {} (default when not set)
ctx.errors = { email: "is invalid", name: "is required" }
ctx.errors[:email]  # => "is invalid"

Chainable Callbacks

Post-call callbacks that fire based on success/failure. Both return self for chaining. Neither raises.

AuthenticateUser.call(email: email, password: password)
  .on_success { |ctx| sign_in(ctx.user) }
  .on_failure { |ctx| flash[:alert] = ctx.error }

# Chain multiple of each
ProcessOrder.call(attrs)
  .on_success { |ctx| Analytics.track("order", id: ctx.order.id) }
  .on_success { |ctx| OrderMailer.confirmation(ctx.order).deliver_later }
  .on_failure { |ctx| Rails.logger.error "Order failed: #{ctx.error}" }
  .on_failure { |ctx| render json: { error: ctx.error }, status: 422 }

Pattern Matching (Ruby 3+)

result = AuthenticateUser.call(email: email, password: password)

case result
in { success: true, user: User => user }
  sign_in(user)
  redirect_to dashboard_path
in { success: false, errors: Hash => errs }
  render :new, locals: { errors: errs }
in { success: false, error: String => msg }
  flash[:alert] = msg
  render :new
end

# deconstruct_keys returns: { success: bool, failure: bool, **all_attributes }
result.deconstruct_keys(nil)
# => { success: true, failure: false, user: #<User>, ... }

# With key filtering:
result.deconstruct_keys([:success, :user])
# => { success: true, user: #<User> }

Rollback Support

These methods are used internally by Easyop::Flow. You generally do not call them directly.

# Register an operation instance as having run successfully
ctx.called!(op_instance)   # returns self (chainable)

# Roll back all registered instances in reverse order
# Errors inside individual rollbacks are swallowed so all run
# Idempotent: second call has no effect
ctx.rollback!

# Example of what Flow does internally:
# After op_a.call and op_b.call succeed, ctx has both registered.
# If op_c fails: ctx.rollback! calls op_b.rollback then op_a.rollback

Ctx::Failure

Easyop::Ctx::Failure is a StandardError subclass raised by ctx.fail!. It carries the failed ctx.

begin
  AuthenticateUser.call!(email: email, password: "wrong")
rescue Easyop::Ctx::Failure => e
  e.ctx            # => the Easyop::Ctx instance
  e.ctx.failure?   # => true
  e.ctx.error      # => "Invalid credentials"

  # Message format:
  e.message        # => "Operation failed: Invalid credentials"
  # When ctx.error is nil:
  # => "Operation failed"
end

Inspect

ctx = Easyop::Ctx.new(name: "Alice", age: 30)
ctx.inspect
# => "#<Easyop::Ctx {:name=>\"Alice\", :age=>30} [ok]>"

ctx.fail! rescue nil
ctx.inspect
# => "#<Easyop::Ctx {:name=>\"Alice\", :age=>30} [FAILED]>"
EasyOp v0.1.0 · MIT License · GitHub