182 lines
3.8 KiB
Go
182 lines
3.8 KiB
Go
package fsm // import "go.unistack.org/micro/v3/fsm"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidState = errors.New("does not exists")
|
|
StateEnd = "end"
|
|
)
|
|
|
|
// Options struct holding fsm options
|
|
type Options struct {
|
|
// DryRun mode
|
|
DryRun bool
|
|
// Initial state
|
|
Initial string
|
|
// HooksBefore func slice runs in order before state
|
|
HooksBefore []HookBeforeFunc
|
|
// HooksAfter func slice runs in order after state
|
|
HooksAfter []HookAfterFunc
|
|
}
|
|
|
|
// HookBeforeFunc func signature
|
|
type HookBeforeFunc func(ctx context.Context, state string, args interface{})
|
|
|
|
// HookAfterFunc func signature
|
|
type HookAfterFunc func(ctx context.Context, state string, args interface{})
|
|
|
|
// Option func signature
|
|
type Option func(*Options)
|
|
|
|
// StateOptions holds state options
|
|
type StateOptions struct {
|
|
DryRun bool
|
|
}
|
|
|
|
// StateDryRun says that state executes in dry run mode
|
|
func StateDryRun(b bool) StateOption {
|
|
return func(o *StateOptions) {
|
|
o.DryRun = b
|
|
}
|
|
}
|
|
|
|
// StateOption func signature
|
|
type StateOption func(*StateOptions)
|
|
|
|
// InitialState sets init state for state machine
|
|
func InitialState(initial string) Option {
|
|
return func(o *Options) {
|
|
o.Initial = initial
|
|
}
|
|
}
|
|
|
|
// HookBefore provides hook func slice
|
|
func HookBefore(fns ...HookBeforeFunc) Option {
|
|
return func(o *Options) {
|
|
o.HooksBefore = fns
|
|
}
|
|
}
|
|
|
|
// HookAfter provides hook func slice
|
|
func HookAfter(fns ...HookAfterFunc) Option {
|
|
return func(o *Options) {
|
|
o.HooksAfter = fns
|
|
}
|
|
}
|
|
|
|
// StateFunc called on state transition and return next step and error
|
|
type StateFunc func(ctx context.Context, args interface{}, opts...StateOption) (string, interface{}, error)
|
|
|
|
// FSM is a finite state machine
|
|
type FSM struct {
|
|
mu sync.Mutex
|
|
statesMap map[string]StateFunc
|
|
statesOrder []string
|
|
opts *Options
|
|
current string
|
|
}
|
|
|
|
// New creates a new finite state machine having the specified initial state
|
|
// with specified options
|
|
func New(opts ...Option) *FSM {
|
|
options := &Options{}
|
|
|
|
for _, opt := range opts {
|
|
opt(options)
|
|
}
|
|
|
|
return &FSM{
|
|
statesMap: map[string]StateFunc{},
|
|
opts: options,
|
|
}
|
|
}
|
|
|
|
// Current returns the current state
|
|
func (f *FSM) Current() string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.current
|
|
}
|
|
|
|
// Current returns the current state
|
|
func (f *FSM) Reset() {
|
|
f.mu.Lock()
|
|
f.current = f.opts.Initial
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
// State adds state to fsm
|
|
func (f *FSM) State(state string, fn StateFunc) {
|
|
f.mu.Lock()
|
|
f.statesMap[state] = fn
|
|
f.statesOrder = append(f.statesOrder, state)
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
// Init initialize fsm and check states
|
|
|
|
// Start runs state machine with provided data
|
|
func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
|
|
var err error
|
|
var ok bool
|
|
var fn StateFunc
|
|
var nstate string
|
|
|
|
f.mu.Lock()
|
|
options := f.opts
|
|
|
|
for _, opt := range opts {
|
|
opt(options)
|
|
}
|
|
|
|
sopts := []StateOption{StateDryRun(options.DryRun)}
|
|
|
|
cstate := options.Initial
|
|
states := make(map[string]StateFunc, len(f.statesMap))
|
|
for k, v := range f.statesMap {
|
|
states[k] = v
|
|
}
|
|
f.current = cstate
|
|
f.mu.Unlock()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
fn, ok = states[cstate]
|
|
if !ok {
|
|
return nil, fmt.Errorf(`state "%s" %w`, cstate, ErrInvalidState)
|
|
}
|
|
f.mu.Lock()
|
|
f.current = cstate
|
|
f.mu.Unlock()
|
|
for _, fn := range options.HooksBefore {
|
|
fn(ctx, cstate, args)
|
|
}
|
|
nstate, args, err = fn(ctx, args, sopts...)
|
|
for _, fn := range options.HooksAfter {
|
|
fn(ctx, cstate, args)
|
|
}
|
|
switch {
|
|
case err != nil:
|
|
return args, err
|
|
case nstate == StateEnd:
|
|
return args, nil
|
|
case nstate == "":
|
|
for idx := range f.statesOrder {
|
|
if f.statesOrder[idx] == cstate && len(f.statesOrder) > idx+1 {
|
|
nstate = f.statesOrder[idx+1]
|
|
}
|
|
}
|
|
}
|
|
cstate = nstate
|
|
}
|
|
}
|
|
}
|