Initial
This commit is contained in:
commit
c8749e6f6b
7 changed files with 236 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.idea
|
45
README.md
Normal file
45
README.md
Normal 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
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/neonxp/workflow
|
||||||
|
|
||||||
|
go 1.13
|
BIN
images/example.png
Normal file
BIN
images/example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
7
placeer.go
Normal file
7
placeer.go
Normal 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
105
workflow.go
Normal 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
75
workflow_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue