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

Add mermaid graph style #559

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
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);

}
}
}
Loading