From 945e51d470704fea953aa8c0f03b7e1fdedfbd3f Mon Sep 17 00:00:00 2001 From: Zoey McCullough Date: Sun, 13 Mar 2022 14:59:21 -0700 Subject: [PATCH 1/3] Added simple diagram and graphing infrastructure. - Creates a simple AST of the state machine which should be full serializable to anything - Added a mermaid state diagram writer with support for signal type info --- src/Liminality/DiagramWriter.cs | 85 +++++++++++ src/Liminality/Extensions.cs | 19 +++ src/Liminality/GraphBuilder.cs | 132 ++++++++++++++++++ src/Liminality/Liminality.csproj | 6 +- test/Liminality.Tests/BasicStateMachine.cs | 1 + .../Fixtures/BasicStateMachineFixture.cs | 33 +++++ test/Liminality.Tests/VisualizationTests.cs | 37 +++++ 7 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 src/Liminality/DiagramWriter.cs create mode 100644 src/Liminality/Extensions.cs create mode 100644 src/Liminality/GraphBuilder.cs create mode 100644 test/Liminality.Tests/Fixtures/BasicStateMachineFixture.cs create mode 100644 test/Liminality.Tests/VisualizationTests.cs diff --git a/src/Liminality/DiagramWriter.cs b/src/Liminality/DiagramWriter.cs new file mode 100644 index 0000000..2cfd2c9 --- /dev/null +++ b/src/Liminality/DiagramWriter.cs @@ -0,0 +1,85 @@ +using System.Text; + +namespace PSIBR.Liminality +{ + public abstract class DiagramWriter + where TStateMachine : StateMachine + { + public DiagramWriter(Graph graph!!) + { + Graph = graph; + } + + protected readonly Graph 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 : DiagramWriter + where TStateMachine : StateMachine + { + public MermaidDiagramWriter(Graph 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 + { + 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); + } + } + } +} diff --git a/src/Liminality/Extensions.cs b/src/Liminality/Extensions.cs new file mode 100644 index 0000000..1f88817 --- /dev/null +++ b/src/Liminality/Extensions.cs @@ -0,0 +1,19 @@ +namespace PSIBR.Liminality +{ + public static class Extensions + { + public static Graph GetGraph(this TStateMachine stateMachine!!) + where TStateMachine : StateMachine + { + var builder = new GraphBuilder(stateMachine); + return builder.Build(); + } + + public static Diagram GetDiagram(this Graph graph!!) + where TStateMachine : StateMachine + { + var writer = new MermaidDiagramWriter(graph); + return writer.Write(); + } + } +} diff --git a/src/Liminality/GraphBuilder.cs b/src/Liminality/GraphBuilder.cs new file mode 100644 index 0000000..2e8f50c --- /dev/null +++ b/src/Liminality/GraphBuilder.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace PSIBR.Liminality +{ + public class GraphBuilder + where TStateMachine : StateMachine + { + public GraphBuilder(TStateMachine stateMachine!!) + { + _stateMachine = stateMachine; + _graph = new Graph(_stateMachine); + } + + private readonly TStateMachine _stateMachine; + private readonly Graph _graph; + private readonly Dictionary _nodeTypeCache = new Dictionary(); + + public Graph 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); + //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!!) + { + var key = $"{graphNode.Name} {(graphNode.Condition is not null ? $":{graphNode.Condition}" : string.Empty)}"; + return key; + } + + private 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 : GraphNode + where TStateMachine : StateMachine + { + 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 + { + 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; } + } +} diff --git a/src/Liminality/Liminality.csproj b/src/Liminality/Liminality.csproj index de4b4b9..ff8a055 100644 --- a/src/Liminality/Liminality.csproj +++ b/src/Liminality/Liminality.csproj @@ -27,12 +27,12 @@ - - + + - + diff --git a/test/Liminality.Tests/BasicStateMachine.cs b/test/Liminality.Tests/BasicStateMachine.cs index f8373ae..4704673 100644 --- a/test/Liminality.Tests/BasicStateMachine.cs +++ b/test/Liminality.Tests/BasicStateMachine.cs @@ -44,5 +44,6 @@ public class Finished { } // Inputs public class Start { } public class Finish { } + public class Cancel { } } } \ No newline at end of file diff --git a/test/Liminality.Tests/Fixtures/BasicStateMachineFixture.cs b/test/Liminality.Tests/Fixtures/BasicStateMachineFixture.cs new file mode 100644 index 0000000..6dcfa6a --- /dev/null +++ b/test/Liminality.Tests/Fixtures/BasicStateMachineFixture.cs @@ -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(builder => builder + .StartsIn() + .For().On().MoveTo() + .For().On().MoveTo() + .For().On().MoveTo() + .Build()); + + x.AddTransient(); + }); + + BasicStateMachine = container.GetService() ?? throw new Exception("Container not properly setup"); + } + + public BasicStateMachine BasicStateMachine { get; } + + public void Dispose() + { + } + } +} diff --git a/test/Liminality.Tests/VisualizationTests.cs b/test/Liminality.Tests/VisualizationTests.cs new file mode 100644 index 0000000..31b22c5 --- /dev/null +++ b/test/Liminality.Tests/VisualizationTests.cs @@ -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 + { + 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); + } + } +} From c08ae9dac7aa0b8f8ae935f058339612835e47d8 Mon Sep 17 00:00:00 2001 From: AIDA Date: Sat, 16 Jul 2022 23:46:35 +0000 Subject: [PATCH 2/3] Style changes as mentioned in review --- src/Liminality/DiagramWriter.cs | 30 ++----------------- src/Liminality/Extensions.cs | 2 +- src/Liminality/GraphBuilder.cs | 7 ++--- src/Liminality/MermaidStateDiagram.cs | 28 +++++++++++++++++ .../{ContainerTests.cs => DependencyTests.cs} | 2 +- test/Liminality.Tests/VisualizationTests.cs | 1 + 6 files changed, 36 insertions(+), 34 deletions(-) create mode 100644 src/Liminality/MermaidStateDiagram.cs rename test/Liminality.Tests/{ContainerTests.cs => DependencyTests.cs} (97%) diff --git a/src/Liminality/DiagramWriter.cs b/src/Liminality/DiagramWriter.cs index 2cfd2c9..54fe98d 100644 --- a/src/Liminality/DiagramWriter.cs +++ b/src/Liminality/DiagramWriter.cs @@ -31,10 +31,10 @@ public virtual string Render() } } - public class MermaidDiagramWriter : DiagramWriter + public class MermaidStateDiagramWriter : DiagramWriter where TStateMachine : StateMachine { - public MermaidDiagramWriter(Graph graph) + public MermaidStateDiagramWriter(Graph graph) : base(graph) { } @@ -56,30 +56,4 @@ public override void WriteNode(Diagram diagram!!, GraphNode rootNode!!) } } } - - 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); - } - } - } } diff --git a/src/Liminality/Extensions.cs b/src/Liminality/Extensions.cs index 1f88817..c03730d 100644 --- a/src/Liminality/Extensions.cs +++ b/src/Liminality/Extensions.cs @@ -12,7 +12,7 @@ public static Graph GetGraph(this TStateMachine st public static Diagram GetDiagram(this Graph graph!!) where TStateMachine : StateMachine { - var writer = new MermaidDiagramWriter(graph); + var writer = new MermaidStateDiagramWriter(graph); return writer.Write(); } } diff --git a/src/Liminality/GraphBuilder.cs b/src/Liminality/GraphBuilder.cs index 2e8f50c..99a42bf 100644 --- a/src/Liminality/GraphBuilder.cs +++ b/src/Liminality/GraphBuilder.cs @@ -15,7 +15,7 @@ public GraphBuilder(TStateMachine stateMachine!!) private readonly TStateMachine _stateMachine; private readonly Graph _graph; - private readonly Dictionary _nodeTypeCache = new Dictionary(); + private readonly Dictionary _nodeTypeCache = new(); public Graph Build() { @@ -39,7 +39,6 @@ public Graph Build() private GraphNode GetOrCreateSubNode(GraphNode rootNode, Type type, Type? signalType) { - var key = MakeCacheKey(type, signalType); //If we know about this, return so we can continue //mapping the transit if (_nodeTypeCache.ContainsKey(type.Name)) @@ -76,13 +75,13 @@ private void CacheNodeType(GraphNode graphNode!!) _nodeTypeCache[key] = graphNode; } - private string MakeCacheKey(GraphNode graphNode!!) + private static string MakeCacheKey(GraphNode graphNode!!) { var key = $"{graphNode.Name} {(graphNode.Condition is not null ? $":{graphNode.Condition}" : string.Empty)}"; return key; } - private string MakeCacheKey(Type type, Type? signalType) + private static string MakeCacheKey(Type type, Type? signalType) { var key = $"{type.Name} {(signalType is not null ? $":{signalType.Name}" : string.Empty)}"; return key; diff --git a/src/Liminality/MermaidStateDiagram.cs b/src/Liminality/MermaidStateDiagram.cs new file mode 100644 index 0000000..43bcaf8 --- /dev/null +++ b/src/Liminality/MermaidStateDiagram.cs @@ -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); + } + } + } +} diff --git a/test/Liminality.Tests/ContainerTests.cs b/test/Liminality.Tests/DependencyTests.cs similarity index 97% rename from test/Liminality.Tests/ContainerTests.cs rename to test/Liminality.Tests/DependencyTests.cs index f56c105..3b9f74c 100644 --- a/test/Liminality.Tests/ContainerTests.cs +++ b/test/Liminality.Tests/DependencyTests.cs @@ -9,7 +9,7 @@ namespace PSIBR.Liminality.Tests { using static BasicStateMachine; - public class ContainerTests + public class DependencyTests { [Fact] public void CanResolveStateMachine() diff --git a/test/Liminality.Tests/VisualizationTests.cs b/test/Liminality.Tests/VisualizationTests.cs index 31b22c5..c41163d 100644 --- a/test/Liminality.Tests/VisualizationTests.cs +++ b/test/Liminality.Tests/VisualizationTests.cs @@ -33,5 +33,6 @@ public void Creating_Diagram_Succeeds() var render = diagram.Render(); Assert.NotNull(render); } + } } From db93aafb4399e2e2a1f39acab346a5e7153905d0 Mon Sep 17 00:00:00 2001 From: AIDA Date: Sun, 17 Jul 2022 00:06:16 +0000 Subject: [PATCH 3/3] Add test run task --- .vscode/tasks.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3329882..ce892c8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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 + } } ] } \ No newline at end of file