diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 59151e6..e45ecf2 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -1,5 +1,7 @@ package graph +import "slices" + func New() *Graph { return &Graph{ graph: make(map[int][]int), @@ -75,6 +77,21 @@ type Info struct { Transitions []Transition } +func (g *Graph) Nodes() []int { + var nodes []int + for node, ok := range g.validNodes { + if !ok { + continue + } + + nodes = append(nodes, node) + } + + slices.Sort(nodes) + + return nodes +} + func (g *Graph) Info() Info { var i Info for _, node := range g.nodeOrder { diff --git a/internal/graph/graph_test.go b/internal/graph/graph_test.go index 932a39a..3d9a2a0 100644 --- a/internal/graph/graph_test.go +++ b/internal/graph/graph_test.go @@ -53,4 +53,8 @@ func TestGraph(t *testing.T) { }, } require.Equal(t, expected, actual) + + actualNodes := g.Nodes() + expectedNodes := []int{1, 2, 3, 4, 5} + require.Equal(t, expectedNodes, actualNodes) } diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..7a3c9ef --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,15 @@ +package util + +import "unicode" + +func CamelCaseToSpacing(s string) string { + var result []rune + for i, r := range s { + if unicode.IsUpper(r) && i > 0 { + result = append(result, ' ') + } + + result = append(result, r) + } + return string(result) +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 0000000..a12d0f3 --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,31 @@ +package util_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/luno/workflow/internal/util" +) + +func TestCamelCaseToSpacing(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"camelCase", "camel Case"}, + {"thisIsATest", "this Is A Test"}, + {"helloWorld", "hello World"}, + {"singleWord", "single Word"}, + {"Lowercase", "Lowercase"}, // No change if no camel case + {"", ""}, // Empty string case + } + + for i, test := range tests { + t.Run(fmt.Sprintf("case %v: %v", i+1, test.input), func(t *testing.T) { + result := util.CamelCaseToSpacing(test.input) + require.Equal(t, test.expected, result) + }) + } +} diff --git a/testdata/graph-visualisation.md b/testdata/graph-visualisation.md index 574ac52..94ef3e6 100644 --- a/testdata/graph-visualisation.md +++ b/testdata/graph-visualisation.md @@ -5,7 +5,12 @@ title: Diagram of example Workflow stateDiagram-v2 direction LR - start-->middle - start-->end - middle-->end + 9: Start + 10: Middle + 11: End + + + 9-->10 + 9-->11 + 10-->11 ``` \ No newline at end of file diff --git a/visualiser.go b/visualiser.go index 4b2517c..5e050a4 100644 --- a/visualiser.go +++ b/visualiser.go @@ -5,6 +5,8 @@ import ( "os" "strings" "text/template" + + "github.com/luno/workflow/internal/util" ) // CreateDiagram creates a diagram in a md file for communicating a workflow's set of steps in an easy-to-understand @@ -39,21 +41,21 @@ func mermaidDiagram[Type any, Status StatusType](a API[Type, Status], path strin graphInfo := w.statusGraph.Info() - var starting []string + var starting []int for _, node := range graphInfo.StartingNodes { - starting = append(starting, statusToString(Status(node))) + starting = append(starting, node) } - var terminal []string + var terminal []int for _, node := range graphInfo.TerminalNodes { - terminal = append(terminal, statusToString(Status(node))) + terminal = append(terminal, node) } var transitions []MermaidTransition for _, transition := range graphInfo.Transitions { transitions = append(transitions, MermaidTransition{ - From: statusToString(Status(transition.From)), - To: statusToString(Status(transition.To)), + From: transition.From, + To: transition.To, }) } @@ -63,22 +65,25 @@ func mermaidDiagram[Type any, Status StatusType](a API[Type, Status], path strin StartingPoints: starting, TerminalPoints: terminal, Transitions: transitions, + Nodes: w.statusGraph.Nodes(), } - return template.Must(template.New("").Parse("```"+mermaidTemplate+"```")).Execute(file, mf) + return template.Must(template.New("").Funcs(map[string]any{ + "Description": description[Status], + }).Parse("```"+mermaidTemplate+"```")).Execute(file, mf) } -func statusToString[Status StatusType](s Status) string { - str := strings.ToLower(s.String()) - str = strings.Replace(str, " ", "_", -1) - return str +func description[Status StatusType](val int) string { + s := Status(val).String() + return util.CamelCaseToSpacing(s) } type MermaidFormat struct { WorkflowName string Direction MermaidDirection - StartingPoints []string - TerminalPoints []string + Nodes []int + StartingPoints []int + TerminalPoints []int Transitions []MermaidTransition } @@ -93,8 +98,8 @@ const ( ) type MermaidTransition struct { - From string - To string + From int + To int } var mermaidTemplate = `mermaid @@ -103,6 +108,10 @@ title: Diagram of {{.WorkflowName}} Workflow --- stateDiagram-v2 direction {{.Direction}} + {{range $key, $value := .Nodes }} + {{$value}}: {{Description $value}} + {{- end }} + {{range $key, $value := .Transitions }} {{$value.From}}-->{{$value.To}} {{- end }}