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

Generics #86

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.18

- name: Test
run: go test -coverprofile=coverage.out ./...
run: make test

- name: Convert coverage
uses: jandelgado/[email protected]
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ _testmain.go

# Testing
.coverprofile
coverage.out

.vscode
.vscode
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ default: services test

.PHONY: test
test:
go test ./...
CGO_ENABLED=1 go test -benchmem -bench=. -v ./... -race -coverprofile=coverage.out -covermode=atomic && go tool cover -func=coverage.out

.PHONY: lint
lint:
Expand Down
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ package main

import (
"fmt"
"github.com/looplab/fsm"
"github.com/looplab/fsm/v2"
)

func main() {
fsm := fsm.NewFSM(
fsm := fsm.New[string, string](
"closed",
fsm.Events{
fsm.Events[string, string]{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
fsm.Callbacks{},
fsm.Callbacks[string, string]{},
)

fmt.Println(fsm.Current())
Expand Down Expand Up @@ -64,7 +64,7 @@ package main

import (
"fmt"
"github.com/looplab/fsm"
"github.com/looplab/fsm/v2"
)

type Door struct {
Expand All @@ -77,14 +77,19 @@ func NewDoor(to string) *Door {
To: to,
}

d.FSM = fsm.NewFSM(
d.FSM = fsm.New[string, string](
"closed",
fsm.Events{
fsm.Events[string, string]{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
fsm.Callbacks{
"enter_state": func(e *fsm.Event) { d.enterState(e) },
fsm.Callbacks[string, string]{
fsm.Callback[string, string]{
When: fsm.AfterAllStates,
F: func(cr *fsm.CallbackContext[MyEvent, MyState]) {
d.enterState(e)
},
},
},
)

Expand Down
151 changes: 151 additions & 0 deletions callback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) 2013 - Max Persson <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fsm

import (
"fmt"

"golang.org/x/exp/constraints"
)

// CallbackType defines at which type of Event this callback should be called.
type CallbackType string

const (
// BeforeEvent called before event E
BeforeEvent = CallbackType("before_event")
// BeforeAllEvents called before all events
BeforeAllEvents = CallbackType("before_all_events")
// AfterEvent called after event E
AfterEvent = CallbackType("after_event")
// AfterAllEvents called after all events
AfterAllEvents = CallbackType("after_all_events")
// EnterState called after entering state S
EnterState = CallbackType("enter_state")
// EnterAllStates called after entering all states
EnterAllStates = CallbackType("enter_all_states")
// LeaveState is called before leaving state S.
LeaveState = CallbackType("leave_state")
// LeaveAllStates is called before leaving all states.
LeaveAllStates = CallbackType("leave_all_states")
)

// Callback defines a condition when the callback function F should be called in certain conditions.
// The order of execution for CallbackTypes in the same event or state is:
// The concrete CallbackType has precedence over a general one, e.g.
// BeforEvent E will be fired before BeforeAllEvents.
type Callback[E constraints.Ordered, S constraints.Ordered] struct {
// When should the callback be called.
When CallbackType
// Event is the event that the callback should be called for. Only relevant for BeforeEvent and AfterEvent.
Event E
// State is the state that the callback should be called for. Only relevant for EnterState and LeaveState.
State S
// F is the callback function.
F func(*CallbackContext[E, S])
}

// Callbacks is a shorthand for defining the callbacks in New.
type Callbacks[E constraints.Ordered, S constraints.Ordered] []Callback[E, S]

// CallbackContext is the info that get passed as a reference in the callbacks.
type CallbackContext[E constraints.Ordered, S constraints.Ordered] struct {
// FSM is an reference to the current FSM.
FSM *FSM[E, S]
// Event is the event name.
Event E
// Src is the state before the transition.
Src S
// Dst is the state after the transition.
Dst S
// Err is an optional error that can be returned from a callback.
Err error
// Args is an optional list of arguments passed to the callback.
Args []any
// canceled is an internal flag set if the transition is canceled.
canceled bool
// async is an internal flag set if the transition should be asynchronous
async bool
}

// Cancel can be called in before_<EVENT> or leave_<STATE> to cancel the
// current transition before it happens. It takes an optional error, which will
// overwrite e.Err if set before.
func (ctx *CallbackContext[E, S]) Cancel(err ...error) {
ctx.canceled = true

if len(err) > 0 {
ctx.Err = err[0]
}
}

// Async can be called in leave_<STATE> to do an asynchronous state transition.
//
// The current state transition will be on hold in the old state until a final
// call to Transition is made. This will complete the transition and possibly
// call the other callbacks.
func (ctx *CallbackContext[E, S]) Async() {
ctx.async = true
}
func (cs Callbacks[E, S]) validate() error {
for i := range cs {
cb := cs[i]
err := cb.validate()
if err != nil {
return err
}
}
return nil
}

func (c *Callback[E, S]) validate() error {
var (
zeroEvent E
zeroState S
)
switch c.When {
case BeforeEvent, AfterEvent:
if c.Event == zeroEvent {
return fmt.Errorf("%v given but no event", c.When)
}
if c.State != zeroState {
return fmt.Errorf("%v given but state %v specified", c.When, c.State)
}
case BeforeAllEvents, AfterAllEvents:
if c.Event != zeroEvent {
return fmt.Errorf("%v given with event %v", c.When, c.Event)
}
if c.State != zeroState {
return fmt.Errorf("%v given with state %v", c.When, c.State)
}
case EnterState, LeaveState:
if c.State == zeroState {
return fmt.Errorf("%v given but no state", c.When)
}
if c.Event != zeroEvent {
return fmt.Errorf("%v given but event %v specified", c.When, c.Event)
}
case EnterAllStates, LeaveAllStates:
if c.State != zeroState {
return fmt.Errorf("%v given with state %v", c.When, c.State)
}
if c.Event != zeroEvent {
return fmt.Errorf("%v given with event %v", c.When, c.Event)
}
default:
return fmt.Errorf("invalid callback:%v", c)
}
return nil
}
62 changes: 62 additions & 0 deletions callback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package fsm

import "testing"

func TestCallbackValidate(t *testing.T) {
tests := []struct {
name string
cb Callback[string, string]
errString string
}{
{
name: "before_event without event",
cb: Callback[string, string]{When: BeforeEvent},
errString: "before_event given but no event",
},
{
name: "before_event with state",
cb: Callback[string, string]{When: BeforeEvent, Event: "open", State: "closed"},
errString: "before_event given but state closed specified",
},
{
name: "before_event with state",
cb: Callback[string, string]{When: BeforeAllEvents, Event: "open"},
errString: "before_all_events given with event open",
},

{
name: "before_event without event",
cb: Callback[string, string]{When: EnterState},
errString: "enter_state given but no state",
},
{
name: "before_event with state",
cb: Callback[string, string]{When: EnterState, Event: "open", State: "closed"},
errString: "enter_state given but event open specified",
},
{
name: "before_event with state",
cb: Callback[string, string]{When: EnterAllStates, State: "closed"},
errString: "enter_all_states given with state closed",
},
}

for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
err := tt.cb.validate()

if tt.errString == "" && err != nil {
t.Errorf("err:%v", err)
}
if tt.errString != "" && err == nil {
t.Errorf("errstring:%s but err is nil", tt.errString)
}

if tt.errString != "" && err.Error() != tt.errString {
t.Errorf("transition failed %v", err)
}
})
}

}
26 changes: 16 additions & 10 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@

package fsm

import (
"fmt"

"golang.org/x/exp/constraints"
)

// InvalidEventError is returned by FSM.Event() when the event cannot be called
// in the current state.
type InvalidEventError struct {
Event string
State string
type InvalidEventError[E constraints.Ordered, S constraints.Ordered] struct {
Event E
State S
}

func (e InvalidEventError) Error() string {
return "event " + e.Event + " inappropriate in current state " + e.State
func (e InvalidEventError[E, S]) Error() string {
return fmt.Sprintf("event %v inappropriate in current state %v", e.Event, e.State)
}

// UnknownEventError is returned by FSM.Event() when the event is not defined.
Expand All @@ -31,7 +37,7 @@ type UnknownEventError struct {
}

func (e UnknownEventError) Error() string {
return "event " + e.Event + " does not exist"
return fmt.Sprintf("event %s does not exist", e.Event)
}

// InTransitionError is returned by FSM.Event() when an asynchronous transition
Expand All @@ -41,7 +47,7 @@ type InTransitionError struct {
}

func (e InTransitionError) Error() string {
return "event " + e.Event + " inappropriate because previous transition did not complete"
return fmt.Sprintf("event %s inappropriate because previous transition did not complete", e.Event)
}

// NotInTransitionError is returned by FSM.Transition() when an asynchronous
Expand All @@ -60,7 +66,7 @@ type NoTransitionError struct {

func (e NoTransitionError) Error() string {
if e.Err != nil {
return "no transition with error: " + e.Err.Error()
return fmt.Sprintf("no transition with error: %s", e.Err.Error())
}
return "no transition"
}
Expand All @@ -73,7 +79,7 @@ type CanceledError struct {

func (e CanceledError) Error() string {
if e.Err != nil {
return "transition canceled with error: " + e.Err.Error()
return fmt.Sprintf("transition canceled with error: %s", e.Err.Error())
}
return "transition canceled"
}
Expand All @@ -86,7 +92,7 @@ type AsyncError struct {

func (e AsyncError) Error() string {
if e.Err != nil {
return "async started with error: " + e.Err.Error()
return fmt.Sprintf("async started with error: %s", e.Err.Error())
}
return "async started"
}
Expand Down
2 changes: 1 addition & 1 deletion errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
func TestInvalidEventError(t *testing.T) {
event := "invalid event"
state := "state"
e := InvalidEventError{Event: event, State: state}
e := InvalidEventError[string, string]{Event: event, State: state}
if e.Error() != "event "+e.Event+" inappropriate in current state "+e.State {
t.Error("InvalidEventError string mismatch")
}
Expand Down
Loading