Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add flowChart style to mermaid visualization #56

Merged
merged 2 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions graphviz_visualizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package fsm

import (
"bytes"
"fmt"
)

// Visualize outputs a visualization of a FSM in Graphviz format.
func Visualize(fsm *FSM) string {
var buf bytes.Buffer

// we sort the key alphabetically to have a reproducible graph output
sortedEKeys := getSortedTransitionKeys(fsm.transitions)
sortedStateKeys, _ := getSortedStates(fsm.transitions)

writeHeaderLine(&buf)
writeTransitions(&buf, fsm.current, sortedEKeys, fsm.transitions)
writeStates(&buf, sortedStateKeys)
writeFooter(&buf)

return buf.String()
}

func writeHeaderLine(buf *bytes.Buffer) {
buf.WriteString(fmt.Sprintf(`digraph fsm {`))
buf.WriteString("\n")
}

func writeTransitions(buf *bytes.Buffer, current string, sortedEKeys []eKey, transitions map[eKey]string) {
// make sure the current state is at top
for _, k := range sortedEKeys {
if k.src == current {
v := transitions[k]
buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event))
buf.WriteString("\n")
}
}
for _, k := range sortedEKeys {
if k.src != current {
v := transitions[k]
buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event))
buf.WriteString("\n")
}
}

buf.WriteString("\n")
}

func writeStates(buf *bytes.Buffer, sortedStateKeys []string) {
for _, k := range sortedStateKeys {
buf.WriteString(fmt.Sprintf(` "%s";`, k))
buf.WriteString("\n")
}
}

func writeFooter(buf *bytes.Buffer) {
buf.WriteString(fmt.Sprintln("}"))
}
38 changes: 38 additions & 0 deletions graphviz_visualizer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package fsm

import (
"fmt"
"strings"
"testing"
)

func TestGraphvizOutput(t *testing.T) {
fsmUnderTest := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
{Name: "part-close", Src: []string{"intermediate"}, Dst: "closed"},
},
Callbacks{},
)

got := Visualize(fsmUnderTest)
wanted := `
digraph fsm {
"closed" -> "open" [ label = "open" ];
"intermediate" -> "closed" [ label = "part-close" ];
"open" -> "closed" [ label = "close" ];

"closed";
"intermediate";
"open";
}`
normalizedGot := strings.ReplaceAll(got, "\n", "")
normalizedWanted := strings.ReplaceAll(wanted, "\n", "")
if normalizedGot != normalizedWanted {
t.Errorf("build graphivz graph failed. \nwanted \n%s\nand got \n%s\n", wanted, got)
fmt.Println([]byte(normalizedGot))
fmt.Println([]byte(normalizedWanted))
}
}
89 changes: 89 additions & 0 deletions mermaid_visualizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package fsm

import (
"bytes"
"fmt"
)

const highlightingColor = "#00AA00"

// MermaidDiagramType the type of the mermaid diagram type
type MermaidDiagramType string

const (
// FlowChart the diagram type for output in flowchart style (https://mermaid-js.github.io/mermaid/#/flowchart) (including current state)
FlowChart MermaidDiagramType = "flowChart"
// StateDiagram the diagram type for output in stateDiagram style (https://mermaid-js.github.io/mermaid/#/stateDiagram)
StateDiagram MermaidDiagramType = "stateDiagram"
)

// VisualizeForMermaidWithGraphType outputs a visualization of a FSM in Mermaid format as specified by the graphType.
func VisualizeForMermaidWithGraphType(fsm *FSM, graphType MermaidDiagramType) (string, error) {
switch graphType {
case FlowChart:
return visualizeForMermaidAsFlowChart(fsm), nil
case StateDiagram:
return visualizeForMermaidAsStateDiagram(fsm), nil
default:
return "", fmt.Errorf("unknown MermaidDiagramType: %s", graphType)
}
}

func visualizeForMermaidAsStateDiagram(fsm *FSM) string {
var buf bytes.Buffer

sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions)

buf.WriteString("stateDiagram\n")
buf.WriteString(fmt.Sprintln(` [*] -->`, fsm.current))

for _, k := range sortedTransitionKeys {
v := fsm.transitions[k]
buf.WriteString(fmt.Sprintf(` %s --> %s: %s`, k.src, v, k.event))
buf.WriteString("\n")
}

return buf.String()
}

// visualizeForMermaidAsFlowChart outputs a visualization of a FSM in Mermaid format (including highlighting of current state).
func visualizeForMermaidAsFlowChart(fsm *FSM) string {
var buf bytes.Buffer

sortedTransitionKeys := getSortedTransitionKeys(fsm.transitions)
sortedStates, statesToIDMap := getSortedStates(fsm.transitions)

writeFlowChartGraphType(&buf)
writeFlowChartStates(&buf, sortedStates, statesToIDMap)
writeFlowChartTransitions(&buf, fsm.transitions, sortedTransitionKeys, statesToIDMap)
writeFlowChartHightlightCurrent(&buf, fsm.current, statesToIDMap)

return buf.String()
}

func writeFlowChartGraphType(buf *bytes.Buffer) {
buf.WriteString("graph LR\n")
}

func writeFlowChartStates(buf *bytes.Buffer, sortedStates []string, statesToIDMap map[string]string) {
for _, state := range sortedStates {
buf.WriteString(fmt.Sprintf(` %s[%s]`, statesToIDMap[state], state))
buf.WriteString("\n")
}

buf.WriteString("\n")
}

func writeFlowChartTransitions(buf *bytes.Buffer, transitions map[eKey]string, sortedTransitionKeys []eKey, statesToIDMap map[string]string) {
for _, transition := range sortedTransitionKeys {
target := transitions[transition]
buf.WriteString(fmt.Sprintf(` %s --> |%s| %s`, statesToIDMap[transition.src], transition.event, statesToIDMap[target]))
buf.WriteString("\n")
}
buf.WriteString("\n")
}

func writeFlowChartHightlightCurrent(buf *bytes.Buffer, current string, statesToIDMap map[string]string) {
buf.WriteString(fmt.Sprintf(` style %s fill:%s`, statesToIDMap[current], highlightingColor))
buf.WriteString("\n")
}
43 changes: 24 additions & 19 deletions utils_test.go → mermaid_visualizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"
)

func TestGraphvizOutput(t *testing.T) {
func TestMermaidOutput(t *testing.T) {
fsmUnderTest := NewFSM(
"closed",
Events{
Expand All @@ -17,27 +17,27 @@ func TestGraphvizOutput(t *testing.T) {
Callbacks{},
)

got := Visualize(fsmUnderTest)
got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, StateDiagram)
if err != nil {
t.Errorf("got error for visualizing with type MERMAID: %s", err)
}
wanted := `
digraph fsm {
"closed" -> "open" [ label = "open" ];
"intermediate" -> "closed" [ label = "part-close" ];
"open" -> "closed" [ label = "close" ];

"closed";
"intermediate";
"open";
}`
stateDiagram
[*] --> closed
closed --> open: open
intermediate --> closed: part-close
open --> closed: close
`
normalizedGot := strings.ReplaceAll(got, "\n", "")
normalizedWanted := strings.ReplaceAll(wanted, "\n", "")
if normalizedGot != normalizedWanted {
t.Errorf("build graphivz graph failed. \nwanted \n%s\nand got \n%s\n", wanted, got)
t.Errorf("build mermaid graph failed. \nwanted \n%s\nand got \n%s\n", wanted, got)
fmt.Println([]byte(normalizedGot))
fmt.Println([]byte(normalizedWanted))
}
}

func TestMermaidOutput(t *testing.T) {
func TestMermaidFlowChartOutput(t *testing.T) {
fsmUnderTest := NewFSM(
"closed",
Events{
Expand All @@ -48,16 +48,21 @@ func TestMermaidOutput(t *testing.T) {
Callbacks{},
)

got, err := VisualizeWithType(fsmUnderTest, MERMAID)
got, err := VisualizeForMermaidWithGraphType(fsmUnderTest, FlowChart)
if err != nil {
t.Errorf("got error for visualizing with type MERMAID: %s", err)
}
wanted := `
stateDiagram
[*] --> closed
closed --> open: open
intermediate --> closed: part-close
open --> closed: close
graph LR
id0[closed]
id1[intermediate]
id2[open]

id0 --> |open| id2
id1 --> |part-close| id0
id2 --> |close| id0

style id0 fill:#00AA00
`
normalizedGot := strings.ReplaceAll(got, "\n", "")
normalizedWanted := strings.ReplaceAll(wanted, "\n", "")
Expand Down
113 changes: 0 additions & 113 deletions utils.go

This file was deleted.

Loading