Skip to content

v0.6.0

Choose a tag to compare

@supostat supostat released this 26 Aug 17:35
· 12 commits to main since this release

Opt-in RSpec matchers over the whole introspection surface. One require
in your spec helper — require "statecraft/rspec" — includes seven
matchers into every example group; RSpec never becomes a runtime
dependency of the gem, and an isolated-process probe keeps it that way.

The question matchers consult the guards with the metadata your
production call will carry:

  • allow_event(:pay).with_metadata(...) over can_fire?
  • refuse_event(:cancel).because_of(:guard) — the refusal with its
    reason, named from refusals_for
  • allow_transition_to(:paid).via(:pay) / .directly over
    available_transitions
  • have_transitioned_to(:paid) — strictly log-based

The class-level pair answers the graph's shape, guards untouched:
have_edge(:pending, :cancelled).via(:cancel) and
have_initial_state(:pending).

The block matcher asserts the transition itself — the state move AND
the appended log row in one expression:

expect { order.fire!(:pay, metadata: { "amount" => 100 }) }
  .to transition(order).from(:pending).to(:paid)
      .via_event(:pay).with_metadata("amount" => 100)

A failing matcher explains itself from the same introspection the
pipeline consults: the current state, the reachable edges, the refusing
guard with its layer. Two honest limits, stated in the README: bang-form
exceptions fly through like with change, and because_of names
record-layer guards only — an input-reading guard: has no name there,
and the failure message says so instead of guessing.

The example app's suite now exercises the matchers in a real Rails
application on every CI run.