From 84655cdaf9b69b16cd73607d0ef3421dc6daa203 Mon Sep 17 00:00:00 2001 From: wowbios Date: Thu, 5 Mar 2026 15:49:48 +0300 Subject: [PATCH] Enhance README with Mermaid v11 Sequence API details and examples; add new sequence diagram actions (Box, Break, Create, Critical) to support advanced diagram features. Update HomeController and MermaidController to demonstrate new API capabilities. --- README.md | 28 +++ .../Controllers/HomeController.cs | 15 +- .../Controllers/MermaidController.cs | 15 +- .../SequenceDiagram/Actions/BoxStart.cs | 30 +++ .../SequenceDiagram/Actions/BreakStart.cs | 23 +++ .../SequenceDiagram/Actions/Create.cs | 24 +++ .../SequenceDiagram/Actions/CriticalStart.cs | 23 +++ .../SequenceDiagram/Actions/Destroy.cs | 21 +++ .../SequenceDiagram/Actions/Message.cs | 4 +- .../SequenceDiagram/Actions/Note.cs | 4 +- .../SequenceDiagram/Actions/NoteOver.cs | 4 +- .../SequenceDiagram/Actions/OptionStart.cs | 23 +++ .../SequenceDiagram/Enum/MemberType.cs | 10 +- .../SequenceDiagram/Enum/MessageType.cs | 22 ++- .../Extensions/MemberTypeExtensions.cs | 8 +- .../Extensions/MessageTypeExtensions.cs | 20 +- .../Extensions/TextExtensions.cs | 9 + .../Interfaces/ISequenceDiagram.cs | 34 +++- .../SequenceDiagram/SequenceDiagramBuilder.cs | 118 +++++++++++- .../SequenceDiagramRenderingTests.cs | 173 ++++++++++++++++++ 20 files changed, 590 insertions(+), 18 deletions(-) create mode 100644 src/FluentMermaid/SequenceDiagram/Actions/BoxStart.cs create mode 100644 src/FluentMermaid/SequenceDiagram/Actions/BreakStart.cs create mode 100644 src/FluentMermaid/SequenceDiagram/Actions/Create.cs create mode 100644 src/FluentMermaid/SequenceDiagram/Actions/CriticalStart.cs create mode 100644 src/FluentMermaid/SequenceDiagram/Actions/Destroy.cs create mode 100644 src/FluentMermaid/SequenceDiagram/Actions/OptionStart.cs create mode 100644 src/FluentMermaid/SequenceDiagram/Extensions/TextExtensions.cs create mode 100644 tests/FluentMermaid.Tests/SequenceDiagramRenderingTests.cs diff --git a/README.md b/README.md index 2128e06..7a26e8f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,34 @@ chart.EdgeStyling.LinkStyleDefault("color:blue"); var mermaid = chart.Render(); ``` +## Sequence API status (Mermaid v11) + +- Backward compatible API: existing `SequenceDiagramBuilder` calls (`AddMember`, `Message`, `Messaging`, `AltOr`, `Optional`, `Parallel`, `Rect`, `Build`) are preserved. +- New additive API: `Alt(... elseBlocks ...)`, `Break(...)`, `Critical(... options ...)`, `Box(...)`, `Create(...)`, `Destroy(...)`. +- Mermaid v11 additions: participant stereotypes (`boundary/control/entity/database/collections/queue`) and additional message arrows (bidirectional and half-arrows). + +### Quick sequence example + +```csharp +using FluentMermaid.SequenceDiagram; +using FluentMermaid.SequenceDiagram.Enum; + +var sequence = new SequenceDiagramBuilder(autoNumber: true); +var api = sequence.AddMember("API", MemberType.Control); +var db = sequence.AddMember("DB", MemberType.Database); + +sequence.Create(db); +sequence.Box("Aqua", "Persistence", d => +{ + d.Critical("Write transaction", c => c.Message(api, db, "INSERT", MessageType.SolidArrow), + ("Timeout", o => o.Break("Abort", b => b.Note(api, NoteLocation.RightOf, "rollback"))), + ("OK", o => o.NoteOver("committed", api, db))); +}); +sequence.Destroy(db); + +var mermaid = sequence.Build(); +``` + # Roadmap - [x] [Flowchart](https://mermaid.js.org/syntax/flowchart.html) - [x] [Sequence diagram](https://mermaid.js.org/syntax/sequenceDiagram.html) diff --git a/examples/aspnet-mvc/MermaidAspNetMvc/Controllers/HomeController.cs b/examples/aspnet-mvc/MermaidAspNetMvc/Controllers/HomeController.cs index edda436..fdc46b4 100644 --- a/examples/aspnet-mvc/MermaidAspNetMvc/Controllers/HomeController.cs +++ b/examples/aspnet-mvc/MermaidAspNetMvc/Controllers/HomeController.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using FluentMermaid.SequenceDiagram; using FluentMermaid.SequenceDiagram.Enum; using FluentMermaid.SequenceDiagram.Interfaces; @@ -44,12 +44,25 @@ private static string CreateSequenceDiagram() IMember bob = builder.AddMember("Bob", MemberType.Participant); bob.AddLink("Wiki", new Uri("https://wiki.contoso.com/alice")); + IMember kitchen = builder.AddMember("KitchenService", MemberType.Control); + IMember pantryDb = builder.AddMember("PantryDb", MemberType.Database); builder.AltOr( "Alice hungry", diagram => diagram.Message(alice, bob, "Wait Bob, I need something to eat", MessageType.Solid), "Alice not hungry", diagram => diagram.Message(alice, bob, "Ok, let`s go", MessageType.Solid)); + + builder.Create(pantryDb); + builder.Box("Aqua", "Dinner prep", diagram => + { + diagram.Critical( + "Order snack", + critical => critical.Message(kitchen, pantryDb, "Reserve ingredients", MessageType.SolidArrow), + ("Kitchen timeout", option => option.Break("Abort dinner", aborted => aborted.NoteOver("Order cancelled", alice, bob))), + ("Kitchen available", option => option.NoteOver("Snack is ready", alice, bob))); + }); + builder.Destroy(pantryDb); builder.NoteOver("Teenagers", alice, bob); diff --git a/examples/blazor/MermaidBlazorWebApp/Controllers/MermaidController.cs b/examples/blazor/MermaidBlazorWebApp/Controllers/MermaidController.cs index 67a60fd..7efb052 100644 --- a/examples/blazor/MermaidBlazorWebApp/Controllers/MermaidController.cs +++ b/examples/blazor/MermaidBlazorWebApp/Controllers/MermaidController.cs @@ -1,4 +1,4 @@ -using FluentMermaid.SequenceDiagram; +using FluentMermaid.SequenceDiagram; using FluentMermaid.SequenceDiagram.Enum; using FluentMermaid.SequenceDiagram.Interfaces; using Microsoft.AspNetCore.Mvc; @@ -27,12 +27,25 @@ private static string CreateSequenceDiagram() IMember bob = builder.AddMember("Bob", MemberType.Participant); bob.AddLink("Wiki", new Uri("https://wiki.contoso.com/alice")); + IMember kitchen = builder.AddMember("KitchenService", MemberType.Control); + IMember pantryDb = builder.AddMember("PantryDb", MemberType.Database); builder.AltOr( "Alice hungry", diagram => diagram.Message(alice, bob, "Wait Bob, I need something to eat", MessageType.Solid), "Alice not hungry", diagram => diagram.Message(alice, bob, "Ok, let`s go", MessageType.Solid)); + + builder.Create(pantryDb); + builder.Box("Aqua", "Dinner prep", diagram => + { + diagram.Critical( + "Order snack", + critical => critical.Message(kitchen, pantryDb, "Reserve ingredients", MessageType.SolidArrow), + ("Kitchen timeout", option => option.Break("Abort dinner", aborted => aborted.NoteOver("Order cancelled", alice, bob))), + ("Kitchen available", option => option.NoteOver("Snack is ready", alice, bob))); + }); + builder.Destroy(pantryDb); builder.NoteOver("Teenagers", alice, bob); diff --git a/src/FluentMermaid/SequenceDiagram/Actions/BoxStart.cs b/src/FluentMermaid/SequenceDiagram/Actions/BoxStart.cs new file mode 100644 index 0000000..42ea1e1 --- /dev/null +++ b/src/FluentMermaid/SequenceDiagram/Actions/BoxStart.cs @@ -0,0 +1,30 @@ +using System.Text; +using FluentMermaid.SequenceDiagram.Interfaces; + +namespace FluentMermaid.SequenceDiagram.Actions; + +internal readonly struct BoxStart : IAction +{ + public BoxStart(string? color, string? label) + { + Color = color; + Label = label; + } + + public string? Color { get; } + + public string? Label { get; } + + public void RenderTo(StringBuilder builder) + { + builder.Append("box"); + + if (!string.IsNullOrWhiteSpace(Color)) + builder.Append(' ').Append(Color); + + if (!string.IsNullOrWhiteSpace(Label)) + builder.Append(' ').Append(Label); + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/SequenceDiagram/Actions/BreakStart.cs b/src/FluentMermaid/SequenceDiagram/Actions/BreakStart.cs new file mode 100644 index 0000000..a2e7308 --- /dev/null +++ b/src/FluentMermaid/SequenceDiagram/Actions/BreakStart.cs @@ -0,0 +1,23 @@ +using System.Text; +using FluentMermaid.SequenceDiagram.Interfaces; + +namespace FluentMermaid.SequenceDiagram.Actions; + +internal readonly struct BreakStart : IAction +{ + public BreakStart(string? title) + { + Title = title; + } + + public string? Title { get; } + + public void RenderTo(StringBuilder builder) + { + builder.Append("break"); + if (!string.IsNullOrWhiteSpace(Title)) + builder.Append(' ').Append(Title); + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/SequenceDiagram/Actions/Create.cs b/src/FluentMermaid/SequenceDiagram/Actions/Create.cs new file mode 100644 index 0000000..3c10e4f --- /dev/null +++ b/src/FluentMermaid/SequenceDiagram/Actions/Create.cs @@ -0,0 +1,24 @@ +using System.Text; +using FluentMermaid.SequenceDiagram.Enum; +using FluentMermaid.SequenceDiagram.Interfaces; + +namespace FluentMermaid.SequenceDiagram.Actions; + +internal readonly struct Create : IAction +{ + public Create(IMember member) + { + Member = member; + } + + public IMember Member { get; } + + public void RenderTo(StringBuilder builder) + { + builder + .Append("create ") + .Append(Member.Type == MemberType.Actor ? "actor" : "participant") + .Append(' ') + .AppendLine(Member.Id); + } +} diff --git a/src/FluentMermaid/SequenceDiagram/Actions/CriticalStart.cs b/src/FluentMermaid/SequenceDiagram/Actions/CriticalStart.cs new file mode 100644 index 0000000..e162f4e --- /dev/null +++ b/src/FluentMermaid/SequenceDiagram/Actions/CriticalStart.cs @@ -0,0 +1,23 @@ +using System.Text; +using FluentMermaid.SequenceDiagram.Interfaces; + +namespace FluentMermaid.SequenceDiagram.Actions; + +internal readonly struct CriticalStart : IAction +{ + public CriticalStart(string? title) + { + Title = title; + } + + public string? Title { get; } + + public void RenderTo(StringBuilder builder) + { + builder.Append("critical"); + if (!string.IsNullOrWhiteSpace(Title)) + builder.Append(' ').Append(Title); + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/SequenceDiagram/Actions/Destroy.cs b/src/FluentMermaid/SequenceDiagram/Actions/Destroy.cs new file mode 100644 index 0000000..244cf09 --- /dev/null +++ b/src/FluentMermaid/SequenceDiagram/Actions/Destroy.cs @@ -0,0 +1,21 @@ +using System.Text; +using FluentMermaid.SequenceDiagram.Interfaces; + +namespace FluentMermaid.SequenceDiagram.Actions; + +internal readonly struct Destroy : IAction +{ + public Destroy(IMember member) + { + Member = member; + } + + public IMember Member { get; } + + public void RenderTo(StringBuilder builder) + { + builder + .Append("destroy ") + .AppendLine(Member.Id); + } +} diff --git a/src/FluentMermaid/SequenceDiagram/Actions/Message.cs b/src/FluentMermaid/SequenceDiagram/Actions/Message.cs index da7b84c..71ddc11 100644 --- a/src/FluentMermaid/SequenceDiagram/Actions/Message.cs +++ b/src/FluentMermaid/SequenceDiagram/Actions/Message.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using FluentMermaid.SequenceDiagram.Enum; using FluentMermaid.SequenceDiagram.Extensions; using FluentMermaid.SequenceDiagram.Interfaces; @@ -34,6 +34,6 @@ public void RenderTo(StringBuilder builder) .Append(Type.Render()) .Append(To.Id) .Append(": ") - .AppendLine(Text); + .AppendLine(Text.RenderText()); } } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/Actions/Note.cs b/src/FluentMermaid/SequenceDiagram/Actions/Note.cs index b9097e0..a9816a6 100644 --- a/src/FluentMermaid/SequenceDiagram/Actions/Note.cs +++ b/src/FluentMermaid/SequenceDiagram/Actions/Note.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using FluentMermaid.SequenceDiagram.Enum; using FluentMermaid.SequenceDiagram.Extensions; using FluentMermaid.SequenceDiagram.Interfaces; @@ -31,6 +31,6 @@ public void RenderTo(StringBuilder builder) .Append(' ') .Append(Member.Id) .Append(':') - .AppendLine(Text); + .AppendLine(Text.RenderText()); } } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/Actions/NoteOver.cs b/src/FluentMermaid/SequenceDiagram/Actions/NoteOver.cs index 594d8be..3a452de 100644 --- a/src/FluentMermaid/SequenceDiagram/Actions/NoteOver.cs +++ b/src/FluentMermaid/SequenceDiagram/Actions/NoteOver.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using FluentMermaid.SequenceDiagram.Enum; using FluentMermaid.SequenceDiagram.Extensions; using FluentMermaid.SequenceDiagram.Interfaces; @@ -26,6 +26,6 @@ public void RenderTo(StringBuilder builder) .Append(' ') .AppendJoin(',', Members.Select(m => m.Id)) .Append(':') - .AppendLine(Text); + .AppendLine(Text.RenderText()); } } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/Actions/OptionStart.cs b/src/FluentMermaid/SequenceDiagram/Actions/OptionStart.cs new file mode 100644 index 0000000..8bb9edb --- /dev/null +++ b/src/FluentMermaid/SequenceDiagram/Actions/OptionStart.cs @@ -0,0 +1,23 @@ +using System.Text; +using FluentMermaid.SequenceDiagram.Interfaces; + +namespace FluentMermaid.SequenceDiagram.Actions; + +internal readonly struct OptionStart : IAction +{ + public OptionStart(string? title) + { + Title = title; + } + + public string? Title { get; } + + public void RenderTo(StringBuilder builder) + { + builder.Append("option"); + if (!string.IsNullOrWhiteSpace(Title)) + builder.Append(' ').Append(Title); + + builder.AppendLine(); + } +} diff --git a/src/FluentMermaid/SequenceDiagram/Enum/MemberType.cs b/src/FluentMermaid/SequenceDiagram/Enum/MemberType.cs index 712da49..ab22958 100644 --- a/src/FluentMermaid/SequenceDiagram/Enum/MemberType.cs +++ b/src/FluentMermaid/SequenceDiagram/Enum/MemberType.cs @@ -1,7 +1,13 @@ -namespace FluentMermaid.SequenceDiagram.Enum; +namespace FluentMermaid.SequenceDiagram.Enum; public enum MemberType { Participant, - Actor + Actor, + Boundary, + Control, + Entity, + Database, + Collections, + Queue } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/Enum/MessageType.cs b/src/FluentMermaid/SequenceDiagram/Enum/MessageType.cs index 7a9eebe..d443d54 100644 --- a/src/FluentMermaid/SequenceDiagram/Enum/MessageType.cs +++ b/src/FluentMermaid/SequenceDiagram/Enum/MessageType.cs @@ -1,4 +1,4 @@ -namespace FluentMermaid.SequenceDiagram.Enum; +namespace FluentMermaid.SequenceDiagram.Enum; public enum MessageType { @@ -9,5 +9,23 @@ public enum MessageType SolidCross, DottedCross, SolidOpenArrow, - DottedOpenArrow + DottedOpenArrow, + SolidBidirectionalArrow, + DottedBidirectionalArrow, + SolidTopHalfArrow, + DottedTopHalfArrow, + SolidBottomHalfArrow, + DottedBottomHalfArrow, + SolidReverseTopHalfArrow, + DottedReverseTopHalfArrow, + SolidReverseBottomHalfArrow, + DottedReverseBottomHalfArrow, + SolidTopStickHalfArrow, + DottedTopStickHalfArrow, + SolidBottomStickHalfArrow, + DottedBottomStickHalfArrow, + SolidReverseTopStickHalfArrow, + DottedReverseTopStickHalfArrow, + SolidReverseBottomStickHalfArrow, + DottedReverseBottomStickHalfArrow } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/Extensions/MemberTypeExtensions.cs b/src/FluentMermaid/SequenceDiagram/Extensions/MemberTypeExtensions.cs index 309a630..68aea6e 100644 --- a/src/FluentMermaid/SequenceDiagram/Extensions/MemberTypeExtensions.cs +++ b/src/FluentMermaid/SequenceDiagram/Extensions/MemberTypeExtensions.cs @@ -1,4 +1,4 @@ -using FluentMermaid.SequenceDiagram.Enum; +using FluentMermaid.SequenceDiagram.Enum; namespace FluentMermaid.SequenceDiagram.Extensions; @@ -9,6 +9,12 @@ public static string Render(this MemberType type) { MemberType.Actor => "actor", MemberType.Participant => "participant", + MemberType.Boundary => "boundary", + MemberType.Control => "control", + MemberType.Entity => "entity", + MemberType.Database => "database", + MemberType.Collections => "collections", + MemberType.Queue => "queue", _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/Extensions/MessageTypeExtensions.cs b/src/FluentMermaid/SequenceDiagram/Extensions/MessageTypeExtensions.cs index 54a816d..c48d13b 100644 --- a/src/FluentMermaid/SequenceDiagram/Extensions/MessageTypeExtensions.cs +++ b/src/FluentMermaid/SequenceDiagram/Extensions/MessageTypeExtensions.cs @@ -1,4 +1,4 @@ -using FluentMermaid.SequenceDiagram.Enum; +using FluentMermaid.SequenceDiagram.Enum; namespace FluentMermaid.SequenceDiagram.Extensions; @@ -15,6 +15,24 @@ public static string Render(this MessageType type) MessageType.DottedCross => "--x", MessageType.SolidOpenArrow => "-)", MessageType.DottedOpenArrow => "--)", + MessageType.SolidBidirectionalArrow => "<<->>", + MessageType.DottedBidirectionalArrow => "<<-->>", + MessageType.SolidTopHalfArrow => "-|\\", + MessageType.DottedTopHalfArrow => "--|\\", + MessageType.SolidBottomHalfArrow => "-|/", + MessageType.DottedBottomHalfArrow => "--|/", + MessageType.SolidReverseTopHalfArrow => "/|-", + MessageType.DottedReverseTopHalfArrow => "/|--", + MessageType.SolidReverseBottomHalfArrow => "\\|-", + MessageType.DottedReverseBottomHalfArrow => "\\|--", + MessageType.SolidTopStickHalfArrow => "-\\\\", + MessageType.DottedTopStickHalfArrow => "--\\\\", + MessageType.SolidBottomStickHalfArrow => "-//", + MessageType.DottedBottomStickHalfArrow => "--//", + MessageType.SolidReverseTopStickHalfArrow => "//-", + MessageType.DottedReverseTopStickHalfArrow => "//--", + MessageType.SolidReverseBottomStickHalfArrow => "\\\\-", + MessageType.DottedReverseBottomStickHalfArrow => "\\\\--", _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/Extensions/TextExtensions.cs b/src/FluentMermaid/SequenceDiagram/Extensions/TextExtensions.cs new file mode 100644 index 0000000..2af9694 --- /dev/null +++ b/src/FluentMermaid/SequenceDiagram/Extensions/TextExtensions.cs @@ -0,0 +1,9 @@ +namespace FluentMermaid.SequenceDiagram.Extensions; + +internal static class TextExtensions +{ + public static string RenderText(this string text) + => text + .Replace("\r\n", "
") + .Replace("\n", "
"); +} diff --git a/src/FluentMermaid/SequenceDiagram/Interfaces/ISequenceDiagram.cs b/src/FluentMermaid/SequenceDiagram/Interfaces/ISequenceDiagram.cs index 3f281a4..8c40f34 100644 --- a/src/FluentMermaid/SequenceDiagram/Interfaces/ISequenceDiagram.cs +++ b/src/FluentMermaid/SequenceDiagram/Interfaces/ISequenceDiagram.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using FluentMermaid.SequenceDiagram.Enum; namespace FluentMermaid.SequenceDiagram.Interfaces; @@ -17,8 +17,30 @@ public interface ISequenceDiagram ISequenceDiagram AltOr(string? altTitle, Action altAction, string? orTitle, Action orAction); + ISequenceDiagram Alt( + string? altTitle, + Action altAction, + IEnumerable<(string? title, Action? action)> elseBlocks); + + ISequenceDiagram Alt( + string? altTitle, + Action altAction, + params (string? title, Action? action)[] elseBlocks); + ISequenceDiagram Optional(string? title, Action action); + ISequenceDiagram Break(string? title, Action action); + + ISequenceDiagram Critical( + string? title, + Action criticalAction, + IEnumerable<(string? title, Action? action)> options); + + ISequenceDiagram Critical( + string? title, + Action criticalAction, + params (string? title, Action? action)[] options); + ISequenceDiagram Note(IMember member, NoteLocation location, string text); ISequenceDiagram NoteOver(string text, params IMember[] members); @@ -29,5 +51,15 @@ public interface ISequenceDiagram ISequenceDiagram Rect(Color color, Action action); + ISequenceDiagram Box(string? color, string? label, Action action); + + ISequenceDiagram Box(string? label, Action action); + + ISequenceDiagram Box(Action action); + + ISequenceDiagram Create(IMember member); + + ISequenceDiagram Destroy(IMember member); + string Build(); } \ No newline at end of file diff --git a/src/FluentMermaid/SequenceDiagram/SequenceDiagramBuilder.cs b/src/FluentMermaid/SequenceDiagram/SequenceDiagramBuilder.cs index 66501ff..92b917d 100644 --- a/src/FluentMermaid/SequenceDiagram/SequenceDiagramBuilder.cs +++ b/src/FluentMermaid/SequenceDiagram/SequenceDiagramBuilder.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using System.Text; using FluentMermaid.SequenceDiagram.Actions; using FluentMermaid.SequenceDiagram.Enum; @@ -31,6 +31,7 @@ public ISequenceDiagram Message(IMember @from, IMember to, string text, MessageT { _ = @from ?? throw new ArgumentNullException(nameof(@from)); _ = to ?? throw new ArgumentNullException(nameof(to)); + _ = text ?? throw new ArgumentNullException(nameof(text)); var message = new Message(from, to, text, type); _actions.Add(message); @@ -70,15 +71,42 @@ public ISequenceDiagram AltOr(string? altTitle, Action altActi _ = altAction ?? throw new ArgumentNullException(nameof(altAction)); _ = orAction ?? throw new ArgumentNullException(nameof(orAction)); + Alt( + altTitle, + altAction, + (orTitle, orAction)); + + return this; + } + + public ISequenceDiagram Alt( + string? altTitle, + Action altAction, + IEnumerable<(string? title, Action? action)> elseBlocks) + { + _ = altAction ?? throw new ArgumentNullException(nameof(altAction)); + _ = elseBlocks ?? throw new ArgumentNullException(nameof(elseBlocks)); + _actions.Add(new AltStart(altTitle)); altAction(this); - _actions.Add(new OrStart(orTitle)); - orAction(this); + + foreach ((string? title, Action? action) in elseBlocks) + { + _actions.Add(new OrStart(title)); + action?.Invoke(this); + } + _actions.Add(new End()); return this; } + public ISequenceDiagram Alt( + string? altTitle, + Action altAction, + params (string? title, Action? action)[] elseBlocks) + => Alt(altTitle, altAction, elseBlocks.AsEnumerable()); + public ISequenceDiagram Optional(string? title, Action action) { _ = action ?? throw new ArgumentNullException(nameof(action)); @@ -90,8 +118,48 @@ public ISequenceDiagram Optional(string? title, Action action) return this; } + public ISequenceDiagram Break(string? title, Action action) + { + _ = action ?? throw new ArgumentNullException(nameof(action)); + + _actions.Add(new BreakStart(title)); + action(this); + _actions.Add(new End()); + + return this; + } + + public ISequenceDiagram Critical( + string? title, + Action criticalAction, + IEnumerable<(string? title, Action? action)> options) + { + _ = criticalAction ?? throw new ArgumentNullException(nameof(criticalAction)); + _ = options ?? throw new ArgumentNullException(nameof(options)); + + _actions.Add(new CriticalStart(title)); + criticalAction(this); + + foreach ((string? optionTitle, Action? optionAction) in options) + { + _actions.Add(new OptionStart(optionTitle)); + optionAction?.Invoke(this); + } + + _actions.Add(new End()); + + return this; + } + + public ISequenceDiagram Critical( + string? title, + Action criticalAction, + params (string? title, Action? action)[] options) + => Critical(title, criticalAction, options.AsEnumerable()); + public ISequenceDiagram Note(IMember member, NoteLocation location, string text) { + _ = member ?? throw new ArgumentNullException(nameof(member)); if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Text should not be null or empty", nameof(text)); @@ -104,6 +172,11 @@ public ISequenceDiagram NoteOver(string text, params IMember[] members) { if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Text should not be null or empty", nameof(text)); + _ = members ?? throw new ArgumentNullException(nameof(members)); + if (members.Length == 0) + throw new ArgumentException("At least one member should be provided", nameof(members)); + if (members.Any(m => m is null)) + throw new ArgumentException("Members should not contain null values", nameof(members)); _actions.Add(new NoteOver(members, text)); @@ -123,6 +196,10 @@ public ISequenceDiagram Parallel(IEnumerable<(string? title, Action action) return this; } + public ISequenceDiagram Box(string? color, string? label, Action action) + { + _ = action ?? throw new ArgumentNullException(nameof(action)); + + _actions.Add(new BoxStart(color, label)); + action(this); + _actions.Add(new End()); + + return this; + } + + public ISequenceDiagram Box(string? label, Action action) + => Box(null, label, action); + + public ISequenceDiagram Box(Action action) + => Box(null, null, action); + + public ISequenceDiagram Create(IMember member) + { + _ = member ?? throw new ArgumentNullException(nameof(member)); + + _actions.Add(new Create(member)); + + return this; + } + + public ISequenceDiagram Destroy(IMember member) + { + _ = member ?? throw new ArgumentNullException(nameof(member)); + + _actions.Add(new Destroy(member)); + + return this; + } + public string Build() { StringBuilder builder = new(); diff --git a/tests/FluentMermaid.Tests/SequenceDiagramRenderingTests.cs b/tests/FluentMermaid.Tests/SequenceDiagramRenderingTests.cs new file mode 100644 index 0000000..40bf112 --- /dev/null +++ b/tests/FluentMermaid.Tests/SequenceDiagramRenderingTests.cs @@ -0,0 +1,173 @@ +using System.Drawing; +using FluentMermaid.SequenceDiagram; +using FluentMermaid.SequenceDiagram.Enum; +using FluentMermaid.SequenceDiagram.Interfaces; + +namespace FluentMermaid.Tests; + +public class SequenceDiagramRenderingTests +{ + [Fact] + public void BackwardCompatibleApi_RendersCoreSyntax() + { + var builder = new SequenceDiagramBuilder(autoNumber: true); + + IMember alice = builder.AddMember("Alice", MemberType.Participant); + IMember bob = builder.AddMember("Bob", MemberType.Participant); + + builder.AltOr( + "Alice hungry", + d => d.Message(alice, bob, "Wait Bob, I need something to eat", MessageType.Solid), + "Alice not hungry", + d => d.Message(alice, bob, "Ok, let`s go", MessageType.Solid)); + + builder.NoteOver("Teenagers", alice, bob); + builder.Messaging(alice, bob) + .Request("Hi Bob!", MessageType.SolidArrow) + .Response("Hello Alice!", MessageType.SolidArrow) + .End(); + + string rendered = Normalize(builder.Build()); + + Assert.Contains("sequenceDiagram", rendered); + Assert.Contains("autonumber", rendered); + Assert.Contains("participant member0 as Alice", rendered); + Assert.Contains("participant member1 as Bob", rendered); + Assert.Contains("alt Alice hungry", rendered); + Assert.Contains("else Alice not hungry", rendered); + Assert.Contains("member0->member1: Wait Bob, I need something to eat", rendered); + Assert.Contains("Note over member0,member1:Teenagers", rendered); + Assert.Contains("member0->>member1: Hi Bob!", rendered); + Assert.Contains("member1->>member0: Hello Alice!", rendered); + } + + [Theory] + [InlineData(MessageType.SolidBidirectionalArrow, "<<->>")] + [InlineData(MessageType.DottedBidirectionalArrow, "<<-->>")] + [InlineData(MessageType.SolidTopHalfArrow, "-|\\")] + [InlineData(MessageType.DottedTopHalfArrow, "--|\\")] + [InlineData(MessageType.SolidBottomHalfArrow, "-|/")] + [InlineData(MessageType.DottedBottomHalfArrow, "--|/")] + [InlineData(MessageType.SolidReverseTopHalfArrow, "/|-")] + [InlineData(MessageType.DottedReverseTopHalfArrow, "/|--")] + [InlineData(MessageType.SolidReverseBottomHalfArrow, "\\|-")] + [InlineData(MessageType.DottedReverseBottomHalfArrow, "\\|--")] + [InlineData(MessageType.SolidTopStickHalfArrow, "-\\\\")] + [InlineData(MessageType.DottedTopStickHalfArrow, "--\\\\")] + [InlineData(MessageType.SolidBottomStickHalfArrow, "-//")] + [InlineData(MessageType.DottedBottomStickHalfArrow, "--//")] + [InlineData(MessageType.SolidReverseTopStickHalfArrow, "//-")] + [InlineData(MessageType.DottedReverseTopStickHalfArrow, "//--")] + [InlineData(MessageType.SolidReverseBottomStickHalfArrow, "\\\\-")] + [InlineData(MessageType.DottedReverseBottomStickHalfArrow, "\\\\--")] + public void MessageTypes_V11Arrows_AreRendered(MessageType messageType, string arrowToken) + { + var builder = new SequenceDiagramBuilder(); + IMember a = builder.AddMember("A", MemberType.Participant); + IMember b = builder.AddMember("B", MemberType.Participant); + + builder.Message(a, b, "m", messageType); + + string rendered = Normalize(builder.Build()); + Assert.Contains($"member0{arrowToken}member1: m", rendered); + } + + [Fact] + public void MemberTypes_V11Participants_AreRendered() + { + var builder = new SequenceDiagramBuilder(); + builder.AddMember("P", MemberType.Participant); + builder.AddMember("A", MemberType.Actor); + builder.AddMember("Boundary", MemberType.Boundary); + builder.AddMember("Control", MemberType.Control); + builder.AddMember("Entity", MemberType.Entity); + builder.AddMember("Database", MemberType.Database); + builder.AddMember("Collections", MemberType.Collections); + builder.AddMember("Queue", MemberType.Queue); + + string rendered = Normalize(builder.Build()); + + Assert.Contains("participant member0 as P", rendered); + Assert.Contains("actor member1 as A", rendered); + Assert.Contains("boundary member2 as Boundary", rendered); + Assert.Contains("control member3 as Control", rendered); + Assert.Contains("entity member4 as Entity", rendered); + Assert.Contains("database member5 as Database", rendered); + Assert.Contains("collections member6 as Collections", rendered); + Assert.Contains("queue member7 as Queue", rendered); + } + + [Fact] + public void AdvancedBlocks_AreRendered() + { + var builder = new SequenceDiagramBuilder(); + IMember service = builder.AddMember("Service", MemberType.Participant); + IMember db = builder.AddMember("Db", MemberType.Database); + + builder.Create(db); + builder.Box("Aqua", "Persistence", d => + { + d.Critical("Must save", c => c.Message(service, db, "Save", MessageType.SolidArrow), + ("Db timeout", o => o.Break("Abort", b => b.Note(service, NoteLocation.RightOf, "Cancelled"))), + ("Ok", o => o.NoteOver("Committed", service, db))); + }); + builder.Destroy(db); + + string rendered = Normalize(builder.Build()); + + Assert.Contains("create participant member1", rendered); + Assert.Contains("box Aqua Persistence", rendered); + Assert.Contains("critical Must save", rendered); + Assert.Contains("option Db timeout", rendered); + Assert.Contains("break Abort", rendered); + Assert.Contains("option Ok", rendered); + Assert.Contains("destroy member1", rendered); + } + + [Fact] + public void Alt_WithMultipleElseBlocks_IsRendered() + { + var builder = new SequenceDiagramBuilder(); + IMember a = builder.AddMember("A", MemberType.Participant); + IMember b = builder.AddMember("B", MemberType.Participant); + + builder.Alt( + "path1", + d => d.Message(a, b, "M1", MessageType.Solid), + ("path2", d => d.Message(a, b, "M2", MessageType.Solid)), + ("path3", d => d.Message(a, b, "M3", MessageType.Solid))); + + string rendered = Normalize(builder.Build()); + + Assert.Contains("alt path1", rendered); + Assert.Contains("else path2", rendered); + Assert.Contains("else path3", rendered); + Assert.Contains("member0->member1: M3", rendered); + } + + [Fact] + public void MultilineText_IsRenderedWithBrTag() + { + var builder = new SequenceDiagramBuilder(); + IMember a = builder.AddMember("A", MemberType.Participant); + IMember b = builder.AddMember("B", MemberType.Participant); + + builder.Message(a, b, "line1\nline2", MessageType.SolidArrow); + builder.Note(a, NoteLocation.RightOf, "note1\r\nnote2"); + + string rendered = Normalize(builder.Build()); + + Assert.Contains("member0->>member1: line1
line2", rendered); + Assert.Contains("Note right of member0:note1
note2", rendered); + } + + [Fact] + public void Parallel_WithoutBlocks_ThrowsArgumentException() + { + var builder = new SequenceDiagramBuilder(); + + Assert.Throws(() => builder.Parallel(Array.Empty<(string? title, Action? action)>())); + } + + private static string Normalize(string input) => input.Replace("\r\n", "\n"); +}