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

Added simple diagram and graphing infrastructure. #15

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
85 changes: 85 additions & 0 deletions src/Liminality/DiagramWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 MermaidDiagramWriter<TStateMachine> : DiagramWriter<TStateMachine>
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
where TStateMachine : StateMachine<TStateMachine>
{
public MermaidDiagramWriter(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);
}
}
}

public class MermaidStateDiagram : Diagram
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
{
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);
}
}
}
}
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 MermaidDiagramWriter<TStateMachine>(graph);
return writer.Write();
}
}
}
132 changes: 132 additions & 0 deletions src/Liminality/GraphBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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 Dictionary<string, GraphNode>();
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved

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)
{
var key = MakeCacheKey(type, signalType);
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
//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 string MakeCacheKey(GraphNode graphNode!!)
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
{
var key = $"{graphNode.Name} {(graphNode.Condition is not null ? $":{graphNode.Condition}" : string.Empty)}";
return key;
}

private string MakeCacheKey(Type type, Type? signalType)
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
aidapsibr marked this conversation as resolved.
Show resolved Hide resolved
{
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
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 { }
}
}
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()
{
}
}
}
37 changes: 37 additions & 0 deletions test/Liminality.Tests/VisualizationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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);
}
}
}