statemachine

package
v0.0.0-...-8d237ee Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2025 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package statemachine provides a simple routing state machine implementation. This is useful for implementing complex state machines that require routing logic. The state machine is implemented as a series of state functions that take a Request and returns a Request with the next State or an error. An error causes the state machine to stop and return the error. A nil state causes the state machine to stop.

You may build a state machine using either function calls or method calls. The Request.Data object you define can be a stack or heap allocated object. Using a stack allocated object is useful when running a lot of state machines in parallel, as it reduces the amount of memory allocation and garbage collection required.

State machines of this design can reduce testing complexity and improve code readability. You can read about how here: https://medium.com/@johnsiilver/go-state-machine-patterns-3b667f345b5e

This package is has OTEL support built in. If the Context passed to the state machine has a span, the state machine will create a child span for each state. If the state machine returns an error, the span will be marked as an error.

Example:

	package main

	import (
		"context"
		"fmt"
		"io"
		"log"
		"net/http"

		"github.com/gostdlib/ops/statemachine"
	)

	var (
		author = flag.String("author", "", "The author of the quote, if not set will choose a random one")
	)

	// Data is the data passed to through the state machine. It can be modified by the state functions.
	type Data struct {
		// This section is data set before starting the state machine.

		// Author is the author of the quote. If not set it will be chosen at random.
		Author string

		// This section is data set during the state machine.

		// Quote is a quote from the author. It is set in the state machine.
		Quote string

		// httpClient is the http client used to make requests.
		httpClient *http.Client
	}

	func Start(req statemachine.Request[Data]) statemachine.Request[Data] {
		if req.Data.httpClient == nil {
			req.Data.httpClient = &http.Client{}
		}

		if req.Data.Author == "" {
			req.Next = RandomAuthor
			return req
		}
		req.Next = RandomQuote
		return req
	}

	func RandomAuthor(req statemachine.Request[Data]) statemachine.Request[Data] {
		const url = "https://api.quotable.io/randomAuthor" // This is a fake URL
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			req.Err = err
			return req
		}

		req = req.WithContext(ctx)
		resp, err := args.Data.httpClient.Do(req)
		if err != nil {
			req.Err = err
			return req
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			req.Err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
			return req
		}
		b, err := io.ReadAll(resp.Body)
		if err != nil {
			req.Err = err
			return req
		}
		args.Data.Author = string(b)
		req.Next = RandomQuote
		return req
	}

	func RandomQuote(req statemachine.Request[Data]) statemachine.Request[Data] {
		const url = "https://api.quotable.io/randomQuote" // This is a fake URL
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			req.Err = err
			return req
		}

		req = req.WithContext(ctx)
		resp, err := args.Data.httpClient.Do(req)
		if err != nil {
			req.Err = err
			return req
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			req.Err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
			return req
		}
		b, err := io.ReadAll(resp.Body)
		if err != nil {
			req.Err = err
			return req
		}
		args.Data.Quote = string(b)
		req.Next = nil  // This is not needed, but a good way to show that the state machine is done.
		return req
	}

	func main() {
		flag.Parse()

		req := statemachine.Request{
  			Ctx: context.Background(),
     			Data: Data{
				Author: *author,
				httpClient: &http.Client{},
			},
   			Next: Start,
		}

		err := statemachine.Run("Get author quotes", req)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(data.Author, "said", data.Quote)
	}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MethodName

func MethodName(method any) string

MethodName takes a function or a method and returns its name. This is useful in testing to determine the name of the next state in a state machine and do a comparison.

Types

type DeferFn

type DeferFn[T any] func(ctx context.Context, data T, err error) T

Defer is a function that is called when the state machine stops. This function can change the data passed and it will modify Request.Data before it is returned by Run(). err indicates if you had an error and what it was, otherwise the Request completed.

type ErrCyclic

type ErrCyclic struct {
	// SMName is the name of the state machine that detected the cyclic error.
	SMName string
	// LastStage is the name of the stage that would have been executed a second time. This caused the cyclic error.
	LastStage string
	// Stages lists the stages that were executed before the cyclic error was detected.
	Stages string
}

ErrCyclic is an error that is returned when a state machine detects a cyclic error.

func (ErrCyclic) Attrs

func (c ErrCyclic) Attrs() []slog.Attr

Attrs implements the base/errors.Attrs interface.

func (ErrCyclic) Error

func (c ErrCyclic) Error() string

Error implements the error interface.

func (ErrCyclic) Is

func (c ErrCyclic) Is(err error) bool

Is implements the errors.Is interface.

type ErrValidation

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

ErrValidation is an error when trying to run a state machine with invalid arguments.

func (ErrValidation) Error

func (v ErrValidation) Error() string

Error implements the error interface.

type Option

type Option[T any] func(Request[T]) (Request[T], error)

Option is an option for the Run() function. This is currently unused, but exists for future expansion.

type Request

type Request[T any] struct {

	// Ctx is the context passed to the state function.
	Ctx context.Context

	// Data is the data to be passed to the next state.
	Data T

	// Err is the error to be returned by the state machine. If Err is not nil, the state machine stops.
	Err error

	// Next is the next state to be executed. If Next is nil, the state machine stops.
	// Must be set to the initial state to execute before calling Run().
	Next State[T]

	// Defers is a list of functions to be called when the state machine stops. This is
	// useful for cleaning up resources or modifying the data before it is returned.
	Defers []DeferFn[T]
	// contains filtered or unexported fields
}

Request are the request passed to a state function.

func Run

func Run[T any](name string, req Request[T], options ...Option[T]) (Request[T], error)

Run runs the state machine with the given a Request. name is the name of the statemachine for the purpose of OTEL tracing. An error is returned if the state machine fails, name is empty, the Request Ctx/Next is nil or the Err field is not nil.

func WithCyclicCheck

func WithCyclicCheck[T any](req Request[T]) (Request[T], error)

WithCyclicCheck is an option that causes the state machine to error if a state is called more than once. This effectively turns the state machine into a directed acyclic graph.

func (Request[T]) Event

func (r Request[T]) Event(name string, keyValues ...attribute.KeyValue)

Event records an OTEL event into the Request span with name and keyvalues. This allows for stages in your statemachine to record information in each state.

Note: This is a no-op if the Request is not recording.

type State

type State[T any] func(req Request[T]) Request[T]

State is a function that takes a Request and returns a Request. If the returned Request has a nil Next, the state machine stops. If the returned Request has a non-nil Err, the state machine stops and returns the error. If the returned Request has a non-nil next, the state machine continues with the next state.

Jump to

Keyboard shortcuts

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