diff --git a/README.md b/README.md index 9a1aecd1..a0208075 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Some useful extensions are also provided: * Parameterised triggers * Reentrant states * Export to DOT graph + * Export to mermaid graph ### Hierarchical States @@ -206,6 +207,33 @@ digraph { This can then be rendered by tools that support the DOT graph language, such as the [dot command line tool](http://www.graphviz.org/doc/info/command.html) from [graphviz.org](http://www.graphviz.org) or [viz.js](https://github.com/mdaines/viz.js). See http://www.webgraphviz.com for instant gratification. Command line example: `dot -T pdf -o phoneCall.pdf phoneCall.dot` to generate a PDF file. +### Export to mermaid graph + +It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date. + +```csharp +phoneCall.Configure(State.OffHook) + .PermitIf(Trigger.CallDialled, State.Ringing); + +string graph = MermaidGraph.Format(phoneCall.GetInfo()); +``` + +The `MermaidGraph.Format()` method returns a string representation of the state machine in the [Mermaid](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams#creating-mermaid-diagrams), e.g.: + +``` +stateDiagram-v2 + [*] --> OffHook + OffHook --> Ringing : CallDialled +``` + +This can then be rendered by GitHub or [Obsidian](https://github.com/obsidianmd) + +``` mermaid +stateDiagram-v2 + [*] --> OffHook + OffHook --> Ringing : CallDialled +``` + ### Async triggers On platforms that provide `Task`, the `StateMachine` supports `async` entry/exit actions and so-on: diff --git a/src/Stateless/Graph/MermaidGraph.cs b/src/Stateless/Graph/MermaidGraph.cs new file mode 100644 index 00000000..699562ee --- /dev/null +++ b/src/Stateless/Graph/MermaidGraph.cs @@ -0,0 +1,23 @@ +using Stateless.Reflection; + +namespace Stateless.Graph +{ + /// + /// Class to generate a MermaidGraph + /// + public static class MermaidGraph + { + /// + /// Generate a Mermaid graph from the state machine info + /// + /// + /// + public static string Format(StateMachineInfo machineInfo) + { + var graph = new StateGraph(machineInfo); + + return graph.ToGraph(new MermaidGraphStyle()); + } + + } +} diff --git a/src/Stateless/Graph/MermaidGraphStyle.cs b/src/Stateless/Graph/MermaidGraphStyle.cs new file mode 100644 index 00000000..bf1fc5ef --- /dev/null +++ b/src/Stateless/Graph/MermaidGraphStyle.cs @@ -0,0 +1,87 @@ +using Stateless.Reflection; +using System; +using System.Collections.Generic; +using System.Reflection.Emit; +using System.Text; + +namespace Stateless.Graph +{ + /// + /// Class to generate a graph in mermaid format + /// + public class MermaidGraphStyle : GraphStyleBase + { + /// + /// Returns the formatted text for a single superstate and its substates. + /// For example, for DOT files this would be a subgraph containing nodes for all the substates. + /// + /// The superstate to generate text for + /// Description of the superstate, and all its substates, in the desired format + public override string FormatOneCluster(SuperState stateInfo) + { + string stateRepresentationString = ""; + return stateRepresentationString; + } + + /// + /// Generate the text for a single decision node + /// + /// Name of the node + /// Label for the node + /// + public override string FormatOneDecisionNode(string nodeName, string label) + { + return String.Empty; + } + + /// + /// Generate the text for a single state + /// + /// The state to generate text for + /// + public override string FormatOneState(State state) + { + return String.Empty; + } + + /// Get the text that starts a new graph + /// + public override string GetPrefix() + { + return "stateDiagram-v2"; + } + + /// + /// + /// + /// + /// + public override string GetInitialTransition(StateInfo initialState) + { + return $"\r\n[*] --> {initialState}"; + } + + + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public override string FormatOneTransition(string sourceNodeName, string trigger, IEnumerable actions, string destinationNodeName, IEnumerable guards) + { + string label = trigger ?? ""; + + return FormatOneLine(sourceNodeName, destinationNodeName, label); + } + + internal string FormatOneLine(string fromNodeName, string toNodeName, string label) + { + return $"\t{fromNodeName} --> {toNodeName} : {label}"; + } + } +} diff --git a/test/Stateless.Tests/MermaidGraphFixture.cs b/test/Stateless.Tests/MermaidGraphFixture.cs new file mode 100644 index 00000000..18b84404 --- /dev/null +++ b/test/Stateless.Tests/MermaidGraphFixture.cs @@ -0,0 +1,57 @@ +using Xunit; + +namespace Stateless.Tests +{ + public class MermaidGraphFixture + { + [Fact] + public void Format_InitialTransition_ShouldReturns() + { + var expected = "stateDiagram-v2\r\n[*] --> A"; + + var sm = new StateMachine(State.A); + + var result = Graph.MermaidGraph.Format(sm.GetInfo()); + + Assert.Equal(expected, result); + + } + + [Fact] + public void Format_SimpleTransition() + { + var expected = "stateDiagram-v2\r\n\tA --> B : X\r\n[*] --> A"; + + var sm = new StateMachine(State.A); + + sm.Configure(State.A) + .Permit(Trigger.X, State.B); + + var result = Graph.MermaidGraph.Format(sm.GetInfo()); + + Assert.Equal(expected, result); + + } + + [Fact] + public void TwoSimpleTransitions() + { + var expected = """ + stateDiagram-v2 + A --> B : X + A --> C : Y + """; + + var sm = new StateMachine(State.A); + + sm.Configure(State.A) + .Permit(Trigger.X, State.B) + .Permit(Trigger.Y, State.C); + + var result = Graph.MermaidGraph.Format(sm.GetInfo()); + + Assert.Equal(expected, result); + + } + } +}