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
| Method | Description |
|---|---|
ctx[key] / ctx.key | Read attribute (nil for missing keys) |
ctx[key] = v / ctx.key = v | Write 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_h | Returns 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.inspect | Human-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]>"