Skip to content

Commit

Permalink
Merge pull request #559 from sulmar/add-mermaid-graph-style
Browse files Browse the repository at this point in the history
Add mermaid graph style
  • Loading branch information
mclift authored Jun 20, 2024
2 parents d58e33b + 84a7d9c commit d6131a6
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 0 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Some useful extensions are also provided:
* Parameterised triggers
* Reentrant states
* Export to DOT graph
* Export to mermaid graph

### Hierarchical States

Expand Down Expand Up @@ -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<T>`, the `StateMachine` supports `async` entry/exit actions and so on:
Expand Down
23 changes: 23 additions & 0 deletions src/Stateless/Graph/MermaidGraph.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Stateless.Reflection;

namespace Stateless.Graph
{
/// <summary>
/// Class to generate a MermaidGraph
/// </summary>
public static class MermaidGraph
{
/// <summary>
/// Generate a Mermaid graph from the state machine info
/// </summary>
/// <param name="machineInfo"></param>
/// <returns></returns>
public static string Format(StateMachineInfo machineInfo)
{
var graph = new StateGraph(machineInfo);

return graph.ToGraph(new MermaidGraphStyle());
}

}
}
87 changes: 87 additions & 0 deletions src/Stateless/Graph/MermaidGraphStyle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Stateless.Reflection;
using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using System.Text;

namespace Stateless.Graph
{
/// <summary>
/// Class to generate a graph in mermaid format
/// </summary>
public class MermaidGraphStyle : GraphStyleBase
{
/// <summary>
/// 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.
/// </summary>
/// <param name="stateInfo">The superstate to generate text for</param>
/// <returns>Description of the superstate, and all its substates, in the desired format</returns>
public override string FormatOneCluster(SuperState stateInfo)
{
string stateRepresentationString = "";
return stateRepresentationString;
}

/// <summary>
/// Generate the text for a single decision node
/// </summary>
/// <param name="nodeName">Name of the node</param>
/// <param name="label">Label for the node</param>
/// <returns></returns>
public override string FormatOneDecisionNode(string nodeName, string label)
{
return String.Empty;
}

/// <summary>
/// Generate the text for a single state
/// </summary>
/// <param name="state">The state to generate text for</param>
/// <returns></returns>
public override string FormatOneState(State state)
{
return String.Empty;
}

/// <summary>Get the text that starts a new graph</summary>
/// <returns></returns>
public override string GetPrefix()
{
return "stateDiagram-v2";
}

/// <summary>
///
/// </summary>
/// <param name="initialState"></param>
/// <returns></returns>
public override string GetInitialTransition(StateInfo initialState)
{
return $"\r\n[*] --> {initialState}";
}



/// <summary>
///
/// </summary>
/// <param name="sourceNodeName"></param>
/// <param name="trigger"></param>
/// <param name="actions"></param>
/// <param name="destinationNodeName"></param>
/// <param name="guards"></param>
/// <returns></returns>
public override string FormatOneTransition(string sourceNodeName, string trigger, IEnumerable<string> actions, string destinationNodeName, IEnumerable<string> guards)
{
string label = trigger ?? "";

return FormatOneLine(sourceNodeName, destinationNodeName, label);
}

internal string FormatOneLine(string fromNodeName, string toNodeName, string label)
{
return $"\t{fromNodeName} --> {toNodeName} : {label}";
}
}
}
57 changes: 57 additions & 0 deletions test/Stateless.Tests/MermaidGraphFixture.cs
Original file line number Diff line number Diff line change
@@ -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, Trigger>(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, Trigger>(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, Trigger>(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);

}
}
}

0 comments on commit d6131a6

Please sign in to comment.