This commit is contained in:
Alexander Kiryukhin 2020-07-15 15:26:24 +03:00
parent 09339ddae3
commit 083fa85ee5
5 changed files with 74 additions and 74 deletions

View file

@ -9,19 +9,21 @@ Simple state machine. Inspired by [Symfony Workflow](https://github.com/symfony/
```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 := NewWorkflow("Start")
w.AddTransition("Start", "A")
w.AddTransition("Start", "B")
w.AddTransition("A", "C")
w.AddTransition("B", "D")
w.AddTransition( "C", "D")
w.AddTransition("C", "Finish")
w.AddTransition("D", "Finish")
w.Can(o, "From initial to A") // == nil
w.Can(o, "From A to C") // == ErrCantApply
w.Can(o, "A") // == nil
w.Can(o, "C") // == ErrTransitionNotFound
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.GetEnabledTransitions(o) // []Place{"A", "B"}
w.Apply(o, "A") // o now at "A" place
w.GetEnabledTransitions(o) // []Place{"C"}
w.DumpToDot() // See above
```
@ -30,14 +32,14 @@ w.DumpToDot() // See above
```
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"];
Start[color="blue"]
Start -> A[label="Start → A"];
Start -> B[label="Start → B"];
A -> C[label="A → C"];
B -> D[label="B → D"];
C -> D[label="C → D"];
C -> Finish[label="C → Finish"];
D -> Finish[label="D → Finish"];
}
```

10
images/example.dot Normal file
View file

@ -0,0 +1,10 @@
digraph {
Start[color="blue"]
Start -> A[label="Start → A"];
Start -> B[label="Start → B"];
A -> C[label="A → C"];
B -> D[label="B → D"];
C -> D[label="C → D"];
C -> Finish[label="C → Finish"];
D -> Finish[label="D → Finish"];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -7,91 +7,83 @@ import (
)
var (
// ErrCantApply error if transition is not applicable to object
ErrCantApply = errors.New("cant apply transition")
// ErrTransitionNotFound error if no transition with this name
ErrTransitionNotFound = errors.New("transition not found")
)
// Workflow state machine
type Workflow struct {
transitions map[string]transition
transitions map[Place][]Place
initialPlace Place
}
// NewWorkflow returns new Workflow instance
func NewWorkflow(initialPlace Place) *Workflow {
return &Workflow{initialPlace: initialPlace, transitions: map[string]transition{}}
return &Workflow{initialPlace: initialPlace, transitions: map[Place][]Place{}}
}
// Can returns nil if transition applicable to object and error if not
func (w *Workflow) Can(obj Placeer, transition string) error {
func (w *Workflow) Can(obj Placeer, to Place) error {
currentPlace := obj.GetPlace()
if currentPlace == "" {
currentPlace = w.initialPlace
}
tr, ok := w.transitions[transition]
tr, ok := w.transitions[currentPlace]
if !ok {
return ErrTransitionNotFound
}
for _, f := range tr.From {
if f == currentPlace {
for _, f := range tr {
if f == to {
return nil
}
}
return ErrCantApply
return ErrTransitionNotFound
}
// GetEnabledTransitions return all applicable transitions for object
func (w *Workflow) GetEnabledTransitions(obj Placeer) []string {
func (w *Workflow) GetEnabledTransitions(obj Placeer) []Place {
currentPlace := obj.GetPlace()
if currentPlace == "" {
currentPlace = w.initialPlace
}
var result = make([]string, 0)
for name, t := range w.transitions {
for _, f := range t.From {
if f == currentPlace {
result = append(result, name)
break
}
}
if _, ok := w.transitions[currentPlace]; !ok {
return nil
}
return result
return w.transitions[currentPlace]
}
// Apply next state from transition to object
func (w *Workflow) Apply(obj Placeer, transition string) error {
func (w *Workflow) Apply(obj Placeer, to Place) error {
currentPlace := obj.GetPlace()
if currentPlace == "" {
currentPlace = w.initialPlace
}
tr, ok := w.transitions[transition]
tr, ok := w.transitions[currentPlace]
if !ok {
return ErrTransitionNotFound
}
for _, f := range tr.From {
if f == currentPlace {
return obj.SetPlace(tr.To)
for _, f := range tr {
if f == to {
return obj.SetPlace(to)
}
}
return ErrCantApply
return ErrTransitionNotFound
}
// AddTransition to workflow
func (w *Workflow) AddTransition(name string, from []Place, to Place) {
w.transitions[name] = transition{
From: from,
To: to,
func (w *Workflow) AddTransition(from Place, to Place) {
if _, ok := w.transitions[from]; !ok {
w.transitions[from] = []Place{}
}
w.transitions[from] = append(w.transitions[from], 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))
for from, to := range w.transitions {
for _, place := range to {
_, _ = buf.WriteString(fmt.Sprintf("%s -> %s[label=\"%s\"];\n", from, place, fmt.Sprintf("%s → %s", from, place)))
}
}
buf.WriteString("}")
@ -100,8 +92,3 @@ func (w *Workflow) DumpToDot() []byte {
// Place is one of state
type Place string
type transition struct {
From []Place
To Place
}

View file

@ -1,14 +1,18 @@
package workflow
import "testing"
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")
w := NewWorkflow("Start")
w.AddTransition("Start", "A")
w.AddTransition("Start", "B")
w.AddTransition("A", "C")
w.AddTransition("B", "D")
w.AddTransition("C", "D")
w.AddTransition("C", "Finish")
w.AddTransition("D", "Finish")
return w
}
@ -28,16 +32,16 @@ func (t *testObject) SetPlace(p Place) error {
func TestWorkflow_Can(t *testing.T) {
o := new(testObject)
w := getTestWorkflow()
if err := w.Can(o, "From initial to A"); err != nil {
if err := w.Can(o, "A"); err != nil {
t.Error("Must has transition")
}
if err := w.Can(o, "From A to C"); err == nil {
if err := w.Can(o, "C"); err == nil {
t.Error("Must has no transition")
}
}
func TestWorkflow_GetEnabledTransitions(t *testing.T) {
w:=getTestWorkflow()
w := getTestWorkflow()
o := new(testObject)
if len(w.GetEnabledTransitions(o)) != 2 {
t.Error("Must be exactly 2 transitions from initial")
@ -47,19 +51,16 @@ func TestWorkflow_GetEnabledTransitions(t *testing.T) {
func TestWorkflow_Apply(t *testing.T) {
o := new(testObject)
w := getTestWorkflow()
if err := w.Apply(o, "From initial to A"); err != nil {
if err := w.Apply(o, "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 {
if err := w.Apply(o, "Finish"); err != ErrTransitionNotFound {
t.Error("Must be transition not found")
}
if err := w.Apply(o, "From A to C"); err != nil {
if err := w.Apply(o, "C"); err != nil {
t.Error(err)
}
if o.GetPlace() != "C" {
@ -69,7 +70,7 @@ func TestWorkflow_Apply(t *testing.T) {
func TestWorkflow_DumpToDot(t *testing.T) {
dump := getTestWorkflow().DumpToDot()
if len(dump) != 288 {
t.Error("Len must be 288")
if len(dump) != 242 {
t.Errorf("Len must be 242, got %d", len(dump))
}
}