SkillAgentSearch skills...

Statetrooper

StateTrooper is a Go package that provides a finite state machine (FSM) for managing states. It allows you to define and enforce state transitions based on predefined rules.

Install / Use

/learn @hishamk/Statetrooper
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Tiny, no frills finite state machine for Go

GoDoc GitHub tag (latest SemVer pre-release)

Go Coverage Go Report Card MIT Code size

StateTrooper is a Go package that provides a finite state machine (FSM) for managing states. It allows you to define and enforce state transitions based on predefined rules.

⚠️ Please keep in mind that StateTrooper is still under active development and therefore full backward compatibility is not guaranteed before reaching v1.0.0.

Features

  • Generic support for different comparable types.
  • Transition history with metadata. History size configurable.
  • Thread safe.
  • Super minimal - no triggers/events or actions/callbacks. For my use case I just needed a structured, serializable way to constrain and track state transitions.
  • Is able to generate Mermaid.js diagram descriptions for the transition rules and transition history.

Rules diagram:

Mermaid.js rules diagram

Transition history diagram:

Mermaid.js transition history diagram

Installation

To install StateTrooper, use the following command:

go get github.com/hishamk/statetrooper

Usage

Import the statetrooper package into your Go code:

import "github.com/hishamk/statetrooper"

Create an instance of the FSM with the desired state enum type, initial state and maximum transition history size:

fsm := statetrooper.NewFSM[CustomStateEnum](CustomStateEnumA, 10)

Add valid transitions between states. AddRule takes variadic parameters for the allowed states:

// Created -> Picked or Canceled
AddRule(StatusCreated, StatusPicked, StatusCanceled)
// Picked -> Packed or Canceled
AddRule(StatusPicked, StatusPacked, StatusCanceled)
// Packed -> Shipped
AddRule(StatusPacked, StatusShipped)
// Shipped -> Delivered
AddRule(StatusShipped, StatusDelivered)
// Canceled -> Reinstated
AddRule(StatusCanceled, StatusReinstated)
// Reinstated -> Picked or Canceled
AddRule(StatusReinstated, StatusPicked, StatusCanceled)

Check if a transition from the current state to the target state is valid:

canTransition := fsm.CanTransition(targetState)

Transition the entity from the current state to the target state with no metadata:

newState, err := fsm.Transition(targetState, nil)
if err != nil {
    // Handle the error
}

Transition the entity from the current state to the target state with metadata:

newState, err := fsm.Transition(
	CustomStateEnumB,
	map[string]string{
		"requested_by":  "Mahmoud",
		"logic_version": "1.0",
	})

Generate Mermaid.js rules diagram:

diagram, _ :=order.State.GenerateMermaidRulesDiagram()

In order to generate a diagram, the states type must have a String() method. Ensure that the formatted string returned does not contain any invalid characters for Mermaid.

Use the generated Mermaid code with your Mermaid visualizer to generate the diagram.

graph LR;
shipped;
canceled;
reinstated;
created;
picked;
packed;
created --> picked;
created --> canceled;
picked --> packed;
picked --> canceled;
packed --> shipped;
shipped --> delivered;
canceled --> reinstated;
reinstated --> picked;
reinstated --> canceled;

Mermaid.js diagram

Generate Mermaid.js transition history diagram:

diagram, _ :=order.State.GenerateMermaidTransitionHistoryDiagram()
graph TD;
packed;
shipped;
delivered;
created;
picked;
canceled;
reinstated;
created -->|1| picked;
picked -->|2| canceled;
canceled -->|3| reinstated;
reinstated -->|4| picked;
picked -->|5| packed;
packed -->|6| shipped;
shipped -->|7| delivered;

Mermaid.js diagram

Benchmarks

| Benchmark | Operations | Time per Operation | Memory Allocated per Operation | | ---------------------------- | ---------- | ------------------ | ------------------------------ | | Benchmark_singleTransition | 5,166,985 | 273.8 ns/op | 314 allocs/op | | Benchmark_twoTransitions | 2,835,214 | 513.6 ns/op | 577 allocs/op | | Benchmark_accessCurrentState | 75,695,847 | 14.36 ns/op | 0 allocs/op | | Benchmark_accessCurrentStateWithMetadata | 3,881,552 | 312.2 ns/op | 336 B/op | | Benchmark_accessCurrentStateWithEmptyMetadata | 55,361,556 | 22.37 ns/op | 0 B/op | | Benchmark_accessTransitions | 39,356,628 | 28.74 ns/op | 48 allocs/op | | Benchmark_marshalJSON | 1,000,000 | 1,174 ns/op | 384 allocs/op | | Benchmark_unmarshalJSON | 318,949 | 3,741 ns/op | 1,240 allocs/op |

Example

Here's an example usage with a custom entity struct and state enum:

type OrderStatusEnum string

// Enum values for the custom entity
const (
	StatusCreated    OrderStatusEnum = "created"
	StatusPicked     OrderStatusEnum = "picked"
	StatusPacked     OrderStatusEnum = "packed"
	StatusShipped    OrderStatusEnum = "shipped"
	StatusDelivered  OrderStatusEnum = "delivered"
	StatusCanceled   OrderStatusEnum = "canceled"
	StatusReinstated OrderStatusEnum = "reinstated"
)

// Order represents a custom entity with its current state
type Order struct {
	State *statetrooper.FSM[OrderStatusEnum]
}

func main() {
	// Create a new order with the initial state
	order := &Order{State: statetrooper.NewFSM[OrderStatusEnum](StatusCreated, 10)}

	// Define the valid state transitions for the order

	// Created -> Picked or Canceled
	order.State.AddRule(StatusCreated, StatusPicked, StatusCanceled)
	// Picked -> Packed or Canceled
	order.State.AddRule(StatusPicked, StatusPacked, StatusCanceled)
	// Packed -> Shipped
	order.State.AddRule(StatusPacked, StatusShipped)
	// Shipped -> Delivered
	order.State.AddRule(StatusShipped, StatusDelivered)
	// Canceled -> Reinstated
	order.State.AddRule(StatusCanceled, StatusReinstated)
	// Reinstated -> Picked or Canceled
	order.State.AddRule(StatusReinstated, StatusPicked, StatusCanceled)

	// Check if a transition is valid
	canTransition := order.State.CanTransition(StatusPicked)
	fmt.Printf("Can transition to %s: %t\n", StatusPicked, canTransition)

	// Transition to picked
	_, err := order.State.Transition(StatusPicked, nil)
	if err != nil {
		fmt.Println("Transition error:", err)
	} else {
		fmt.Println("Transition successful. Current state:", order.State.CurrentState())
	}

	// Check if a transition to canceled is valid
	canTransition = order.State.CanTransition(StatusCanceled)
	fmt.Printf("Can transition to %s: %t\n", StatusCanceled, canTransition)

	// Transition to canceled
	_, err = order.State.Transition(StatusCanceled, nil)
	if err != nil {
		fmt.Println("Transition error:", err)
	} else {
		fmt.Println("Transition successful. Current state:", order.State.CurrentState())
	}

	// Check if we can resinstate the order
	canTransition = order.State.CanTransition(StatusReinstated)
	fmt.Printf("Can transition to %s: %t\n", StatusReinstated, canTransition)

	// Transition to reinstated
	_, err = order.State.Transition(StatusReinstated, nil)
	if err != nil {
		fmt.Println("Transition error:", err)
	} else {
		fmt.Println("Transition successful. Current state:", order.State.CurrentState())
	}

	// Transition to picked
	_, err = order.State.Transition(StatusPicked, nil)
	if err != nil {
		fmt.Println("Transition error:", err)
	} else {
		fmt.Println("Transition successful. Current state:", order.State.CurrentState())
	}

	// Transition to packed
	_, err = order.State.Transition(StatusPacked, nil)
	if err != nil {
		fmt.Println("Transition error:", err)
	} else {
		fmt.Println("Transition successful. Current state:", order.State.CurrentState())
	}

	// Transition to shipped
	_, err = order.State.Transition(
		StatusShipped,
		map[string]string{
			"carrier":         "Aramex",
			"tracking_number": "1234567890",
		})

	if err != nil {
		fmt.Println("Transition error:", err)
	} else {
		fmt.Println("Transition successful. Current state:", order.State.CurrentState())
	}

	// Transition to delivered
	_, err = order.State.Transition(StatusDelivered, nil)
	if err != nil {
		fmt.Println("Transition error:", err)
	} else {
		fmt.Println("Transition successful. Current state:", order.State.CurrentState())
	}

	// print the current FSM data
	fmt.Println("Current FSM data:", order.State)
}

Note that states can be defined using any comparable type, such as strings, int, etc e.g.:

// CustomStateEnum represents the state enum for the custom entity
type CustomStateEnum int

// Enum values for the custom entity
const (
	CustomStateEnumA CustomStateEnum = iota
	CustomStateEnumB
	CustomStateEnumC
)

func (e CustomStateEnum) String() string {
        return fmt.Sprintf("%d", e)
}

Accessing current state metadata

Use CurrentStateWithMetadata when you need both the entity's state and the metadata from the transition that set it. The method returns the current state and a copy of the metadata if the last transition supplied any.

state, metadata := order.State.CurrentStateWithMetadata()
fmt.Printf("Current state: %s, metadata: %v\n", state, metadata)

Serialization

Current state, transition history and any metadata can be

Related Skills

View on GitHub
GitHub Stars221
CategoryDevelopment
Updated3mo ago
Forks5

Languages

Go

Security Score

97/100

Audited on Dec 1, 2025

No findings