Merge pull request #121 from unistack-org/fsm

fsm: add state execution options
This commit is contained in:
Василий Толстов 2022-04-19 18:45:14 +03:00 committed by GitHub
commit d9be99cfde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 37 additions and 15 deletions

View File

@ -14,6 +14,8 @@ var (
// Options struct holding fsm options // Options struct holding fsm options
type Options struct { type Options struct {
// DryRun mode
DryRun bool
// Initial state // Initial state
Initial string Initial string
// HooksBefore func slice runs in order before state // HooksBefore func slice runs in order before state
@ -31,29 +33,44 @@ type HookAfterFunc func(ctx context.Context, state string, args interface{})
// Option func signature // Option func signature
type Option func(*Options) type Option func(*Options)
// StateInitial sets init state for state machine // StateOptions holds state options
func StateInitial(initial string) Option { 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) { return func(o *Options) {
o.Initial = initial o.Initial = initial
} }
} }
// StateHookBefore provides hook func slice // HookBefore provides hook func slice
func StateHookBefore(fns ...HookBeforeFunc) Option { func HookBefore(fns ...HookBeforeFunc) Option {
return func(o *Options) { return func(o *Options) {
o.HooksBefore = fns o.HooksBefore = fns
} }
} }
// StateHookAfter provides hook func slice // HookAfter provides hook func slice
func StateHookAfter(fns ...HookAfterFunc) Option { func HookAfter(fns ...HookAfterFunc) Option {
return func(o *Options) { return func(o *Options) {
o.HooksAfter = fns o.HooksAfter = fns
} }
} }
// StateFunc called on state transition and return next step and error // StateFunc called on state transition and return next step and error
type StateFunc func(ctx context.Context, args interface{}) (string, interface{}, error) type StateFunc func(ctx context.Context, args interface{}, opts...StateOption) (string, interface{}, error)
// FSM is a finite state machine // FSM is a finite state machine
type FSM struct { type FSM struct {
@ -101,6 +118,8 @@ func (f *FSM) State(state string, fn StateFunc) {
f.mu.Unlock() f.mu.Unlock()
} }
// Init initialize fsm and check states
// Start runs state machine with provided data // Start runs state machine with provided data
func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) { func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
var err error var err error
@ -115,6 +134,8 @@ func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (inte
opt(options) opt(options)
} }
sopts := []StateOption{StateDryRun(options.DryRun)}
cstate := options.Initial cstate := options.Initial
states := make(map[string]StateFunc, len(f.statesMap)) states := make(map[string]StateFunc, len(f.statesMap))
for k, v := range f.statesMap { for k, v := range f.statesMap {
@ -138,15 +159,16 @@ func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (inte
for _, fn := range options.HooksBefore { for _, fn := range options.HooksBefore {
fn(ctx, cstate, args) fn(ctx, cstate, args)
} }
nstate, args, err = fn(ctx, args) nstate, args, err = fn(ctx, args, sopts...)
for _, fn := range options.HooksAfter { for _, fn := range options.HooksAfter {
fn(ctx, cstate, args) fn(ctx, cstate, args)
} }
if err != nil { switch {
case err != nil:
return args, err return args, err
} else if nstate == StateEnd { case nstate == StateEnd:
return args, nil return args, nil
} else if nstate == "" { case nstate == "":
for idx := range f.statesOrder { for idx := range f.statesOrder {
if f.statesOrder[idx] == cstate && len(f.statesOrder) > idx+1 { if f.statesOrder[idx] == cstate && len(f.statesOrder) > idx+1 {
nstate = f.statesOrder[idx+1] nstate = f.statesOrder[idx+1]

View File

@ -16,22 +16,22 @@ func TestFSMStart(t *testing.T) {
pfa := func(_ context.Context, state string, _ interface{}) { pfa := func(_ context.Context, state string, _ interface{}) {
fmt.Fprintf(buf, "after state %s\n", state) fmt.Fprintf(buf, "after state %s\n", state)
} }
f := New(StateInitial("1"), StateHookBefore(pfb), StateHookAfter(pfa)) f := New(InitialState("1"), HookBefore(pfb), HookAfter(pfa))
f1 := func(_ context.Context, req interface{}) (string, interface{}, error) { f1 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
args := req.(map[string]interface{}) args := req.(map[string]interface{})
if v, ok := args["request"].(string); !ok || v == "" { if v, ok := args["request"].(string); !ok || v == "" {
return "", nil, fmt.Errorf("empty request") return "", nil, fmt.Errorf("empty request")
} }
return "2", map[string]interface{}{"response": "test2"}, nil return "2", map[string]interface{}{"response": "test2"}, nil
} }
f2 := func(_ context.Context, req interface{}) (string, interface{}, error) { f2 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
args := req.(map[string]interface{}) args := req.(map[string]interface{})
if v, ok := args["response"].(string); !ok || v == "" { if v, ok := args["response"].(string); !ok || v == "" {
return "", nil, fmt.Errorf("empty response") return "", nil, fmt.Errorf("empty response")
} }
return "", map[string]interface{}{"response": "test"}, nil return "", map[string]interface{}{"response": "test"}, nil
} }
f3 := func(_ context.Context, req interface{}) (string, interface{}, error) { f3 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
args := req.(map[string]interface{}) args := req.(map[string]interface{})
if v, ok := args["response"].(string); !ok || v == "" { if v, ok := args["response"].(string); !ok || v == "" {
return "", nil, fmt.Errorf("empty response") return "", nil, fmt.Errorf("empty response")