package
0.0.12
Repository: https://github.com/openshift-online/ocm-common.git
Documentation: pkg.go.dev

# README

The State Machine

This package provides a lightweight, generic state machine framework for enforcing well-defined state transitions during program execution. The framework is designed for broad applicability, including:

  • Flow validation: Ensuring valid sequences of operations in a process.
  • Language validation (e.g., string syntax): Verifying adherence to grammar rules.

Configuring the state machine

The State machine configuration centers around defining a set of states and their valid transitions. Each state holds an acceptor object responsible for validating the current data against the transition's criteria. This allows for user-defined, data-driven state transitions, even for states with multiple outgoing paths.

Let's start with an example: we want have an API that manages our ticketing system and, based on the inputs it receives, it move the ticket to the next status.

Our ticket workflow is very simple:

NEW => ASSIGNED => PROGRESSING => REVIEW => COMPLETED  
                        ^           |  
                        |===========|  

The ticket starts in status NEW, then moves to ASSIGNED, then to PROGRESSING. After the developer has finished, the ticket moves to REVIEW. Here, based on the received input, it can move to COMPLETED or back to PROGRESSING.

The transitions will be as follows:

  • -> NEW: from start state to 'NEW' when the 'NEW' input is received
  • NEW->ASSIGNED: to ASSIGNED when receive the 'ASSIGN' input
  • ASSIGNED->PROGRESSING: to PROGRESSING when receive the 'PROGRESS' input
  • PROGRESSING->REVIEW: to REVIEW when receive the 'REVIEW' input
  • REVIEW->APPROVED: to complete when receive the 'APPROVED' input
  • REVIEW->REJECTED: to progressing when receive the 'REJECTED' input
  • REJECTED->END
  • APPROVED->END

Let's start with state definition:

definition := state_machine.StateMachineDefinition[string, string]{
    States: []state_machine.StateDefinition[string, string]{
        {Name: "NEW", Acceptor: makeAcceptorForString("NEW")},
        {Name: "ASSIGNED", Acceptor: makeAcceptorForString("ASSIGN")},
        {Name: "PROGRESSING", Acceptor: makeAcceptorForString("PROGRESS")},
        {Name: "REVIEW", Acceptor: makeAcceptorForString("REVIEW")},
        {Name: "COMPLETED", Acceptor: makeAcceptorForString("APPROVED")},
    },
}

The makeAcceptorForString is an utility function that returns an acceptor that recognize a simple string. Complex acceptors can be implemented, for example to accept regular expressions.

The one in the example is implemented as follows:

func makeAcceptorForString(s string) func(value string) bool {
	return func(value string) bool {
		return value == s
	}
}

Now that we have defined the statuses, we need to define the transitions:

definition := state_machine.StateMachineDefinition[string, string]{
    States: []state_machine.StateDefinition[string, string]{
        {Name: "NEW", Acceptor: makeAcceptorForString("NEW")},
        {Name: "ASSIGNED", Acceptor: makeAcceptorForString("ASSIGN")},
        {Name: "PROGRESSING", Acceptor: makeAcceptorForString("PROGRESS")},
        {Name: "REVIEW", Acceptor: makeAcceptorForString("REVIEW")},
        {Name: "COMPLETED", Acceptor: makeAcceptorForString("APPROVED")},
    },
    Transitions: []state_machine.TransitionDefinition{
        {StateName: state_machine.StartState, ValidTransitions: []string{"NEW"}},
        {StateName: "NEW", ValidTransitions: []string{"ASSIGNED"}},
        {StateName: "ASSIGNED", ValidTransitions: []string{"PROGRESSING"}},
        {StateName: "PROGRESSING", ValidTransitions: []string{"REVIEW"}},
        {StateName: "REVIEW", ValidTransitions: []string{"COMPLETED", "PROGRESSING"}},
        {StateName: "COMPLETED", ValidTransitions: []string{state_machine.EndState}},
    },
}

Now that we have both the definitions and the transitions, we can get the state machine:

stateMachine := return NewStateMachineBuilder[string, string]().
    WithStateMachineDefinition(&definition).
    Build()

Let's try to parse some flow:

receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "APPROVED"}
currentState := stateMachine
var err error
for _, s := range receivedEvents {
    currentState, err = currentState.Move(s)
    if err != nil {
        fmt.Printf("%v\n", err)
        return
    }
}

if currentState.Eof() {
    fmt.Println("Ticket completed")
} else {
    fmt.Println("Ticket still needs some work")
}

The output of this code (Example 1) will be 'Ticket completed'

Let's try with an invalid flow (Example 2).

receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "ASSIGN"}
...

Finally, let's try a valid flow with some recursion.

receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW"}
...

This time we won't get any error, but our output will be "Ticket still needs some work" (Example 3)

This time the output will be 'unexpected token ASSIGN'.

If needed, you can observe the status transitions by registering an observer:

stateMachine := state_machine.NewStateMachineBuilder[string, string]().
	WithStateMachineDefinition(&definition).
	WithTransitionObserver(func(from, to *state_machine.State[string, string], value string) {
		fmt.Printf("%s => %s (received value: %s) \n", from.Name(), to.Name(), value)
	}).
	Build()

With this change, the output will be (Example 4):

NEW => ASSIGNED (received value: ASSIGN) 
ASSIGNED => PROGRESSING (received value: PROGRESS) 
PROGRESSING => REVIEW (received value: REVIEW) 
REVIEW => PROGRESSING (received value: PROGRESS) 
PROGRESSING => REVIEW (received value: REVIEW) 
REVIEW => PROGRESSING (received value: PROGRESS) 
PROGRESSING => REVIEW (received value: REVIEW) 
Ticket still needs some work

An interceptor can be used to abort the parsing if some event occurs (Example 5):

reviewCount := 0

stateMachine := state_machine.NewStateMachineBuilder[string, string]().
    WithStateMachineDefinition(&definition).
    WithTransitionObserver(func(from, to *state_machine.State[string, string], value string) {
        fmt.Printf("%s => %s (received value: %s) \n", from.Name(), to.Name(), value)
    }).
    WithTransitionInterceptor(func(from, to *state_machine.State[string, string], value string) error {
        if to.Name() == "REVIEW" {
            reviewCount++
            if reviewCount > 2 {
                return fmt.Errorf("Too many reviews. Aborting.")
            }
        }

        return nil
    }).
    Build()

This time the parsing is aborted if the statemachine passes through the REVIEW state more than 2 times. The output will be:

NEW => ASSIGNED (received value: ASSIGN) 
ASSIGNED => PROGRESSING (received value: PROGRESS) 
PROGRESSING => REVIEW (received value: REVIEW) 
REVIEW => PROGRESSING (received value: PROGRESS) 
PROGRESSING => REVIEW (received value: REVIEW) 
REVIEW => PROGRESSING (received value: PROGRESS) 
Too many reviews. Aborting.

Examples

Example 1

package main

import (
	"fmt"
	"github.com/openshift-online/ocm-common/pkg/utils/state_machine"
)

func makeAcceptorForString(s string) func(value string) bool {
	return func(value string) bool {
		return value == s
	}
}

func main() {
	definition := state_machine.StateMachineDefinition[string, string]{
		States: []state_machine.StateDefinition[string, string]{
			{Name: "NEW", Acceptor: makeAcceptorForString("NEW")},
			{Name: "ASSIGNED", Acceptor: makeAcceptorForString("ASSIGN")},
			{Name: "PROGRESSING", Acceptor: makeAcceptorForString("PROGRESS")},
			{Name: "REVIEW", Acceptor: makeAcceptorForString("REVIEW")},
			{Name: "COMPLETED", Acceptor: makeAcceptorForString("APPROVED")},
		},
		Transitions: []state_machine.TransitionDefinition{
			{StateName: state_machine.StartState, ValidTransitions: []string{"NEW"}},
			{StateName: "NEW", ValidTransitions: []string{"ASSIGNED"}},
			{StateName: "ASSIGNED", ValidTransitions: []string{"PROGRESSING"}},
			{StateName: "PROGRESSING", ValidTransitions: []string{"REVIEW"}},
			{StateName: "REVIEW", ValidTransitions: []string{"COMPLETED", "PROGRESSING"}},
			{StateName: "COMPLETED", ValidTransitions: []string{state_machine.EndState}},
		},
	}

	stateMachine := state_machine.NewStateMachineBuilder[string, string]().
		WithStateMachineDefinition(&definition).
		Build()

	receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "APPROVED"}
	currentState := stateMachine
	var err error
	for _, s := range receivedEvents {
		currentState, err = currentState.Move(s)
		if err != nil {
			fmt.Printf("%v\n", err)
			return
		}
	}

	if currentState.Eof() {
		fmt.Println("Ticket completed")
	} else {
		fmt.Println("Ticket still needs some work")
	}
}

Example 2

package main

import (
	"fmt"
	"github.com/openshift-online/ocm-common/pkg/utils/state_machine"
)

func makeAcceptorForString(s string) func(value string) bool {
	return func(value string) bool {
		return value == s
	}
}

func main() {
	definition := state_machine.StateMachineDefinition[string, string]{
		States: []state_machine.StateDefinition[string, string]{
			{Name: "NEW", Acceptor: makeAcceptorForString("NEW")},
			{Name: "ASSIGNED", Acceptor: makeAcceptorForString("ASSIGN")},
			{Name: "PROGRESSING", Acceptor: makeAcceptorForString("PROGRESS")},
			{Name: "REVIEW", Acceptor: makeAcceptorForString("REVIEW")},
			{Name: "COMPLETED", Acceptor: makeAcceptorForString("APPROVED")},
		},
		Transitions: []state_machine.TransitionDefinition{
			{StateName: state_machine.StartState, ValidTransitions: []string{"NEW"}},
			{StateName: "NEW", ValidTransitions: []string{"ASSIGNED"}},
			{StateName: "ASSIGNED", ValidTransitions: []string{"PROGRESSING"}},
			{StateName: "PROGRESSING", ValidTransitions: []string{"REVIEW"}},
			{StateName: "REVIEW", ValidTransitions: []string{"COMPLETED", "PROGRESSING"}},
			{StateName: "COMPLETED", ValidTransitions: []string{state_machine.EndState}},
		},
	}

	stateMachine := state_machine.NewStateMachineBuilder[string, string]().
		WithStateMachineDefinition(&definition).
		Build()

	receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "ASSIGN"}
	currentState := stateMachine
	var err error
	for _, s := range receivedEvents {
		currentState, err = currentState.Move(s)
		if err != nil {
			fmt.Printf("%v\n", err)
			return
		}
	}

	if currentState.Eof() {
		fmt.Println("Ticket completed")
	} else {
		fmt.Println("Ticket still needs some work")
	}
}

Example 3

package main

import (
	"fmt"
	"github.com/openshift-online/ocm-common/pkg/utils/state_machine"
)

func makeAcceptorForString(s string) func(value string) bool {
	return func(value string) bool {
		return value == s
	}
}

func main() {
	definition := state_machine.StateMachineDefinition[string, string]{
		States: []state_machine.StateDefinition[string, string]{
			{Name: "NEW", Acceptor: makeAcceptorForString("NEW")},
			{Name: "ASSIGNED", Acceptor: makeAcceptorForString("ASSIGN")},
			{Name: "PROGRESSING", Acceptor: makeAcceptorForString("PROGRESS")},
			{Name: "REVIEW", Acceptor: makeAcceptorForString("REVIEW")},
			{Name: "COMPLETED", Acceptor: makeAcceptorForString("APPROVED")},
		},
		Transitions: []state_machine.TransitionDefinition{
			{StateName: state_machine.StartState, ValidTransitions: []string{"NEW"}},
			{StateName: "NEW", ValidTransitions: []string{"ASSIGNED"}},
			{StateName: "ASSIGNED", ValidTransitions: []string{"PROGRESSING"}},
			{StateName: "PROGRESSING", ValidTransitions: []string{"REVIEW"}},
			{StateName: "REVIEW", ValidTransitions: []string{"COMPLETED", "PROGRESSING"}},
			{StateName: "COMPLETED", ValidTransitions: []string{state_machine.EndState}},
		},
	}

	stateMachine := state_machine.NewStateMachineBuilder[string, string]().
		WithStateMachineDefinition(&definition).
		Build()

	receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW"}
	currentState := stateMachine
	var err error
	for _, s := range receivedEvents {
		currentState, err = currentState.Move(s)
		if err != nil {
			fmt.Printf("%v\n", err)
			return
		}
	}

	if currentState.Eof() {
		fmt.Println("Ticket completed")
	} else {
		fmt.Println("Ticket still needs some work")
	}
}

Example 4

package main

import (
	"fmt"
	"github.com/openshift-online/ocm-common/pkg/utils/state_machine"
)

func makeAcceptorForString(s string) func(value string) bool {
	return func(value string) bool {
		return value == s
	}
}

func main() {
	definition := state_machine.StateMachineDefinition[string, string]{
		States: []state_machine.StateDefinition[string, string]{
			{Name: "NEW", Acceptor: makeAcceptorForString("NEW")},
			{Name: "ASSIGNED", Acceptor: makeAcceptorForString("ASSIGN")},
			{Name: "PROGRESSING", Acceptor: makeAcceptorForString("PROGRESS")},
			{Name: "REVIEW", Acceptor: makeAcceptorForString("REVIEW")},
			{Name: "COMPLETED", Acceptor: makeAcceptorForString("APPROVED")},
		},
		Transitions: []state_machine.TransitionDefinition{
			{StateName: state_machine.StartState, ValidTransitions: []string{"NEW"}},
			{StateName: "NEW", ValidTransitions: []string{"ASSIGNED"}},
			{StateName: "ASSIGNED", ValidTransitions: []string{"PROGRESSING"}},
			{StateName: "PROGRESSING", ValidTransitions: []string{"REVIEW"}},
			{StateName: "REVIEW", ValidTransitions: []string{"COMPLETED", "PROGRESSING"}},
			{StateName: "COMPLETED", ValidTransitions: []string{state_machine.EndState}},
		},
	}

	stateMachine := state_machine.NewStateMachineBuilder[string, string]().
		WithStateMachineDefinition(&definition).
		WithTransitionObserver(func(from, to *state_machine.State[string, string], value string) {
			fmt.Printf("%s => %s (received value: %s) \n", from.Name(), to.Name(), value)
		}).
		Build()

	receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW"}
	currentState := stateMachine
	var err error
	for _, s := range receivedEvents {
		currentState, err = currentState.Move(s)
		if err != nil {
			fmt.Printf("%v\n", err)
			return
		}
	}

	if currentState.Eof() {
		fmt.Println("Ticket completed")
	} else {
		fmt.Println("Ticket still needs some work")
	}
}

Example 5

package main

import (
	"fmt"
	"github.com/openshift-online/ocm-common/pkg/utils/state_machine"
)

func makeAcceptorForString(s string) func(value string) bool {
	return func(value string) bool {
		return value == s
	}
}

func main() {
	definition := state_machine.StateMachineDefinition[string, string]{
		States: []state_machine.StateDefinition[string, string]{
			{Name: "NEW", Acceptor: makeAcceptorForString("NEW")},
			{Name: "ASSIGNED", Acceptor: makeAcceptorForString("ASSIGN")},
			{Name: "PROGRESSING", Acceptor: makeAcceptorForString("PROGRESS")},
			{Name: "REVIEW", Acceptor: makeAcceptorForString("REVIEW")},
			{Name: "COMPLETED", Acceptor: makeAcceptorForString("APPROVED")},
		},
		Transitions: []state_machine.TransitionDefinition{
			{StateName: state_machine.StartState, ValidTransitions: []string{"NEW"}},
			{StateName: "NEW", ValidTransitions: []string{"ASSIGNED"}},
			{StateName: "ASSIGNED", ValidTransitions: []string{"PROGRESSING"}},
			{StateName: "PROGRESSING", ValidTransitions: []string{"REVIEW"}},
			{StateName: "REVIEW", ValidTransitions: []string{"COMPLETED", "PROGRESSING"}},
			{StateName: "COMPLETED", ValidTransitions: []string{state_machine.EndState}},
		},
	}

	reviewCount := 0

	stateMachine := state_machine.NewStateMachineBuilder[string, string]().
		WithStateMachineDefinition(&definition).
		WithTransitionObserver(func(from, to *state_machine.State[string, string], value string) {
			fmt.Printf("%s => %s (received value: %s) \n", from.Name(), to.Name(), value)
		}).
		WithTransitionInterceptor(func(from, to *state_machine.State[string, string], value string) error {
			if to.Name() == "REVIEW" {
				reviewCount++
				if reviewCount > 2 {
					return fmt.Errorf("Too many reviews. Aborting.")
				}
			}

			return nil
		}).
		Build()

	receivedEvents := []string{"NEW", "ASSIGN", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW", "PROGRESS", "REVIEW"}
	currentState := stateMachine
	var err error
	for _, s := range receivedEvents {
		currentState, err = currentState.Move(s)
		if err != nil {
			fmt.Printf("%v\n", err)
			return
		}
	}

	if currentState.Eof() {
		fmt.Println("Ticket completed")
	} else {
		fmt.Println("Ticket still needs some work")
	}
}

# Functions

No description provided by the author
No description provided by the author

# Constants

No description provided by the author
No description provided by the author

# Structs

State represent a single state of the state machine T - the type of the data attached to the state.
No description provided by the author
No description provided by the author
No description provided by the author

# Interfaces

StateBuilder - builder of State objects.
No description provided by the author
No description provided by the author

# Type aliases

No description provided by the author
No description provided by the author
No description provided by the author