Update
This commit is contained in:
parent
09339ddae3
commit
083fa85ee5
5 changed files with 74 additions and 74 deletions
40
README.md
40
README.md
|
@ -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
10
images/example.dot
Normal 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 |
61
workflow.go
61
workflow.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue