aasm

package module
v0.0.0-...-cec0976 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 2 Imported by: 0

README

go-ruby-aasm/aasm

aasm — go-ruby-aasm

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's aasm gemActs As State Machine. It reproduces the state-machine definition (states with initial/final flags and enter/exit callbacks; events with from → to transitions, guards, and before/after/success/error/after_commit callbacks) and the runtime that fires an event against a host object — current state, the may_fire?/fire/fire! trio, permitted events, whiny transitions, and the faithful AASM callback ordering — without any Ruby runtime.

It is the AASM engine for go-embedded-ruby, but a standalone, reusable module.

What it is — and isn't. Everything AASM does to decide and sequence a transition is deterministic and needs no interpreter, so it lives here as pure Go: selecting a transition by from set and guards, running the callback chain in AASM's documented order, changing state, and — for fire! — persisting. The three couplings to a host object are seams: reading and writing the current state (GetState/SetState), persistence (Persist, used by fire! only), and the guards/callbacks themselves (func([]any) (bool, error) / func([]any) (any, error)). A future rbgo binding wires those seams to Ruby methods and blocks; the engine calls no Ruby of its own.

Features

Faithful port of the aasm gem's engine:

  • DefinitionNew(name) then a fluent State(…) / Event(…) DSL mirroring aasm do … end. States carry Initial(), Final(), Enter(cb), Exit(cb); events carry Before, After, Success, Error, AfterCommit, and one or more Transitions(Transition{From, To, Guards, Unless, Before, After, Success}). States named only by a transition are auto-registered.
  • InstanceBind(Seams) couples a definition to a host object. CurrentState (defaults to the initial state until set), Is(state), MayFire(event) (guards evaluated), Fire(event, args…) (in-memory) and FireBang(event, args…) (the gem's event!, persists), States, Events, PermittedEvents.
  • Callback ordering — a successful fire runs: guards → event before → transition before → old state exitstate change → new state enter → transition after → event after(fire! only) Persist → transition success → event success(fire! only) after_commit.
  • GuardsGuards (all must pass) and Unless (all must fail) select the transition. A false guard blocks (not an error); a guard error aborts the fire.
  • Whiny transitions — on by default: a blocked/undefined fire returns ErrInvalidTransition / ErrUndefinedEvent; WhinyTransitions(false) makes it return (false, nil) instead.
  • Error routing — a callback error is routed to the event's Error callbacks, which may swallow it (return nil) or re-raise; with none defined it propagates.
  • Multiple named machines — several independent Machines bind to distinct state fields on the same object (the gem's aasm(:name) do … end).

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Install

go get github.com/go-ruby-aasm/aasm

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-aasm/aasm"
)

// A host object owns its own state field; the engine reaches it through seams.
type job struct{ state string }

func main() {
	j := &job{}

	m := aasm.New("").
		State("sleeping", aasm.Initial()).
		State("running").
		State("cleaning").
		Event("run", aasm.Transitions(aasm.Transition{
			From: []string{"sleeping"}, To: "running",
		})).
		Event("clean", aasm.Transitions(aasm.Transition{
			From: []string{"running"}, To: "cleaning",
		}))

	inst := m.Bind(aasm.Seams{
		GetState: func() string { return j.state },
		SetState: func(s string) error { j.state = s; return nil },
		Persist:  func() error { /* db.Save(j) */ return nil },
	})

	fmt.Println(inst.CurrentState()) // sleeping (initial, before any set)

	ok, _ := inst.MayFire("run") // true
	_, _ = inst.Fire("run")      // sleeping -> running (in-memory)
	_, _ = inst.FireBang("clean") // running -> cleaning, then Persist

	fmt.Println(inst.CurrentState(), ok) // cleaning true
	fmt.Println(inst.PermittedEvents())  // events fireable from "cleaning"
}
Guards, callbacks and error routing
m := aasm.New("").
	State("pending", aasm.Initial()).
	State("active", aasm.Enter(func(a []any) (any, error) { /* on enter */ return nil, nil })).
	Event("activate",
		aasm.Before(func(a []any) (any, error) { return nil, nil }),
		aasm.Success(func(a []any) (any, error) { return nil, nil }),
		aasm.Error(func(err error, a []any) error { return err }), // re-raise (or return nil to swallow)
		aasm.Transitions(aasm.Transition{
			From:   []string{"pending"},
			To:     "active",
			Guards: []aasm.Guard{func(a []any) (bool, error) { return true, nil }},
		}),
	)

Value model

gem this package
aasm do … end New(name) + .State(…) / .Event(…)
state :x, initial: true State("x", Initial())
event :go do transitions from:, to: end Event("go", Transitions(Transition{From:, To:}))
before/after/success/error/after_commit Before/After/Success/Error/AfterCommit
before_enter/enter · before_exit/exit Enter(cb) · Exit(cb)
transitions … guard:/if:/unless: Transition{Guards: …, Unless: …}
obj.go inst.Fire("go", args…)
obj.go! inst.FireBang("go", args…)
obj.may_go? inst.MayFire("go", args…)
aasm.current_state inst.CurrentState()
aasm.states / aasm.events inst.States() / inst.Events()
aasm.permitted_events inst.PermittedEvents()
aasm_read_state / aasm_write_state Seams.GetState / Seams.SetState
persisting event! save Seams.Persist (host seam; fire! only)
AASM::InvalidTransition / undefined event ErrInvalidTransition / ErrUndefinedEvent

Tests & coverage

The suite is deterministic and self-contained: an in-memory host drives the state/persist seams, and stub guards/callbacks drive every branch — guard-blocked and invalid transitions, callback error propagation and swallowing, the persist and set-state seam errors, whiny vs non-whiny, multiple named machines, and every stage of the callback chain. Coverage holds at 100% on every OS and arch lane.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-aasm/aasm authors.

Documentation

Overview

Package aasm is a pure-Go (CGO-free) reimplementation of the deterministic core of Ruby's aasm gem — "Acts As State Machine". It reproduces the state machine DEFINITION (states with initial/final flags and enter/exit callbacks; events with from→to transitions, guards, and before/after/success/error/ after_commit callbacks) and the runtime that fires an event against a host object — the current state, the may_fire?/fire/fire! trio, permitted events, whiny transitions, and the faithful AASM callback ordering — without any Ruby runtime.

It is the AASM engine for [go-embedded-ruby](https://github.com/go-embedded-ruby/ruby), but a standalone, reusable module.

Definition and instance

A Machine is a state-machine definition, built with a small fluent DSL that mirrors the gem's `aasm do … end` block:

m := aasm.New("").
	State("sleeping", aasm.Initial()).
	State("running").
	Event("run", aasm.Transitions(aasm.Transition{
		From: []string{"sleeping"}, To: "running",
	}))

A Machine carries no per-object state. It is bound to a host object through Seams to yield an Instance:

inst := m.Bind(aasm.Seams{
	GetState: func() string { return obj.state },
	SetState: func(s string) error { obj.state = s; return nil },
	Persist:  func() error { return db.Save(obj) },
})

inst.CurrentState()          // "sleeping" (the initial state until set)
ok, err := inst.MayFire("run")
ok, err = inst.Fire("run")   // in-memory transition
ok, err = inst.FireBang("run") // transition + Persist (the gem's `run!`)

Seams

The three host couplings are seams so the same engine drives a plain struct, an ORM row, or a Ruby object:

  • GetState / SetState read and write the current state on the host object (the gem's aasm_read_state / aasm_write_state).
  • Persist is invoked by FireBang only, after the in-memory transition, to persist the object (the gem's `event!` save). Fire never persists.

Guards and callbacks are themselves seams — Guard is func([]any) (bool, error) and Callback is func([]any) (any, error) — that a binding wires to Ruby methods or blocks; the `any` return mirrors a Ruby method result and is ignored by the engine.

Callback ordering

A successful fire runs, in order: the transition guards (to select the transition), the event `before`, the transition `before`, the old state's `exit`, the state change, the new state's `enter`, the transition `after`, the event `after`, then — for FireBang — Persist, then the transition and event `success`, and finally — for FireBang — `after_commit`. Any callback error stops the sequence and is routed to the event's `error` callbacks (which may swallow or re-raise); if none are defined it propagates. A guard that returns false is not an error: it blocks the transition, which raises ErrInvalidTransition when whiny (the default) or returns (false, nil) when Machine.WhinyTransitions is false.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUndefinedEvent is returned (when whiny) by Fire / FireBang for an event
	// the machine does not define — the gem raises AASM::UndefinedState /
	// NoMethodError for `obj.no_such_event!`.
	ErrUndefinedEvent = errors.New("aasm: undefined event")

	// ErrInvalidTransition is returned (when whiny) when the event is defined but
	// no transition is available from the current state — either no transition
	// lists the state in its `from` set, or every candidate's guards blocked it.
	// It mirrors the gem's AASM::InvalidTransition.
	ErrInvalidTransition = errors.New("aasm: invalid transition")
)

The gem's transition errors, exposed as sentinels so a host (rbgo) can map them onto the Ruby AASM exception tree and callers can match with errors.Is.

Functions

This section is empty.

Types

type Callback

type Callback func(args []any) (any, error)

Callback is a state/event callback seam. It receives the arguments passed to Fire/FireBang and returns a value (mirroring a Ruby method result, ignored by the engine) and an error. A non-nil error stops the transition.

type ErrorCallback

type ErrorCallback func(err error, args []any) error

ErrorCallback is the event `error` seam. It receives the error raised during a transition and may return nil to swallow it or an error to propagate.

type EventOption

type EventOption func(*eventDef)

EventOption configures an event in Machine.Event.

func After

func After(cb Callback) EventOption

After appends an event-level after callback.

func AfterCommit

func AfterCommit(cb Callback) EventOption

AfterCommit appends an event-level after_commit callback, run by FireBang only after Persist and the success callbacks.

func Before

func Before(cb Callback) EventOption

Before appends an event-level before callback.

func Error

func Error(cb ErrorCallback) EventOption

Error appends an event-level error callback.

func Success

func Success(cb Callback) EventOption

Success appends an event-level success callback (run after the transition and, for FireBang, after Persist).

func Transitions

func Transitions(t Transition) EventOption

Transitions appends a transition to the event (the gem's `transitions from:, to:, guard:, …` inside an `event` block).

type Guard

type Guard func(args []any) (bool, error)

Guard is a transition guard seam: it decides whether a transition may be taken. A false result blocks the transition (not an error); a non-nil error aborts the fire and is routed to the event's error callbacks.

type Instance

type Instance struct {
	// contains filtered or unexported fields
}

Instance is a Machine bound to a host object through Seams.

func (*Instance) CurrentState

func (i *Instance) CurrentState() string

CurrentState reports the host's current state, defaulting to the machine's initial state while the host state is blank (the gem's `aasm.current_state`).

func (*Instance) Events

func (i *Instance) Events() []string

Events returns the machine's event names in declaration order.

func (*Instance) Fire

func (i *Instance) Fire(name string, args ...any) (bool, error)

Fire runs the event as an in-memory transition (no Persist), returning whether it transitioned. See the package doc for callback ordering.

func (*Instance) FireBang

func (i *Instance) FireBang(name string, args ...any) (bool, error)

FireBang runs the event and, on success, invokes Persist (the gem's `event!`).

func (*Instance) Is

func (i *Instance) Is(state string) bool

Is reports whether the current state equals state (the gem's `obj.x?`).

func (*Instance) Machine

func (i *Instance) Machine() *Machine

Machine returns the definition this instance is bound to.

func (*Instance) MayFire

func (i *Instance) MayFire(name string, args ...any) (bool, error)

MayFire reports whether event could fire from the current state — the event is defined and some transition's from set matches with guards passing (the gem's `may_event?`). A guard error is surfaced.

func (*Instance) PermittedEvents

func (i *Instance) PermittedEvents(args ...any) []string

PermittedEvents returns the names of events that may fire from the current state, in declaration order (the gem's `aasm.permitted_events`). An event whose guard evaluation errors is treated as not permitted.

func (*Instance) States

func (i *Instance) States() []string

States returns the machine's state names in declaration order.

type Machine

type Machine struct {
	// contains filtered or unexported fields
}

Machine is a state-machine definition — the pure-Go analogue of one `aasm do … end` block. It holds states and events but no per-object state; bind it to a host with Machine.Bind. A class with several `aasm(:name) do … end` blocks maps to several independent Machines.

func New

func New(name string) *Machine

New returns an empty machine with the given name (use "" for the gem's default unnamed machine). Whiny transitions are on by default, matching the gem.

func (*Machine) Bind

func (m *Machine) Bind(s Seams) *Instance

Bind binds the machine to a host object and returns a runnable instance.

func (*Machine) Event

func (m *Machine) Event(name string, opts ...EventOption) *Machine

Event declares (or extends) an event and returns the machine for chaining. States named by the event's transitions are auto-registered if not already declared.

func (*Machine) Events

func (m *Machine) Events() []string

Events returns the declared event names in declaration order.

func (*Machine) InitialState

func (m *Machine) InitialState() string

InitialState reports the state name flagged initial, or "" if none is.

func (*Machine) Name

func (m *Machine) Name() string

Name reports the machine's name.

func (*Machine) State

func (m *Machine) State(name string, opts ...StateOption) *Machine

State declares (or extends) a state and returns the machine for chaining.

func (*Machine) States

func (m *Machine) States() []string

States returns the declared state names in declaration order.

func (*Machine) WhinyTransitions

func (m *Machine) WhinyTransitions(v bool) *Machine

WhinyTransitions toggles whether a blocked/invalid Fire returns an error (true, the default) or (false, nil). It returns the machine for chaining.

type Seams

type Seams struct {
	GetState func() string
	SetState func(state string) error
	Persist  func() error
}

Seams couple a Machine to a host object. GetState/SetState read and write the current state (the gem's aasm_read_state / aasm_write_state); Persist, optional, is invoked by FireBang only, after the in-memory transition.

type StateOption

type StateOption func(*stateDef)

StateOption configures a state in Machine.State.

func Enter

func Enter(cb Callback) StateOption

Enter appends an enter callback, run when the machine enters the state.

func Exit

func Exit(cb Callback) StateOption

Exit appends an exit callback, run when the machine leaves the state.

func Final

func Final() StateOption

Final flags the state as a final state (the gem's `state :x, final: true`).

func Initial

func Initial() StateOption

Initial flags the state as the machine's initial state (the gem's `state :x, initial: true`).

type Transition

type Transition struct {
	From    []string
	To      string
	Guards  []Guard
	Unless  []Guard
	Before  []Callback
	After   []Callback
	Success []Callback
}

Transition is the specification of one from→to transition passed to Transitions. Guards must all pass and Unless guards must all fail for the transition to be selected.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL