contracts-rb
contracts-rb adds small, explicit behavioral contracts to Ruby without imposing a type system. It supports parameter and return constraints, preconditions, postconditions, object invariants, state snapshots, mutation policies, exception declarations, introspection, and a dependency-free CLI.
Official repository: github.com/magnexis/contracts-rb
RubyGems: rubygems.org/gems/contracts-rb
Why contracts-rb?
Ruby tests describe expected examples; contracts keep critical method promises executable at runtime. Use them for domain boundaries, money movement, state transitions, service objects, and public library APIs. They are not a replacement for ordinary tests, schema validation, or authorization.
Install
gem "contracts-rb", "~> 0.1"
require "contracts"
Five-minute example
class Account
include Contracts
attr_reader :balance
invariant("balance is non-negative") { balance >= 0 }
contract :withdraw do
params amount: Numeric
requires("amount is positive") { |amount:| amount.positive? }
requires("enough funds") { |amount:| amount <= balance }
changes :balance
returns Numeric
ensures { |result, before:| balance == before.balance - result }
end
def initialize(balance:) = @balance = balance
def withdraw(amount:) = (@balance -= amount)
end
Constraints include Contracts.nilable, any, matching, range, one_of, array_of, hash_of, respond_to, duck_type, anything, nothing, and predicate.
Stateful contracts
Use observe to capture relevant receiver fields; changes permits a subset, must_change requires a transition, and pure/changes_nothing permits none. Snapshots are immutable (before.balance, before[:balance], before.metadata) and deep: true protects nested arrays and hashes. Invariants run after initialization and around contracted methods by default. Contracts.check_invariants!(object) is available for explicit checks.
Constraint reference
| Factory | Example | Meaning |
|---|---|---|
nilable |
Contracts.nilable(String) |
String or nil |
any |
Contracts.any(String, Symbol) |
any supplied constraint |
matching |
Contracts.matching(/\A[a-z]+\z/i) |
string matching a regex |
range |
Contracts.range(18..120) |
value inside a range |
one_of |
Contracts.one_of(:draft, :published) |
one literal value |
array_of |
Contracts.array_of(String) |
array whose elements match |
hash_of |
Contracts.hash_of(Symbol, Numeric) |
hash with matching keys/values |
predicate |
Contracts.predicate("valid ID") { ... } |
custom runtime rule |
Runtime configuration
Contracts.configure do |config|
config.enabled = true
config.failure_mode = :raise # :raise, :warn, :log, :collect
config.sample_rate = 1.0
config.invariant_checking = :contracted_methods
config.snapshot_strategy = :declared
config.undeclared_exceptions = :ignore
end
For production, use sampling and a :log or :collect failure mode where an exception would be too disruptive. Sensitive parameter names are redacted from errors by default.
Exceptions and inheritance
Declare exceptions with raises PaymentError; use on_raise(PaymentError) { |error, before:| ... } for rollback guarantees. Set undeclared_exceptions to :warn or :violate to enforce exception declarations. Inheritance mode defaults to :merge, applying parent parameter, precondition, postcondition, return, and observation rules to overriding child contracts. :strict rejects structural parameter constraint changes.
Configuration and production use
Contracts.configure do |config|
config.enabled = true
config.failure_mode = :log # :raise, :warn, :log, :collect
config.sample_rate = 0.05
config.undeclared_exceptions = :violate # :ignore, :warn, :violate
end
Errors avoid parameter values by default. Set include_values_in_errors only in trusted environments. Contracts complement tests and domain validation; use them for durable API promises and high-value invariants, not every local assertion.
Tooling
bundle exec contracts version, inspect, validate, doctor, and docs --format markdown|json --output PATH are available. Load application code with --require ./lib/my_app.
bundle exec contracts inspect BankAccount#withdraw --require ./examples/banking/bank_account
bundle exec contracts docs --require ./examples/banking/bank_account --format markdown --output docs/contracts
bundle exec contracts doctor --require ./lib/my_app
require "contracts/rspec" provides basic RSpec matchers when RSpec is installed. require "contracts/rails" installs a lightweight optional Railtie.
Compatibility
Ruby 3.1+. Rails is optional. The core is plain Ruby and has no runtime dependencies.
Limitations
0.1.0 wraps declared instance methods directly; its explicit singleton API is experimental. Deep snapshots, static implication proofs for inheritance, and Rails instrumentation are intentionally deferred.
Development
bundle install
bundle exec rake spec
gem build contracts-rb.gemspec
MIT licensed. See CONTRIBUTING.md and SECURITY.md.
Release process
Maintainers validate with bundle exec rubocop ., bundle exec rspec, and bundle exec rake build. The resulting contracts-rb-<version>.gem is attached to the matching GitHub release and published through an authenticated RubyGems session.
Benchmark methodology
Run bundle exec ruby benchmark/runtime_overhead.rb on the deployment Ruby and hardware. Compare the included uncontracted and parameter/return-contracted methods; do not extrapolate those measurements to contracts that capture state or execute expensive predicates.