Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 54 additions & 40 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,66 @@ package stateless
import (
"context"
"fmt"
"reflect"
)

type transitionKey struct{}

func withTransition(ctx context.Context, transition Transition) context.Context {
func withTransition[S State, T Trigger](ctx context.Context, transition Transition[S, T]) context.Context {
return context.WithValue(ctx, transitionKey{}, transition)
}

// GetTransition returns the transition from the context.
// If there is no transition the returned value is empty.
func GetTransition(ctx context.Context) Transition {
tr, _ := ctx.Value(transitionKey{}).(Transition)
func GetTransition[S State, T Trigger](ctx context.Context) Transition[S, T] {
tr, _ := ctx.Value(transitionKey{}).(Transition[S, T])
return tr
}

// Args is a generic list of arguments.
type Args []any

func (a Args) Len() int {
return len(a)
}

func (a Args) TypeOf(i int) reflect.Type {
return reflect.TypeOf(a[i])
}

var _ Validatable = Args{} // Ensure Args implements Validatable

// ActionFunc describes a generic action function.
// The context will always contain Transition information.
type ActionFunc = func(ctx context.Context, args ...any) error
type ActionFunc[A any] func(ctx context.Context, arg A) error

// GuardFunc defines a generic guard function.
type GuardFunc = func(ctx context.Context, args ...any) bool
type GuardFunc[A any] func(ctx context.Context, arg A) bool

// DestinationSelectorFunc defines a functions that is called to select a dynamic destination.
type DestinationSelectorFunc = func(ctx context.Context, args ...any) (State, error)
type DestinationSelectorFunc[S State, A any] func(ctx context.Context, arg A) (S, error)

// StateConfiguration is the configuration for a single state value.
type StateConfiguration struct {
sm *StateMachine
sr *stateRepresentation
lookup func(State) *stateRepresentation
type StateConfiguration[S State, T Trigger, A any] struct {
sm *StateMachine[S, T, A]
sr *stateRepresentation[S, T, A]
lookup func(S) *stateRepresentation[S, T, A]
}

// State is configured with this configuration.
func (sc *StateConfiguration) State() State {
func (sc *StateConfiguration[S, T, _]) State() S {
return sc.sr.State
}

// Machine that is configured with this configuration.
func (sc *StateConfiguration) Machine() *StateMachine {
func (sc *StateConfiguration[S, T, A]) Machine() *StateMachine[S, T, A] {
return sc.sm
}

// InitialTransition adds an initial transition to this state.
// When entering the current state the state machine will look for an initial transition,
// and enter the target state.
func (sc *StateConfiguration) InitialTransition(targetState State) *StateConfiguration {
func (sc *StateConfiguration[S, T, A]) InitialTransition(targetState S) *StateConfiguration[S, T, A] {
if sc.sr.HasInitialState {
panic(fmt.Sprintf("stateless: This state has already been configured with an initial transition (%v).", sc.sr.InitialTransitionTarget))
}
Expand All @@ -60,22 +74,22 @@ func (sc *StateConfiguration) InitialTransition(targetState State) *StateConfigu
}

// Permit accept the specified trigger and transition to the destination state if the guard conditions are met (if any).
func (sc *StateConfiguration) Permit(trigger Trigger, destinationState State, guards ...GuardFunc) *StateConfiguration {
func (sc *StateConfiguration[S, T, A]) Permit(trigger T, destinationState S, guards ...GuardFunc[A]) *StateConfiguration[S, T, A] {
if destinationState == sc.sr.State {
panic("stateless: Permit() require that the destination state is not equal to the source state. To accept a trigger without changing state, use either Ignore() or PermitReentry().")
}
sc.sr.AddTriggerBehaviour(&transitioningTriggerBehaviour{
baseTriggerBehaviour: baseTriggerBehaviour{Trigger: trigger, Guard: newtransitionGuard(guards...)},
sc.sr.AddTriggerBehaviour(&transitioningTriggerBehaviour[S, T, A]{
baseTriggerBehaviour: baseTriggerBehaviour[T, A]{Trigger: trigger, Guard: newtransitionGuard[A](guards...)},
Destination: destinationState,
})
return sc
}

// InternalTransition add an internal transition to the state machine.
// An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine.
func (sc *StateConfiguration) InternalTransition(trigger Trigger, action ActionFunc, guards ...GuardFunc) *StateConfiguration {
sc.sr.AddTriggerBehaviour(&internalTriggerBehaviour{
baseTriggerBehaviour: baseTriggerBehaviour{Trigger: trigger, Guard: newtransitionGuard(guards...)},
func (sc *StateConfiguration[S, T, A]) InternalTransition(trigger T, action ActionFunc[A], guards ...GuardFunc[A]) *StateConfiguration[S, T, A] {
sc.sr.AddTriggerBehaviour(&internalTriggerBehaviour[S, T, A]{
baseTriggerBehaviour: baseTriggerBehaviour[T, A]{Trigger: trigger, Guard: newtransitionGuard[A](guards...)},
Action: action,
})
return sc
Expand All @@ -85,37 +99,37 @@ func (sc *StateConfiguration) InternalTransition(trigger Trigger, action ActionF
// Reentry behaves as though the configured state transitions to an identical sibling state.
// Applies to the current state only. Will not re-execute superstate actions, or
// cause actions to execute transitioning between super- and sub-states.
func (sc *StateConfiguration) PermitReentry(trigger Trigger, guards ...GuardFunc) *StateConfiguration {
sc.sr.AddTriggerBehaviour(&reentryTriggerBehaviour{
baseTriggerBehaviour: baseTriggerBehaviour{Trigger: trigger, Guard: newtransitionGuard(guards...)},
func (sc *StateConfiguration[S, T, A]) PermitReentry(trigger T, guards ...GuardFunc[A]) *StateConfiguration[S, T, A] {
sc.sr.AddTriggerBehaviour(&reentryTriggerBehaviour[S, T, A]{
baseTriggerBehaviour: baseTriggerBehaviour[T, A]{Trigger: trigger, Guard: newtransitionGuard[A](guards...)},
Destination: sc.sr.State,
})
return sc
}

// Ignore the specified trigger when in the configured state, if the guards return true.
func (sc *StateConfiguration) Ignore(trigger Trigger, guards ...GuardFunc) *StateConfiguration {
sc.sr.AddTriggerBehaviour(&ignoredTriggerBehaviour{
baseTriggerBehaviour: baseTriggerBehaviour{Trigger: trigger, Guard: newtransitionGuard(guards...)},
func (sc *StateConfiguration[S, T, A]) Ignore(trigger T, guards ...GuardFunc[A]) *StateConfiguration[S, T, A] {
sc.sr.AddTriggerBehaviour(&ignoredTriggerBehaviour[T, A]{
baseTriggerBehaviour: baseTriggerBehaviour[T, A]{Trigger: trigger, Guard: newtransitionGuard(guards...)},
})
return sc
}

// PermitDynamic accept the specified trigger and transition to the destination state, calculated dynamically by the supplied function.
func (sc *StateConfiguration) PermitDynamic(trigger Trigger, selector DestinationSelectorFunc, guards ...GuardFunc) *StateConfiguration {
func (sc *StateConfiguration[S, T, A]) PermitDynamic(trigger T, selector DestinationSelectorFunc[S, A], guards ...GuardFunc[A]) *StateConfiguration[S, T, A] {
guardDescriptors := make([]invocationInfo, len(guards))
for i, guard := range guards {
guardDescriptors[i] = newinvocationInfo(guard)
}
sc.sr.AddTriggerBehaviour(&dynamicTriggerBehaviour{
baseTriggerBehaviour: baseTriggerBehaviour{Trigger: trigger, Guard: newtransitionGuard(guards...)},
sc.sr.AddTriggerBehaviour(&dynamicTriggerBehaviour[S, T, A]{
baseTriggerBehaviour: baseTriggerBehaviour[T, A]{Trigger: trigger, Guard: newtransitionGuard[A](guards...)},
Destination: selector,
})
return sc
}

// OnActive specify an action that will execute when activating the configured state.
func (sc *StateConfiguration) OnActive(action func(context.Context) error) *StateConfiguration {
func (sc *StateConfiguration[S, T, A]) OnActive(action func(context.Context) error) *StateConfiguration[S, T, A] {
sc.sr.ActivateActions = append(sc.sr.ActivateActions, actionBehaviourSteady{
Action: action,
Description: newinvocationInfo(action),
Expand All @@ -124,7 +138,7 @@ func (sc *StateConfiguration) OnActive(action func(context.Context) error) *Stat
}

// OnDeactivate specify an action that will execute when deactivating the configured state.
func (sc *StateConfiguration) OnDeactivate(action func(context.Context) error) *StateConfiguration {
func (sc *StateConfiguration[S, T, A]) OnDeactivate(action func(context.Context) error) *StateConfiguration[S, T, A] {
sc.sr.DeactivateActions = append(sc.sr.DeactivateActions, actionBehaviourSteady{
Action: action,
Description: newinvocationInfo(action),
Expand All @@ -133,17 +147,17 @@ func (sc *StateConfiguration) OnDeactivate(action func(context.Context) error) *
}

// OnEntry specify an action that will execute when transitioning into the configured state.
func (sc *StateConfiguration) OnEntry(action ActionFunc) *StateConfiguration {
sc.sr.EntryActions = append(sc.sr.EntryActions, actionBehaviour{
func (sc *StateConfiguration[S, T, A]) OnEntry(action ActionFunc[A]) *StateConfiguration[S, T, A] {
sc.sr.EntryActions = append(sc.sr.EntryActions, actionBehaviour[S, T, A]{
Action: action,
Description: newinvocationInfo(action),
})
return sc
}

// OnEntryFrom Specify an action that will execute when transitioning into the configured state from a specific trigger.
func (sc *StateConfiguration) OnEntryFrom(trigger Trigger, action ActionFunc) *StateConfiguration {
sc.sr.EntryActions = append(sc.sr.EntryActions, actionBehaviour{
func (sc *StateConfiguration[S, T, A]) OnEntryFrom(trigger T, action ActionFunc[A]) *StateConfiguration[S, T, A] {
sc.sr.EntryActions = append(sc.sr.EntryActions, actionBehaviour[S, T, A]{
Action: action,
Description: newinvocationInfo(action),
Trigger: &trigger,
Expand All @@ -152,17 +166,17 @@ func (sc *StateConfiguration) OnEntryFrom(trigger Trigger, action ActionFunc) *S
}

// OnExit specify an action that will execute when transitioning from the configured state.
func (sc *StateConfiguration) OnExit(action ActionFunc) *StateConfiguration {
sc.sr.ExitActions = append(sc.sr.ExitActions, actionBehaviour{
func (sc *StateConfiguration[S, T, A]) OnExit(action ActionFunc[A]) *StateConfiguration[S, T, A] {
sc.sr.ExitActions = append(sc.sr.ExitActions, actionBehaviour[S, T, A]{
Action: action,
Description: newinvocationInfo(action),
})
return sc
}

// OnExitWith specifies an action that will execute when transitioning from the configured state with a specific trigger.
func (sc *StateConfiguration) OnExitWith(trigger Trigger, action ActionFunc) *StateConfiguration {
sc.sr.ExitActions = append(sc.sr.ExitActions, actionBehaviour{
func (sc *StateConfiguration[S, T, A]) OnExitWith(trigger T, action ActionFunc[A]) *StateConfiguration[S, T, A] {
sc.sr.ExitActions = append(sc.sr.ExitActions, actionBehaviour[S, T, A]{
Action: action,
Description: newinvocationInfo(action),
Trigger: &trigger,
Expand All @@ -176,7 +190,7 @@ func (sc *StateConfiguration) OnExitWith(trigger Trigger, action ActionFunc) *St
// entry actions for the superstate are executed.
// Likewise when leaving from the substate to outside the supserstate,
// exit actions for the superstate will execute.
func (sc *StateConfiguration) SubstateOf(superstate State) *StateConfiguration {
func (sc *StateConfiguration[S, T, A]) SubstateOf(superstate S) *StateConfiguration[S, T, A] {
state := sc.sr.State
// Check for accidental identical cyclic configuration
if state == superstate {
Expand All @@ -185,7 +199,7 @@ func (sc *StateConfiguration) SubstateOf(superstate State) *StateConfiguration {

// Check for accidental identical nested cyclic configuration
var empty struct{}
supersets := map[State]struct{}{state: empty}
supersets := map[S]struct{}{state: empty}
// Build list of super states and check for

activeSc := sc.lookup(superstate)
Expand Down
39 changes: 19 additions & 20 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package stateless_test
import (
"context"
"fmt"
"reflect"

"github.com/qmuntal/stateless"
"reflect"
)

const (
Expand All @@ -29,35 +28,35 @@ const (
)

func Example() {
phoneCall := stateless.NewStateMachine(stateOffHook)
phoneCall := stateless.NewStateMachine[string, string, stateless.Args](stateOffHook)
phoneCall.SetTriggerParameters(triggerSetVolume, reflect.TypeOf(0))
phoneCall.SetTriggerParameters(triggerCallDialed, reflect.TypeOf(""))

phoneCall.Configure(stateOffHook).
Permit(triggerCallDialed, stateRinging)

phoneCall.Configure(stateRinging).
OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...any) error {
OnEntryFrom(triggerCallDialed, func(_ context.Context, args stateless.Args) error {
onDialed(args[0].(string))
return nil
}).
Permit(triggerCallConnected, stateConnected)

phoneCall.Configure(stateConnected).
OnEntry(startCallTimer).
OnExit(func(_ context.Context, _ ...any) error {
OnExit(func(_ context.Context, args stateless.Args) error {
stopCallTimer()
return nil
}).
InternalTransition(triggerMuteMicrophone, func(_ context.Context, _ ...any) error {
InternalTransition(triggerMuteMicrophone, func(_ context.Context, _ stateless.Args) error {
onMute()
return nil
}).
InternalTransition(triggerUnmuteMicrophone, func(_ context.Context, _ ...any) error {
InternalTransition(triggerUnmuteMicrophone, func(_ context.Context, _ stateless.Args) error {
onUnmute()
return nil
}).
InternalTransition(triggerSetVolume, func(_ context.Context, args ...any) error {
InternalTransition(triggerSetVolume, func(_ context.Context, args stateless.Args) error {
onSetVolume(args[0].(int))
return nil
}).
Expand All @@ -66,7 +65,7 @@ func Example() {

phoneCall.Configure(stateOnHold).
SubstateOf(stateConnected).
OnExitWith(triggerPhoneHurledAgainstWall, func(ctx context.Context, args ...any) error {
OnExitWith(triggerPhoneHurledAgainstWall, func(ctx context.Context, _ stateless.Args) error {
onWasted()
return nil
}).
Expand All @@ -75,16 +74,16 @@ func Example() {

phoneCall.ToGraph()

phoneCall.Fire(triggerCallDialed, "qmuntal")
phoneCall.Fire(triggerCallConnected)
phoneCall.Fire(triggerSetVolume, 2)
phoneCall.Fire(triggerPlacedOnHold)
phoneCall.Fire(triggerMuteMicrophone)
phoneCall.Fire(triggerUnmuteMicrophone)
phoneCall.Fire(triggerTakenOffHold)
phoneCall.Fire(triggerSetVolume, 11)
phoneCall.Fire(triggerPlacedOnHold)
phoneCall.Fire(triggerPhoneHurledAgainstWall)
phoneCall.Fire(triggerCallDialed, stateless.Args{"qmuntal"})
phoneCall.Fire(triggerCallConnected, nil)
phoneCall.Fire(triggerSetVolume, stateless.Args{2})
phoneCall.Fire(triggerPlacedOnHold, nil)
phoneCall.Fire(triggerMuteMicrophone, nil)
phoneCall.Fire(triggerUnmuteMicrophone, nil)
phoneCall.Fire(triggerTakenOffHold, nil)
phoneCall.Fire(triggerSetVolume, stateless.Args{11})
phoneCall.Fire(triggerPlacedOnHold, nil)
phoneCall.Fire(triggerPhoneHurledAgainstWall, nil)
fmt.Printf("State is %v\n", phoneCall.MustState())

// Output:
Expand Down Expand Up @@ -120,7 +119,7 @@ func onWasted() {
fmt.Println("Wasted!")
}

func startCallTimer(_ context.Context, _ ...any) error {
func startCallTimer(_ context.Context, _ stateless.Args) error {
fmt.Println("[Timer:] Call started at 11:00am")
return nil
}
Expand Down
Loading