This commit is contained in:
Alexander Kiryukhin 2019-12-08 13:40:57 +03:00
commit c8749e6f6b
7 changed files with 236 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.idea

45
README.md Normal file
View file

@ -0,0 +1,45 @@
# Workflow for Go
Simple state machine. Inspired by [Symfony Workflow](https://github.com/symfony/workflow).
## Example usage
```go
o := new(ObjectImplementedPlaceer)
w := NewWorkflow("initial")
w.AddTransition("From initial to A", []Place{"initial"}, "A")
w.AddTransition("From initial to B", []Place{"initial"}, "B")
w.AddTransition("From A to C", []Place{"A"}, "C")
w.AddTransition("From B,C to D", []Place{"B", "C"}, "D")
w.AddTransition("From C,D to Finish", []Place{"C", "D"}, "Finish")
w.Can(o, "From initial to A") // == nil
w.Can(o, "From A to C") // == ErrCantApply
w.GetEnabledTransitions(o) // []string{"From initial to A", "From initial to B"}
w.Apply(o, "From inital to A") // o now at "A" place
w.GetEnabledTransitions(o) // []string{"From A to C"}
w.DumpToDot() // See above
```
## Dump result
```
digraph {
initial[color="blue"];
initial -> A[label="From initial to A"];
initial -> B[label="From initial to B"];
A -> C[label="From A to C"];
B -> D[label="From B,C to D"];
C -> D[label="From B,C to D"];
C -> Finish[label="From C,D to Finish"];
D -> Finish[label="From C,D to Finish"];
}
```
Visualization:
![Workflow visualization](images/example.png)

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/neonxp/workflow
go 1.13

BIN
images/example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

7
placeer.go Normal file
View file

@ -0,0 +1,7 @@
package workflow
// Placeer interface for objects that has place and can change place
type Placeer interface {
GetPlace() Place
SetPlace(Place) error
}

105
workflow.go Normal file
View file

@ -0,0 +1,105 @@
package workflow
import (
"bytes"
"errors"
"fmt"
)
var (
ErrCantApply = errors.New("cant apply transition")
ErrTransitionNotFound = errors.New("transition not found")
)
// Workflow state machine
type Workflow struct {
transitions map[string]transition
initialPlace Place
}
// NewWorkflow returns new Workflow instance
func NewWorkflow(initialPlace Place) *Workflow {
return &Workflow{initialPlace: initialPlace, transitions: map[string]transition{}}
}
// Can returns nil if transition applicable to object and error if not
func (w *Workflow) Can(obj Placeer, transition string) error {
currentPlace := obj.GetPlace()
if currentPlace == "" {
currentPlace = w.initialPlace
}
tr, ok := w.transitions[transition]
if !ok {
return ErrTransitionNotFound
}
for _, f := range tr.From {
if f == currentPlace {
return nil
}
}
return ErrCantApply
}
// GetEnabledTransitions return all applicable transitions for object
func (w *Workflow) GetEnabledTransitions(obj Placeer) []string {
currentPlace := obj.GetPlace()
if currentPlace == "" {
currentPlace = w.initialPlace
}
result := make([]string, 0)
for name, t := range w.transitions {
for _, f := range t.From {
if f == currentPlace {
result = append(result, name)
break
}
}
}
return result
}
// Apply next state from transition to object
func (w *Workflow) Apply(obj Placeer, transition string) error {
currentPlace := obj.GetPlace()
if currentPlace == "" {
currentPlace = w.initialPlace
}
tr, ok := w.transitions[transition]
if !ok {
return ErrTransitionNotFound
}
for _, f := range tr.From {
if f == currentPlace {
return obj.SetPlace(tr.To)
}
}
return ErrCantApply
}
// AddTransition to workflow
func (w *Workflow) AddTransition(name string, from []Place, to Place) {
w.transitions[name] = transition{
From: from,
To: to,
}
}
// DumpToDot dumps transitions to Graphviz Dot format
func (w *Workflow) DumpToDot() []byte {
buf := bytes.NewBufferString(fmt.Sprintf("digraph {\n%s[color=\"blue\"]\n", w.initialPlace))
for name, t := range w.transitions {
for _, f := range t.From {
_, _ = buf.WriteString(fmt.Sprintf("%s -> %s[label=\"%s\"];\n", f, t.To, name))
}
}
buf.WriteString("}")
return buf.Bytes()
}
// Place is one of state
type Place string
type transition struct {
From []Place
To Place
}

75
workflow_test.go Normal file
View file

@ -0,0 +1,75 @@
package workflow
import "testing"
func getTestWorkflow() *Workflow {
w := NewWorkflow("initial")
w.AddTransition("From initial to A", []Place{"initial"}, "A")
w.AddTransition("From initial to B", []Place{"initial"}, "B")
w.AddTransition("From A to C", []Place{"A"}, "C")
w.AddTransition("From B,C to D", []Place{"B", "C"}, "D")
w.AddTransition("From C,D to Finish", []Place{"C", "D"}, "Finish")
return w
}
type testObject struct {
place Place
}
func (t *testObject) GetPlace() Place {
return t.place
}
func (t *testObject) SetPlace(p Place) error {
t.place = p
return nil
}
func TestWorkflow_Can(t *testing.T) {
o := new(testObject)
w := getTestWorkflow()
if err := w.Can(o, "From initial to A"); err != nil {
t.Error("Must has transition")
}
if err := w.Can(o, "From A to C"); err == nil {
t.Error("Must has no transition")
}
}
func TestWorkflow_GetEnabledTransitions(t *testing.T) {
w:=getTestWorkflow()
o := new(testObject)
if len(w.GetEnabledTransitions(o)) != 2 {
t.Error("Must be exactly 2 transitions from initial")
}
}
func TestWorkflow_Apply(t *testing.T) {
o := new(testObject)
w := getTestWorkflow()
if err := w.Apply(o, "From initial to A"); err != nil {
t.Error(err)
}
if o.GetPlace() != "A" {
t.Error("Must be at A place")
}
if err := w.Apply(o, "From B,C to D"); err != ErrCantApply {
t.Error("Must be cant move")
}
if err := w.Apply(o, "From A to D"); err != ErrTransitionNotFound {
t.Error("Must be transition not found")
}
if err := w.Apply(o, "From A to C"); err != nil {
t.Error(err)
}
if o.GetPlace() != "C" {
t.Error("Must be at C place")
}
}
func TestWorkflow_DumpToDot(t *testing.T) {
dump := getTestWorkflow().DumpToDot()
if len(dump) != 288 {
t.Error("Len must be 288")
}
}