Skip to content

renaldid/machina

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

machina

Go Reference Go Version License: MIT Coverage

machina is a type-safe finite state machine for Go, built on generics.

It improves on looplab/fsm, the most popular Go FSM library, by replacing stringly-typed states and events with any comparable type and catching invalid usage at compile time instead of at runtime.

// looplab/fsm - states and events are plain strings, errors surface at runtime
fsm.NewFSM("idle", fsm.Events{
    {Name: "start", Src: []string{"idle"}, Dst: "running"},
}, ...)

// machina - states and events are typed, invalid usage is a compile error
machina.New[State, Event](Idle,
    machina.T(Idle, Start, Running),
)

Install

go get github.com/renaldid/machina

Zero dependencies.


Quick start

package main

import (
    "context"
    "fmt"
    "github.com/renaldid/machina"
)

type State int

const (
    Pending State = iota
    Active
    Closed
)

type Event int

const (
    Activate Event = iota
    Close
)

func main() {
    m := machina.New[State, Event](Pending,
        machina.T(Pending, Activate, Active),
        machina.T(Active,  Close,    Closed),
        machina.T(Pending, Close,    Closed),
    )

    ctx := context.Background()
    _ = m.Send(ctx, Activate)
    fmt.Println(m.State()) // Active

    _ = m.Send(ctx, Close)
    fmt.Println(m.State()) // Closed
}

API

Creating an FSM

m := machina.New[S, E](initialState, opts...)

S and E can be any comparable type: int, string, a custom type State int, etc.

Registering transitions

// T: unconditional transition
machina.T(from, event, to)

// TG: guarded transition, proceeds only when guard returns true
machina.TG(from, event, to, func(ctx context.Context) bool {
    return isAuthorized(ctx)
})

New panics if you register two transitions for the same (from, event) pair. This is always a programmer error and is caught at startup, not at runtime.

Querying state

m.State()     // current state
m.Can(event)  // true if event is valid from current state
m.States()    // all states with outgoing transitions (order not guaranteed)

Sending events

err := m.Send(ctx, event)

Send is safe for concurrent use. Returns:

Error Meaning
nil Transition succeeded
machina.ErrInvalidTransition No transition registered for (currentState, event)
machina.ErrGuardRejected Guard returned false
any other error Returned by a hook

Use errors.Is to check:

if errors.Is(err, machina.ErrInvalidTransition) { ... }
if errors.Is(err, machina.ErrGuardRejected) { ... }

Hooks

// Called when leaving a state. If it returns error, transition is aborted.
machina.OnExit(state, func(ctx context.Context, event E) error { ... })

// Called after entering a state. State has already changed.
machina.OnEnter(state, func(ctx context.Context, event E) error { ... })

// Called after every successful transition.
machina.OnTransition(func(ctx context.Context, from S, event E, to S) error { ... })

Multiple hooks on the same state are called in registration order.

Note: hooks must not call FSM methods. Send holds the write lock for the full transition, so calling it from a hook will deadlock.

Visualization

fmt.Println(m.DOT())

Outputs Graphviz DOT notation:

digraph machina {
    rankdir=LR;
    "Pending" -> "Active"  [label="Activate"];
    "Active"  -> "Closed"  [label="Close"];
    "Pending" -> "Closed"  [label="Close"];
}

Render: dot -Tpng graph.dot -o graph.png


Full example: order workflow

type OrderState int
const (
    Pending  OrderState = iota
    Paid
    Shipped
    Delivered
    Cancelled
)

type OrderEvent int
const (
    PaymentReceived OrderEvent = iota
    ItemShipped
    ItemDelivered
    CancelOrder
)

m := machina.New[OrderState, OrderEvent](Pending,
    machina.T(Pending, PaymentReceived, Paid),
    machina.T(Paid,    ItemShipped,     Shipped),
    machina.T(Shipped, ItemDelivered,   Delivered),
    machina.TG(Pending, CancelOrder, Cancelled, func(ctx context.Context) bool {
        return isAdmin(ctx)
    }),
    machina.TG(Paid, CancelOrder, Cancelled, func(ctx context.Context) bool {
        return isAdmin(ctx)
    }),

    machina.OnEnter[OrderState, OrderEvent](Shipped, func(ctx context.Context, e OrderEvent) error {
        return sendShippingNotification(ctx)
    }),
    machina.OnTransition[OrderState, OrderEvent](func(ctx context.Context, from OrderState, e OrderEvent, to OrderState) error {
        return auditLog(ctx, from, e, to)
    }),
)

Why machina over looplab/fsm?

looplab/fsm machina
State/event types string any comparable
Invalid transition detection runtime compile error + typed sentinel
Duplicate transition silent overwrite panic at startup
Guard functions
Entry/exit hooks
Context propagation partial ✓ all hooks
DOT export
Thread-safe
Dependencies 0 0
Go generics
Last release 2021 active

License

MIT

About

Type-safe finite state machine for Go. Generics-based alternative to looplab/fsm

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages