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