From c8749e6f6bf8f39a94791526b39fadc11360476c Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Sun, 8 Dec 2019 13:40:57 +0300 Subject: Initial --- .gitignore | 1 + README.md | 45 +++++++++++++++++++++++ go.mod | 3 ++ images/example.png | Bin 0 -> 12535 bytes placeer.go | 7 ++++ workflow.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++ workflow_test.go | 75 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 236 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 images/example.png create mode 100644 placeer.go create mode 100644 workflow.go create mode 100644 workflow_test.go 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 Binary files /dev/null and b/images/example.png 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") + } +} -- cgit v1.2.3