aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Kiryukhin <a.kiryukhin@mail.ru>2019-12-08 13:40:57 +0300
committerAlexander Kiryukhin <a.kiryukhin@mail.ru>2019-12-08 13:40:57 +0300
commitc8749e6f6bf8f39a94791526b39fadc11360476c (patch)
treee6a8cfff3afe8750b5452a31755060f07eb99502
Initial
-rw-r--r--.gitignore1
-rw-r--r--README.md45
-rw-r--r--go.mod3
-rw-r--r--images/example.pngbin0 -> 12535 bytes
-rw-r--r--placeer.go7
-rw-r--r--workflow.go105
-rw-r--r--workflow_test.go75
7 files changed, 236 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..723ef36
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..df1e04f
--- /dev/null
+++ b/README.md
@@ -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)
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b77ed42
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/neonxp/workflow
+
+go 1.13
diff --git a/images/example.png b/images/example.png
new file mode 100644
index 0000000..bcbf7d4
--- /dev/null
+++ b/images/example.png
Binary files differ
diff --git a/placeer.go b/placeer.go
new file mode 100644
index 0000000..1d6eddf
--- /dev/null
+++ b/placeer.go
@@ -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
+}
diff --git a/workflow.go b/workflow.go
new file mode 100644
index 0000000..8911cec
--- /dev/null
+++ b/workflow.go
@@ -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
+}
diff --git a/workflow_test.go b/workflow_test.go
new file mode 100644
index 0000000..cb4fe3b
--- /dev/null
+++ b/workflow_test.go
@@ -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")
+ }
+}