Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test passing state and trigger as generic types #35

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
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
64 changes: 32 additions & 32 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.13.x, 1.14.x, 1.15.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: go test -race -covermode atomic -coverprofile profile.cov ./...
- name: Send coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
flag-name: Go-${{ matrix.go-version }}
parallel: true
finish:
needs: test
runs-on: ubuntu-latest
steps:
- uses: shogo82148/actions-goveralls@v1
with:
parallel-finished: true

on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.18.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: go test -race -covermode atomic -coverprofile profile.cov ./...
- name: Send coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
flag-name: Go-${{ matrix.go-version }}
parallel: true
finish:
needs: test
runs-on: ubuntu-latest
steps:
- uses: shogo82148/actions-goveralls@v1
with:
parallel-finished: true
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@

# Stateless

**Create *state machines* and lightweight *state machine-based workflows* directly in Go code:**
**Create _state machines_ and lightweight _state machine-based workflows_ directly in Go code:**

```go
phoneCall := stateless.NewStateMachine(stateOffHook)
phoneCall := stateless.NewStateMachine[string, string](stateOffHook)

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

Expand Down Expand Up @@ -50,19 +50,19 @@ The state machine implemented in this library is based on the theory of [UML sta

Most standard state machine constructs are supported:

* Support for states and triggers of any comparable type (int, strings, boolean, structs, etc.)
* Hierarchical states
* Entry/exit events for states
* Guard clauses to support conditional transitions
* Introspection
- Support for states and triggers of any comparable type (int, strings, boolean, structs, etc.)
- Hierarchical states
- Entry/exit events for states
- Guard clauses to support conditional transitions
- Introspection

Some useful extensions are also provided:

* Ability to store state externally (for example, in a property tracked by an ORM)
* Parameterised triggers
* Reentrant states
* Thread-safe
* Export to DOT graph
- Ability to store state externally (for example, in a property tracked by an ORM)
- Parameterised triggers
- Reentrant states
- Thread-safe
- Export to DOT graph

### Hierarchical States

Expand Down Expand Up @@ -102,9 +102,9 @@ sm.Configure(State.C)
Stateless is designed to be embedded in various application models. For example, some ORMs place requirements upon where mapped data may be stored, and UI frameworks often require state to be stored in special "bindable" properties. To this end, the `StateMachine` constructor can accept function arguments that will be used to read and write the state values:

```go
machine := stateless.NewStateMachineWithExternalStorage(func(_ context.Context) (stateless.State, error) {
machine := stateless.NewStateMachineWithExternalStorage[string, string](func(_ context.Context) (string, error) {
return myState.Value, nil
}, func(_ context.Context, state stateless.State) error {
}, func(_ context.Context, state string) error {
myState.Value = state
return nil
}, stateless.FiringQueued)
Expand Down Expand Up @@ -208,7 +208,7 @@ This can then be rendered by tools that support the DOT graph language, such as

This is the complete Phone Call graph as builded in `example_test.go`.

![Phone Call graph](assets/phone-graph.png?raw=true "Phone Call complete DOT")
![Phone Call graph](assets/phone-graph.png?raw=true 'Phone Call complete DOT')

## Project Goals

Expand Down
72 changes: 36 additions & 36 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import (

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 {
return ctx.Value(transitionKey{}).(Transition)
func GetTransition[S State, T Trigger](ctx context.Context) Transition[S, T] {
return ctx.Value(transitionKey{}).(Transition[S, T])
}

// ActionFunc describes a generic action function.
Expand All @@ -25,29 +25,29 @@ type ActionFunc = func(ctx context.Context, args ...interface{}) error
type GuardFunc = func(ctx context.Context, args ...interface{}) bool

// DestinationSelectorFunc defines a functions that is called to select a dynamic destination.
type DestinationSelectorFunc = func(ctx context.Context, args ...interface{}) (State, error)
type DestinationSelectorFunc[S State] func(ctx context.Context, args ...interface{}) (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] struct {
sm *StateMachine[S, T]
sr *stateRepresentation[S, T]
lookup func(S) *stateRepresentation[S, T]
}

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

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

// InitialTransition adds internal 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]) InitialTransition(targetState S) *StateConfiguration[S, T] {
if sc.sr.HasInitialState {
panic(fmt.Sprintf("stateless: This state has already been configured with an initial transition (%v).", sc.sr.InitialTransitionTarget))
}
Expand All @@ -59,22 +59,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]) Permit(trigger T, destinationState S, guards ...GuardFunc) *StateConfiguration[S, T] {
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]{
baseTriggerBehaviour: baseTriggerBehaviour[T]{Trigger: trigger, Guard: newtransitionGuard(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]) InternalTransition(trigger T, action ActionFunc, guards ...GuardFunc) *StateConfiguration[S, T] {
sc.sr.AddTriggerBehaviour(&internalTriggerBehaviour[S, T]{
baseTriggerBehaviour: baseTriggerBehaviour[T]{Trigger: trigger, Guard: newtransitionGuard(guards...)},
Action: action,
})
return sc
Expand All @@ -84,37 +84,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]) PermitReentry(trigger T, guards ...GuardFunc) *StateConfiguration[S, T] {
sc.sr.AddTriggerBehaviour(&reentryTriggerBehaviour[S, T]{
baseTriggerBehaviour: baseTriggerBehaviour[T]{Trigger: trigger, Guard: newtransitionGuard(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]) Ignore(trigger T, guards ...GuardFunc) *StateConfiguration[S, T] {
sc.sr.AddTriggerBehaviour(&ignoredTriggerBehaviour[T]{
baseTriggerBehaviour: baseTriggerBehaviour[T]{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]) PermitDynamic(trigger T, selector DestinationSelectorFunc[S], guards ...GuardFunc) *StateConfiguration[S, T] {
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]{
baseTriggerBehaviour: baseTriggerBehaviour[T]{Trigger: trigger, Guard: newtransitionGuard(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]) OnActive(action func(context.Context) error) *StateConfiguration[S, T] {
sc.sr.ActivateActions = append(sc.sr.ActivateActions, actionBehaviourSteady{
Action: action,
Description: newinvocationInfo(action),
Expand All @@ -123,7 +123,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]) OnDeactivate(action func(context.Context) error) *StateConfiguration[S, T] {
sc.sr.DeactivateActions = append(sc.sr.DeactivateActions, actionBehaviourSteady{
Action: action,
Description: newinvocationInfo(action),
Expand All @@ -132,17 +132,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]) OnEntry(action ActionFunc) *StateConfiguration[S, T] {
sc.sr.EntryActions = append(sc.sr.EntryActions, actionBehaviour[S, T]{
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]) OnEntryFrom(trigger T, action ActionFunc) *StateConfiguration[S, T] {
sc.sr.EntryActions = append(sc.sr.EntryActions, actionBehaviour[S, T]{
Action: action,
Description: newinvocationInfo(action),
Trigger: &trigger,
Expand All @@ -151,8 +151,8 @@ 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]) OnExit(action ActionFunc) *StateConfiguration[S, T] {
sc.sr.ExitActions = append(sc.sr.ExitActions, actionBehaviour[S, T]{
Action: action,
Description: newinvocationInfo(action),
})
Expand All @@ -165,7 +165,7 @@ func (sc *StateConfiguration) OnExit(action ActionFunc) *StateConfiguration {
// 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]) SubstateOf(superstate S) *StateConfiguration[S, T] {
state := sc.sr.State
// Check for accidental identical cyclic configuration
if state == superstate {
Expand All @@ -174,7 +174,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
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const (
)

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

Expand Down
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
module github.com/qmuntal/stateless

go 1.13
go 1.18

require github.com/stretchr/testify v1.4.0

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)
Loading