View Source Bond.Server (Bond v1.8.0)
Design-by-Contract for GenServer process state.
Bond's struct @invariant constrains every value of a type, but the state most worth
constraining in Elixir is the state that changes over time inside a process. Bond.Server
brings contracts to that state: declare module-wide properties of a server's state and Bond
checks them automatically around the server's state-transition callbacks.
defmodule Counter do
use GenServer
use Bond.Server
@state_invariant non_negative: state.count >= 0
@impl true
def init(n), do: {:ok, %{count: n}}
@impl true
def handle_call(:inc, _from, state), do: {:reply, :ok, %{state | count: state.count + 1}}
endA @state_invariant is checked after every state-transition callback returns a new state —
init/1, handle_call/3, handle_cast/2, handle_info/2, handle_continue/2, and
code_change/3 — and a violation raises Bond.InvariantError (with :kind
:state_invariant). Unlike a struct
@invariant, this fires even when a callback mutates state inline (the common case), because
Bond wraps the callbacks themselves rather than relying on the state flowing through a
contracted pure function.
Usage order
use GenServermust come beforeuse Bond.Server. Bond.Server detects the callbacks you define via the@on_definitioncompiler hook, which only sees definitions made after it is installed;use GenServerprovides default callback implementations during its own expansion, so putting it first keeps those defaults out of Bond.Server's view.
Transition invariants
A @transition_invariant relates the prior state to the next state across a transition,
via the implicit old_state and new_state bindings:
@transition_invariant monotonic: new_state.count >= old_state.countIt is checked across every transition callback — handle_call/3, handle_cast/2,
handle_info/2, handle_continue/2 — and a violation raises Bond.InvariantError (with
:kind :transition_invariant). A transition invariant is what the Design-by-Contract
literature calls a history constraint (Liskov & Wing).
init/1 and code_change/3 are treated as re-creations: they establish a new state (checked by
@state_invariant) but have no comparable prior state, so transition invariants do not apply to
them.
Like Bond's other contracts, @state_invariant and @transition_invariant checks honour the
:invariants configuration and runtime gate: both share the :invariants kind, so they observe
the precondition ≤ postcondition ≤ invariant chain, are compiled out entirely under
invariants: :purge, and can be toggled at runtime with Bond.Config.enable/1/disable/1. See
Bond and Bond.Config.