Skip to content
Draft
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
18 changes: 18 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "test",
"command": "dotnet",
"type": "shell",
"args": [
"test"
],
"group": "test",
"problemMatcher": "$msCompile",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
}
]
}
59 changes: 59 additions & 0 deletions src/Liminality/DiagramWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Text;

namespace PSIBR.Liminality
{
public abstract class DiagramWriter<TStateMachine>
where TStateMachine : StateMachine<TStateMachine>
{
public DiagramWriter(Graph<TStateMachine> graph!!)
{
Graph = graph;
}

protected readonly Graph<TStateMachine> Graph;
public abstract Diagram Write();

public abstract void WriteNode(Diagram diagram, GraphNode rootNode);
}

public abstract class Diagram
{
private StringBuilder _stringBuilder = new StringBuilder();
public abstract void AddTransition(GraphNode? left, GraphNode? right);
public virtual void AddSyntaxLine(string syntax)
{
_stringBuilder.AppendLine(syntax);
}

public virtual string Render()
{
return _stringBuilder.ToString();
}
}

public class MermaidStateDiagramWriter<TStateMachine> : DiagramWriter<TStateMachine>
where TStateMachine : StateMachine<TStateMachine>
{
public MermaidStateDiagramWriter(Graph<TStateMachine> graph)
: base(graph)
{
}

public override Diagram Write()
{
var diagram = new MermaidStateDiagram();
diagram.AddTransition(null, Graph);
WriteNode(diagram, Graph);
return diagram;
}

public override void WriteNode(Diagram diagram!!, GraphNode rootNode!!)
{
foreach (var node in rootNode)
{
diagram.AddTransition(rootNode, node);
WriteNode(diagram, node);
}
}
}
}
19 changes: 19 additions & 0 deletions src/Liminality/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace PSIBR.Liminality
{
public static class Extensions
{
public static Graph<TStateMachine> GetGraph<TStateMachine>(this TStateMachine stateMachine!!)
where TStateMachine : StateMachine<TStateMachine>
{
var builder = new GraphBuilder<TStateMachine>(stateMachine);
return builder.Build();
}

public static Diagram GetDiagram<TStateMachine>(this Graph<TStateMachine> graph!!)
where TStateMachine : StateMachine<TStateMachine>
{
var writer = new MermaidStateDiagramWriter<TStateMachine>(graph);
return writer.Write();
}
}
}
131 changes: 131 additions & 0 deletions src/Liminality/GraphBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace PSIBR.Liminality
{
public class GraphBuilder<TStateMachine>
where TStateMachine : StateMachine<TStateMachine>
{
public GraphBuilder(TStateMachine stateMachine!!)
{
_stateMachine = stateMachine;
_graph = new Graph<TStateMachine>(_stateMachine);
}

private readonly TStateMachine _stateMachine;
private readonly Graph<TStateMachine> _graph;
private readonly Dictionary<string, GraphNode> _nodeTypeCache = new();

public Graph<TStateMachine> Build()
{
var rootNode = new GraphNode(_stateMachine.Definition.StateMap.InitialState);
foreach (var stateMap in _stateMachine.Definition.StateMap)
{
var input = stateMap.Key;
var transition = stateMap.Value;

//Enables us to continue node transit mapping
//in many to many situations
var subNode = GetOrCreateSubNode(rootNode, input.CurrentStateType, input.SignalType);
var goesToNode = CreateNode(transition.NewStateType);
subNode.Add(goesToNode);
}

_graph.Add(rootNode);

return _graph;
}

private GraphNode GetOrCreateSubNode(GraphNode rootNode, Type type, Type? signalType)
{
//If we know about this, return so we can continue
//mapping the transit
if (_nodeTypeCache.ContainsKey(type.Name))
return _nodeTypeCache[type.Name];
//If we don't know about this but the root
//and the type are the same, this is a mapping
//of initial state -> something else
else if (rootNode.Name == type.Name && rootNode.Condition == signalType?.Name)
return rootNode;

var node = CreateNode(type, signalType);
rootNode.Add(node);
return node;
}

private GraphNode CreateNode(Type type)
{
var node = new GraphNode(type, null);
CacheNodeType(node);
return node;
}

private GraphNode CreateNode(Type type!!, Type? signalType)
{
var node = new GraphNode(type, signalType);
CacheNodeType(node);
return node;
}

private void CacheNodeType(GraphNode graphNode!!)
{
var key = MakeCacheKey(graphNode);
if (!_nodeTypeCache.ContainsKey(key))
_nodeTypeCache[key] = graphNode;
}

private static string MakeCacheKey(GraphNode graphNode!!)
{
var key = $"{graphNode.Name} {(graphNode.Condition is not null ? $":{graphNode.Condition}" : string.Empty)}";
return key;
}

private static string MakeCacheKey(Type type, Type? signalType)
{
var key = $"{type.Name} {(signalType is not null ? $":{signalType.Name}" : string.Empty)}";
return key;
}
}

[DebuggerDisplay("Name = {Name}, Goes to: {Count} other node(s)")]
public class Graph<TStateMachine> : GraphNode
where TStateMachine : StateMachine<TStateMachine>
{
private readonly TStateMachine _stateMachine;

public Graph(TStateMachine stateMachine!!)
: base(stateMachine.GetType().Name)
{
_stateMachine = stateMachine;
}
}

[DebuggerDisplay("Name = {Name}, Goes to: {Count} other node(s) when signaled with {Condition}")]
public class GraphNode : List<GraphNode>
{
public GraphNode(Type type!!)
: this(type.Name, null)
{
}

public GraphNode(Type type!!, Type? signalType)
: this(type.Name, signalType?.Name)
{
}

public GraphNode(string name!!)
: this(name, null)
{
}

public GraphNode(string name!!, string? condition)
{
Name = name;
Condition = condition;
}

public string Name { get; set; }
public string? Condition { get; set; }
}
}
6 changes: 3 additions & 3 deletions src/Liminality/Liminality.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
</PropertyGroup>

<ItemGroup>
<None Include="..\..\readme.md" Pack="true" PackagePath="\"/>
<None Include="..\..\icon.png" Pack="true" PackagePath="\"/>
<None Include="..\..\readme.md" Pack="true" PackagePath="\" />
<None Include="..\..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.*" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.*" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.*" />
</ItemGroup>

Expand Down
28 changes: 28 additions & 0 deletions src/Liminality/MermaidStateDiagram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace PSIBR.Liminality
{
public class MermaidStateDiagram : Diagram
{
private const string DiagramTypeToken = "stateDiagram-v2";
private const string Indent = " ";
private const string GoesToToken = "-->";
private const string InitialStateToken = "[*]";

public MermaidStateDiagram()
{
AddSyntaxLine($"{DiagramTypeToken}");
}

public override void AddTransition(GraphNode? left, GraphNode? right)
{
string leftSyntax = left is null ? InitialStateToken : left.Name;
string rightSyntax = right is null ? InitialStateToken : right.Name;
string signalSyntax = left?.Condition is not null ? $":{left.Condition}" : string.Empty;

if (leftSyntax != rightSyntax && !string.IsNullOrWhiteSpace(signalSyntax))
{
var syntax = $"{Indent}{leftSyntax} {GoesToToken} {rightSyntax}{signalSyntax}";
AddSyntaxLine(syntax);
}
}
}
}
1 change: 1 addition & 0 deletions test/Liminality.Tests/BasicStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ public class Finished { }
// Inputs
public class Start { }
public class Finish { }
public class Cancel { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace PSIBR.Liminality.Tests
{
using static BasicStateMachine;

public class ContainerTests
public class DependencyTests
{
[Fact]
public void CanResolveStateMachine()
Expand Down
33 changes: 33 additions & 0 deletions test/Liminality.Tests/Fixtures/BasicStateMachineFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Lamar;
using Microsoft.Extensions.DependencyInjection;
using System;
using static PSIBR.Liminality.Tests.BasicStateMachine;

namespace PSIBR.Liminality.Tests.Fixtures
{
public class BasicStateMachineFixture : IDisposable
{
public BasicStateMachineFixture()
{
var container = new Container(x =>
{
x.AddStateMachineDependencies<BasicStateMachine>(builder => builder
.StartsIn<Idle>()
.For<Idle>().On<Start>().MoveTo<InProgress>()
.For<InProgress>().On<Finish>().MoveTo<Finished>()
.For<InProgress>().On<Cancel>().MoveTo<Idle>()
.Build());

x.AddTransient<BasicStateMachine>();
});

BasicStateMachine = container.GetService<BasicStateMachine>() ?? throw new Exception("Container not properly setup");
}

public BasicStateMachine BasicStateMachine { get; }

public void Dispose()
{
}
}
}
38 changes: 38 additions & 0 deletions test/Liminality.Tests/VisualizationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using PSIBR.Liminality.Tests.Fixtures;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace PSIBR.Liminality
{
public class VisualizationTests : IClassFixture<BasicStateMachineFixture>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Things I found:

  • Nested states/signals should be represented with DeclaringType.NestedType.FinalType name format
  • re-entrant states arent displaying.

{
public VisualizationTests(BasicStateMachineFixture fixture)
{
Fixture = fixture;
}

protected readonly BasicStateMachineFixture Fixture;

[Fact]
public void Creating_Graph_Succeeds()
{
Fixture.BasicStateMachine.GetGraph();
}

[Fact]
public void Creating_Diagram_Succeeds()
{
var graph = Fixture.BasicStateMachine.GetGraph();
Assert.NotNull(graph);
var diagram = graph.GetDiagram();
Assert.NotNull(diagram);
var render = diagram.Render();
Assert.NotNull(render);
}

}
}