Contracts
Contracts let you declare and introspect an operation's interface – what it accepts, what it returns, and which errors it can raise.
Declaring a success type
Use success to declare the expected return type. The return value of perform is validated at runtime:
class FindUser < Dex::Operation
prop :user_id, Integer
success _Ref(User)
def perform
user = User.find_by(id: user_id)
error!(:not_found) unless user
user
end
end
FindUser.call(user_id: 1) # => User instance (validated)Returning a mismatched type raises ArgumentError immediately. Returning nil is always allowed (even with a success type declared).
Declaring error codes
Use error to declare which error codes the operation may raise:
class CreateUser < Dex::Operation
prop :email, String
error :email_taken, :invalid_email
def perform
error!(:email_taken) if User.exists?(email: email)
User.create!(email: email)
end
endWhen error codes are declared, calling error! with an undeclared code raises ArgumentError – a programming mistake caught at runtime. See Error Handling for details.
Introspecting with .contract
Every operation exposes a .contract class method that returns a frozen Contract data object:
CreateUser.contract
# => #<data Dex::Operation::Contract
# params={email: String},
# success=nil,
# errors=[:email_taken, :invalid_email]>The contract has three fields:
| Field | Type | Description |
|---|---|---|
params | Hash{Symbol => Type} | Declared properties and their types |
success | Type or nil | Declared success type |
errors | Array<Symbol> | Declared error codes |
Pattern matching on contracts
Contract is a Data.define, so it supports pattern matching and to_h:
CreateUser.contract => { params:, success:, errors: }
params # => { email: String }
success # => nil
errors # => [:email_taken, :invalid_email]
CreateUser.contract.to_h
# => { params: { email: String }, success: nil, errors: [:email_taken, :invalid_email] }Inheritance
Contracts inherit from parent classes. A child class's declared errors are merged with the parent's:
class BaseOperation < Dex::Operation
error :unauthorized
end
class CreateUser < BaseOperation
error :email_taken
def perform
error!(:unauthorized) # works – inherited from parent
error!(:email_taken) # works – declared on this class
end
end
CreateUser.contract.errors # => [:unauthorized, :email_taken]Success types also inherit – a child class can override the parent's success type.
Use cases
Contracts are useful for:
- Documentation – describe intent at the class level, not just in comments
- Testing – assert the contract without calling the operation (see Testing)
- Tooling – build admin panels, API docs, or monitoring dashboards from contract data
- Catching mistakes – typos in error codes and wrong return types are caught at runtime