From 71de51be4ecdaf61833cba3ead2da616d14465d0 Mon Sep 17 00:00:00 2001 From: KIRCHSTH Date: Tue, 22 Jun 2021 22:41:25 +0200 Subject: [PATCH 1/7] - Update nuget references to Structurizr.* 1.0.0 - Update from netcoreapp 1.1 to netcoreapp 2.1 - Update from netstandard 1.3 to netstandard 2.0 - Structurizr.AdrTools: link use "%2F" instead of "/" (sync with java impl.) - C4PlantUML: -- Updated that it works with new calculated CanonicalName -- Test updated with new sample -- RelationshipView stores "DirectionValues" in Position (properties are not available anymore) -- !!! generated PlantUML itself is not updated (no new styles, ...) --- .../Structurizr.ActiveDirectory.csproj | 4 +- .../AdrTools/AdrToolsImporterTests.cs | 6 +- .../Structurizr.AdrTools.Tests.csproj | 2 +- .../AdrTools/AdrToolsImporter.cs | 8 +- .../Structurizr.AdrTools.csproj | 4 +- .../Structurizr.Analysis.csproj | 4 +- .../Structurizr.Annotations.csproj | 12 ++ .../Structurizr.Cecil.Examples.csproj | 2 +- Structurizr.Cecil/Structurizr.Cecil.csproj | 4 +- .../Structurizr.Examples.csproj | 4 +- .../IO/C4PlantUML/C4PlantUmlWriterTests.cs | 153 +++++++++--------- .../IO/PlantUML/PlantUMLWriterTests.cs | 4 +- .../Structurizr.PlantUML.Tests.csproj | 2 +- .../ModelExtensions/ModelExtensions.cs | 36 +++++ .../RelationshipViewExtensions.cs | 29 +++- .../IO/C4PlantUML/PlantUMLWriterBase.cs | 21 ++- .../Structurizr.PlantUML.csproj | 4 +- .../Structurizr.Reflection.Examples.csproj | 2 +- .../Structurizr.Reflection.csproj | 2 +- 19 files changed, 195 insertions(+), 108 deletions(-) create mode 100644 Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs diff --git a/Structurizr.ActiveDirectory/Structurizr.ActiveDirectory.csproj b/Structurizr.ActiveDirectory/Structurizr.ActiveDirectory.csproj index 1d7b98d..581ff58 100644 --- a/Structurizr.ActiveDirectory/Structurizr.ActiveDirectory.csproj +++ b/Structurizr.ActiveDirectory/Structurizr.ActiveDirectory.csproj @@ -8,12 +8,12 @@ Exe - netcoreapp1.1 + netcoreapp2.1 - + \ No newline at end of file diff --git a/Structurizr.AdrTools.Tests/AdrTools/AdrToolsImporterTests.cs b/Structurizr.AdrTools.Tests/AdrTools/AdrToolsImporterTests.cs index b0122bf..f3849c7 100644 --- a/Structurizr.AdrTools.Tests/AdrTools/AdrToolsImporterTests.cs +++ b/Structurizr.AdrTools.Tests/AdrTools/AdrToolsImporterTests.cs @@ -106,7 +106,8 @@ public void Test_ImportArchitectureDecisionRecords_RewritesLinksBetweenADRsWhenT importer.ImportArchitectureDecisionRecords(); Decision decision5 = _documentation.Decisions.Where(d => d.Id == "5").First(); - Assert.True(decision5.Content.Contains("Amended by [9. Help scripts](#/:9)")); + // sync with java impl %2F instead of / + Assert.True(decision5.Content.Contains("Amended by [9. Help scripts](#%2F:9)")); } [Fact] @@ -127,7 +128,8 @@ public void Test_ImportArchitectureDecisionRecords_SupportsTheIncorrectSpellingO Decision decision4 = _documentation.Decisions.Where(d => d.Id == "4").First(); Assert.Equal(DecisionStatus.Superseded, decision4.Status); - Assert.True(decision4.Content.Contains("Superceded by [10. AsciiDoc format](#/:10)")); + // sync with java impl %2F instead of / + Assert.True(decision4.Content.Contains("Superceded by [10. AsciiDoc format](#%2F:10)")); } } diff --git a/Structurizr.AdrTools.Tests/Structurizr.AdrTools.Tests.csproj b/Structurizr.AdrTools.Tests/Structurizr.AdrTools.Tests.csproj index 9f13f19..e939dbb 100644 --- a/Structurizr.AdrTools.Tests/Structurizr.AdrTools.Tests.csproj +++ b/Structurizr.AdrTools.Tests/Structurizr.AdrTools.Tests.csproj @@ -1,6 +1,6 @@ - netcoreapp1.1 + netcoreapp2.1 false diff --git a/Structurizr.AdrTools/AdrTools/AdrToolsImporter.cs b/Structurizr.AdrTools/AdrTools/AdrToolsImporter.cs index 0bfa2db..81676fb 100644 --- a/Structurizr.AdrTools/AdrTools/AdrToolsImporter.cs +++ b/Structurizr.AdrTools/AdrTools/AdrToolsImporter.cs @@ -90,11 +90,15 @@ public ISet ImportArchitectureDecisionRecords(SoftwareSystem softwareS private string CalculateUrl(SoftwareSystem softwareSystem, string id) { if (softwareSystem == null) { - return "#/:" + UrlEncode(id); + // sync with java impl + return "#" + UrlEncode("/") + ":" + UrlEncode(id); } else { - return "#" + UrlEncode(softwareSystem.CanonicalName) + ":" + UrlEncode(id); + //CanonicalName impl changed: old "/", new "SoftwareSystem://" (CanonicalNameGenerator.SoftwareSystemType) + var name = softwareSystem.CanonicalName; + name = name.Substring("SoftwareSystem:/".Length); // last "/" is reused + return "#" + UrlEncode(name) + ":" + UrlEncode(id); } } diff --git a/Structurizr.AdrTools/Structurizr.AdrTools.csproj b/Structurizr.AdrTools/Structurizr.AdrTools.csproj index 38c9414..c9c8733 100644 --- a/Structurizr.AdrTools/Structurizr.AdrTools.csproj +++ b/Structurizr.AdrTools/Structurizr.AdrTools.csproj @@ -7,11 +7,11 @@ - netstandard1.3;net45 + netstandard2.0;net45 - + diff --git a/Structurizr.Analysis/Structurizr.Analysis.csproj b/Structurizr.Analysis/Structurizr.Analysis.csproj index 3bfbc40..f536c8b 100644 --- a/Structurizr.Analysis/Structurizr.Analysis.csproj +++ b/Structurizr.Analysis/Structurizr.Analysis.csproj @@ -7,11 +7,11 @@ - netstandard1.3;net45 + netstandard2.0;net45 - + \ No newline at end of file diff --git a/Structurizr.Annotations/Structurizr.Annotations.csproj b/Structurizr.Annotations/Structurizr.Annotations.csproj index 2395b0c..b0aab59 100644 --- a/Structurizr.Annotations/Structurizr.Annotations.csproj +++ b/Structurizr.Annotations/Structurizr.Annotations.csproj @@ -17,7 +17,12 @@ + + netstandard2.0;net20 bin\$(Configuration)\$(TargetFramework)\Structurizr.Annotations.xml 1.6.0 @@ -48,4 +53,11 @@ 4.1.0 + + + + 4.1.0 + + + diff --git a/Structurizr.Cecil.Examples/Structurizr.Cecil.Examples.csproj b/Structurizr.Cecil.Examples/Structurizr.Cecil.Examples.csproj index 9d7a008..2a94992 100644 --- a/Structurizr.Cecil.Examples/Structurizr.Cecil.Examples.csproj +++ b/Structurizr.Cecil.Examples/Structurizr.Cecil.Examples.csproj @@ -8,7 +8,7 @@ - + diff --git a/Structurizr.Cecil/Structurizr.Cecil.csproj b/Structurizr.Cecil/Structurizr.Cecil.csproj index ffa5ec8..4a98d97 100644 --- a/Structurizr.Cecil/Structurizr.Cecil.csproj +++ b/Structurizr.Cecil/Structurizr.Cecil.csproj @@ -16,12 +16,12 @@ - netstandard1.3;net45 + netstandard2.0;net45 - + diff --git a/Structurizr.Examples/Structurizr.Examples.csproj b/Structurizr.Examples/Structurizr.Examples.csproj index 6de67c5..29b95ca 100644 --- a/Structurizr.Examples/Structurizr.Examples.csproj +++ b/Structurizr.Examples/Structurizr.Examples.csproj @@ -1,12 +1,12 @@  Exe - netcoreapp1.1 + netcoreapp2.1 Structurizr.Examples.PlantUML false - + diff --git a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs index 36fc4f1..1fa0d38 100644 --- a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs +++ b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs @@ -383,23 +383,23 @@ legend right !define Node(e_alias, e_label, e_techn) rectangle ""==e_label\n[e_techn]"" <> as e_alias ' Structurizr.DeploymentView: DevelopmentDeployment -title Internet Banking System - Deployment +title Internet Banking System - Deployment - Development LAYOUT_WITH_LEGEND() -Node(Deployment__Development__DeveloperLaptop__389f399, ""Developer Laptop"", ""Microsoft Windows 10 or Apple \nmacOS"") { - Node(Deployment__Development__DeveloperLaptop__DockerContainerWebServer__1b73d2e, ""Docker Container - Web Server"", ""Docker"") { - Node(Deployment__Development__DeveloperLaptop__DockerContainerWebServer__ApacheTomcat__1cc9f55, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { +Node(DeveloperLaptop__389f399, ""Developer Laptop"", ""Microsoft Windows 10 or Apple \nmacOS"") { + Node(DockerContainerWebServer__1b73d2e, ""Docker Container - Web Server"", ""Docker"") { + Node(ApacheTomcat__1cc9f55, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { Container(InternetBankingSystem__WebApplication1__28f79f6, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") Container(InternetBankingSystem__APIApplication1__1f227f4, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."") } } - Node(Deployment__Development__DeveloperLaptop__DockerContainerDatabaseServer__2eae566, ""Docker Container - Database Server"", ""Docker"") { - Node(Deployment__Development__DeveloperLaptop__DockerContainerDatabaseServer__DatabaseServer__24d13de, ""Database Server"", ""Oracle 12c"") { + Node(DockerContainerDatabaseServer__2eae566, ""Docker Container - Database Server"", ""Docker"") { + Node(DatabaseServer__24d13de, ""Database Server"", ""Oracle 12c"") { ContainerDb(InternetBankingSystem__Database1__3296ca6, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") } } - Node(Deployment__Development__DeveloperLaptop__WebBrowser__3930fd, ""Web Browser"", ""Google Chrome, Mozilla \nFirefox, Apple Safari or \nMicrosoft Edge"") { + Node(WebBrowser__3930fd, ""Web Browser"", ""Google Chrome, Mozilla \nFirefox, Apple Safari or \nMicrosoft Edge"") { Container(InternetBankingSystem__SinglePageApplication1__bbe85d, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") } } @@ -450,46 +450,46 @@ legend right !define Node(e_alias, e_label, e_techn) rectangle ""==e_label\n[e_techn]"" <> as e_alias ' Structurizr.DeploymentView: LiveDeployment -title Internet Banking System - Deployment +title Internet Banking System - Deployment - Live LAYOUT_WITH_LEGEND() -Node(Deployment__Live__BigBankplc__3ffe15e, ""Big Bank plc"", ""Big Bank plc data center"") { - Node(Deployment__Live__BigBankplc__bigbankweb***__3f92e18, ""bigbank-web*** (x4)"", ""Ubuntu 16.04 LTS"") { - Node(Deployment__Live__BigBankplc__bigbankweb***__ApacheTomcat__27b4383, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(InternetBankingSystem__WebApplication2__1720850, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") +Node(BigBankplc__3ffe15e, ""Big Bank plc"", ""Big Bank plc data center"") { + Node(bigbankweb***__3f92e18, ""bigbank-web*** (x4)"", ""Ubuntu 16.04 LTS"") { + Node(ApacheTomcat__27b4383, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { + Container(InternetBankingSystem__WebApplication1__1720850, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") } } - Node(Deployment__Live__BigBankplc__bigbankapi***__263d9e8, ""bigbank-api*** (x8)"", ""Ubuntu 16.04 LTS"") { - Node(Deployment__Live__BigBankplc__bigbankapi***__ApacheTomcat__3b84ab, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(InternetBankingSystem__APIApplication2__1408a33, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."") + Node(bigbankapi***__263d9e8, ""bigbank-api*** (x8)"", ""Ubuntu 16.04 LTS"") { + Node(ApacheTomcat__3b84ab, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { + Container(InternetBankingSystem__APIApplication1__1408a33, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."") } } - Node(Deployment__Live__BigBankplc__bigbankdb01__35ec592, ""bigbank-db01"", ""Ubuntu 16.04 LTS"") { - Node(Deployment__Live__BigBankplc__bigbankdb01__OraclePrimary__19fd8f, ""Oracle - Primary"", ""Oracle 12c"") { - ContainerDb(InternetBankingSystem__Database2__1c974ec, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") + Node(bigbankdb01__35ec592, ""bigbank-db01"", ""Ubuntu 16.04 LTS"") { + Node(OraclePrimary__19fd8f, ""Oracle - Primary"", ""Oracle 12c"") { + ContainerDb(InternetBankingSystem__Database1__1c974ec, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") } } - Node(Deployment__Live__BigBankplc__bigbankdb02__1db08a2, ""bigbank-db02"", ""Ubuntu 16.04 LTS"") { - Node(Deployment__Live__BigBankplc__bigbankdb02__OracleSecondary__1c4ec22, ""Oracle - Secondary"", ""Oracle 12c"") { - ContainerDb(InternetBankingSystem__Database3__d89394, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") + Node(bigbankdb02__1db08a2, ""bigbank-db02"", ""Ubuntu 16.04 LTS"") { + Node(OracleSecondary__1c4ec22, ""Oracle - Secondary"", ""Oracle 12c"") { + ContainerDb(InternetBankingSystem__Database1__d89394, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") } } } -Node(Deployment__Live__Customer'scomputer__2510bf3, ""Customer's computer"", ""Microsoft Windows or Apple \nmacOS"") { - Node(Deployment__Live__Customer'scomputer__WebBrowser__ba951, ""Web Browser"", ""Google Chrome, Mozilla \nFirefox, Apple Safari or \nMicrosoft Edge"") { - Container(InternetBankingSystem__SinglePageApplication2__298b31c, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") +Node(Customer'scomputer__2510bf3, ""Customer's computer"", ""Microsoft Windows or Apple \nmacOS"") { + Node(WebBrowser__ba951, ""Web Browser"", ""Google Chrome, Mozilla \nFirefox, Apple Safari or \nMicrosoft Edge"") { + Container(InternetBankingSystem__SinglePageApplication1__298b31c, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") } } -Node(Deployment__Live__Customer'smobiledevice__1d6bcb6, ""Customer's mobile device"", ""Apple iOS or Android"") { +Node(Customer'smobiledevice__1d6bcb6, ""Customer's mobile device"", ""Apple iOS or Android"") { Container(InternetBankingSystem__MobileApp1__d004b3, ""Mobile App"", ""Xamarin"", ""Provides a limited subset of the Internet banking functionality to customers via their mobile device."") } -Rel(InternetBankingSystem__APIApplication2__1408a33, InternetBankingSystem__Database2__1c974ec, ""Reads from and writes to"", ""JDBC"") -Rel(InternetBankingSystem__APIApplication2__1408a33, InternetBankingSystem__Database3__d89394, ""Reads from and writes to"", ""JDBC"") -Rel(InternetBankingSystem__MobileApp1__d004b3, InternetBankingSystem__APIApplication2__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(InternetBankingSystem__SinglePageApplication2__298b31c, InternetBankingSystem__APIApplication2__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") -Rel_Up(InternetBankingSystem__WebApplication2__1720850, InternetBankingSystem__SinglePageApplication2__298b31c, ""Delivers to the customer's web browser"") -Rel_Left(Deployment__Live__BigBankplc__bigbankdb01__OraclePrimary__19fd8f, Deployment__Live__BigBankplc__bigbankdb02__OracleSecondary__1c4ec22, ""Replicates data to"") +Rel(InternetBankingSystem__APIApplication1__1408a33, InternetBankingSystem__Database1__1c974ec, ""Reads from and writes to"", ""JDBC"") +Rel(InternetBankingSystem__APIApplication1__1408a33, InternetBankingSystem__Database1__d89394, ""Reads from and writes to"", ""JDBC"") +Rel(InternetBankingSystem__MobileApp1__d004b3, InternetBankingSystem__APIApplication1__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") +Rel_Left(OraclePrimary__19fd8f, OracleSecondary__1c4ec22, ""Replicates data to"") +Rel(InternetBankingSystem__SinglePageApplication1__298b31c, InternetBankingSystem__APIApplication1__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") +Rel_Up(InternetBankingSystem__WebApplication1__1720850, InternetBankingSystem__SinglePageApplication1__298b31c, ""Delivers to the customer's web browser"") @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -504,9 +504,9 @@ private void AddLayoutDetails(Workspace workspace) // all SystemLandscapeView, SystemContext, ... (update relation): // Rel_Up(EmailSystem, PersonalBankingCustomer, ""Sends e-mails to"") // Rel_Right(InternetBankingSystem, EmailSystem, ""Sends e-mail using"") - var emailSystem = workspace.Model.GetElementWithCanonicalName("/E-mail System"); - var personalBankingCustomer = workspace.Model.GetElementWithCanonicalName("/Personal Banking Customer"); - var internetBankingSystem = workspace.Model.GetElementWithCanonicalName("/Internet Banking System"); + var emailSystem = workspace.Model.GetElementWithCanonicalOrStaticalName("SoftwareSystem://E-mail System"); + var personalBankingCustomer = workspace.Model.GetElementWithCanonicalOrStaticalName("Person://Personal Banking Customer"); + var internetBankingSystem = workspace.Model.GetElementWithCanonicalOrStaticalName("SoftwareSystem://Internet Banking System"); var systemLandscapeView = workspace.Views.SystemLandscapeViews.First(); systemLandscapeView.Relationships @@ -519,7 +519,7 @@ private void AddLayoutDetails(Workspace workspace) .SetDirection(DirectionValues.Right); // but only SystemLandscapeView should use Down relations, therefore add the tags relation view specific (via AddViewTags) - var mainframeBankingSystem = workspace.Model.GetElementWithCanonicalName("/Mainframe Banking System"); + var mainframeBankingSystem = workspace.Model.GetElementWithCanonicalOrStaticalName("SoftwareSystem://Mainframe Banking System"); foreach (var relationshipView in systemLandscapeView.Relationships .Where(rv => rv.Relationship.DestinationId == mainframeBankingSystem.Id)) { @@ -537,12 +537,12 @@ private void AddLayoutDetails(Workspace workspace) // Rel_Right(InternetBankingSystem__SinglePageApplication, InternetBankingSystem__APIApplication__SignInController, ...) // Rel_Right(InternetBankingSystem__APIApplication__SecurityComponent, InternetBankingSystem__Database, ...) var singlePageApplication = - workspace.Model.GetElementWithCanonicalName("/Internet Banking System/Single-Page Application"); + workspace.Model.GetElementWithCanonicalOrStaticalName("Container://Internet Banking System.Single-Page Application"); var signInController = - workspace.Model.GetElementWithCanonicalName("/Internet Banking System/API Application/Sign In Controller"); + workspace.Model.GetElementWithCanonicalOrStaticalName("Component://Internet Banking System.API Application.Sign In Controller"); var securityComponent = - workspace.Model.GetElementWithCanonicalName("/Internet Banking System/API Application/Security Component"); - var database = workspace.Model.GetElementWithCanonicalName("/Internet Banking System/Database") as Container; + workspace.Model.GetElementWithCanonicalOrStaticalName("Component://Internet Banking System.API Application.Security Component"); + var database = workspace.Model.GetElementWithCanonicalOrStaticalName("Container://Internet Banking System.Database") as Container; database.SetIsDatabase(true); var dynamicView = workspace.Views.DynamicViews.First(); dynamicView.Relationships @@ -555,10 +555,8 @@ private void AddLayoutDetails(Workspace workspace) // ContainerView // Rel_Up(InternetBankingSystem__WebApplication, InternetBankingSystem__SinglePageApplication, "Delivers to the customer's web browser") - var apiApplication = workspace.Model.GetElementWithCanonicalName("/Internet Banking System/API Application"); - var webApplication = workspace.Model.GetElementWithCanonicalName("/Internet Banking System/Web Application"); - - + var apiApplication = workspace.Model.GetElementWithCanonicalOrStaticalName("Container://Internet Banking System.API Application"); + var webApplication = workspace.Model.GetElementWithCanonicalOrStaticalName("Container://Internet Banking System.Web Application"); var containerView = workspace.Views.ContainerViews.First(); containerView.Relationships .First(r => r.Relationship.SourceId == apiApplication.Id && @@ -584,29 +582,34 @@ private void AddLayoutDetails(Workspace workspace) // DeploymentView´(with already copied relations): DevelopmentDeployment, LiveDeployment // Rel_Up(InternetBankingSystem__WebApplication1, InternetBankingSystem__SinglePageApplication1, "Delivers to the customer's web browser") // Rel_Up(InternetBankingSystem__WebApplication2, InternetBankingSystem__SinglePageApplication2, "Delivers to the customer's web browser") - // Rel_Left(Deployment__Live__BigBankplc__bigbankdb01__OraclePrimary, Deployment__Live__BigBankplc__bigbankdb02__OracleSecondary, "Replicates data to") - var webApplication1 = workspace.Model.GetElementWithCanonicalName("/Internet Banking System/Web Application[1]"); - var webApplication2 = workspace.Model.GetElementWithCanonicalName("/Internet Banking System/Web Application[2]"); - var singlePageApplication1 = - workspace.Model.GetElementWithCanonicalName("/Internet Banking System/Single-Page Application[1]"); - var singlePageApplication2 = - workspace.Model.GetElementWithCanonicalName("/Internet Banking System/Single-Page Application[2]"); - var oraclePrimary = - workspace.Model.GetElementWithCanonicalName("/Deployment/Live/Big Bank plc/bigbank-db01/Oracle - Primary"); - var oracleSecondary = - workspace.Model.GetElementWithCanonicalName("/Deployment/Live/Big Bank plc/bigbank-db02/Oracle - Secondary"); + // Rel_Left(Live__BigBankplc__bigbankdb01__OraclePrimary, Live__BigBankplc__bigbankdb02__OracleSecondary, "Replicates data to") + + // Model is changed that instances are counted per parent orig ...[2] cannot be used anymore, separate per view, full names have to be used + var developmentWebApplication = + workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Development/Developer Laptop/Docker Container - Web Server/Apache Tomcat/Internet Banking System.Web Application[1]"); + var developmentSinglePageApplication = + workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Development/Developer Laptop/Web Browser/Internet Banking System.Single-Page Application[1]"); var developmentDeploymentView = workspace.Views.DeploymentViews.First(); - var liveDeploymentView = workspace.Views.DeploymentViews.Last(); developmentDeploymentView.Relationships - .First(r => r.Relationship.SourceId == webApplication1.Id && - r.Relationship.DestinationId == singlePageApplication1.Id) + .First(r => r.Relationship.SourceId == developmentWebApplication.Id && + r.Relationship.DestinationId == developmentSinglePageApplication.Id) .SetDirection(DirectionValues.Up); + + var liveWebApplication = + workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Live/Big Bank plc/bigbank-web***/Apache Tomcat/Internet Banking System.Web Application[1]"); + var liveSinglePageApplication = + workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Live/Customer's computer/Web Browser/Internet Banking System.Single-Page Application[1]"); + var liveOraclePrimary = + workspace.Model.GetElementWithCanonicalOrStaticalName("DeploymentNode://Live/Big Bank plc/bigbank-db01/Oracle - Primary"); + var liveOracleSecondary = + workspace.Model.GetElementWithCanonicalOrStaticalName("DeploymentNode://Live/Big Bank plc/bigbank-db02/Oracle - Secondary"); + var liveDeploymentView = workspace.Views.DeploymentViews.Last(); liveDeploymentView.Relationships - .First(r => r.Relationship.SourceId == webApplication2.Id && - r.Relationship.DestinationId == singlePageApplication2.Id) + .First(r => r.Relationship.SourceId == liveWebApplication.Id && + r.Relationship.DestinationId == liveSinglePageApplication.Id) .SetDirection(DirectionValues.Up); liveDeploymentView.Relationships - .First(r => r.Relationship.SourceId == oraclePrimary.Id && r.Relationship.DestinationId == oracleSecondary.Id) + .First(r => r.Relationship.SourceId == liveOraclePrimary.Id && r.Relationship.DestinationId == liveOracleSecondary.Id) .SetDirection(DirectionValues.Left); } @@ -721,17 +724,17 @@ title Web Application - Dynamic !includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Deployment.puml ' Structurizr.DeploymentView: deployment -title Software System - Deployment +title Software System - Deployment - Default LAYOUT_WITH_LEGEND() -Node(Deployment__Default__DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { - Node(Deployment__Default__DatabaseServer__MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { +Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { + Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") } } -Node(Deployment__Default__WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { - Node(Deployment__Default__WebServer__ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { +Node(WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { + Node(ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"") } } @@ -1155,17 +1158,17 @@ legend right !define Node(e_alias, e_label, e_techn) rectangle ""==e_label\n[e_techn]"" <> as e_alias ' Structurizr.DeploymentView: deployment -title Software System - Deployment +title Software System - Deployment - Default LAYOUT_WITH_LEGEND() -Node(Deployment__Default__DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { - Node(Deployment__Default__DatabaseServer__MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { +Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { + Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") } } -Node(Deployment__Default__WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { - Node(Deployment__Default__WebServer__ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { +Node(WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { + Node(ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"") } } @@ -1189,17 +1192,17 @@ public void test_writeDeploymentView_WithCustomBaseUrl() !includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Deployment.puml ' Structurizr.DeploymentView: deployment -title Software System - Deployment +title Software System - Deployment - Default LAYOUT_WITH_LEGEND() -Node(Deployment__Default__DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { - Node(Deployment__Default__DatabaseServer__MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { +Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { + Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") } } -Node(Deployment__Default__WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { - Node(Deployment__Default__WebServer__ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { +Node(WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { + Node(ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"") } } diff --git a/Structurizr.PlantUML.Tests/IO/PlantUML/PlantUMLWriterTests.cs b/Structurizr.PlantUML.Tests/IO/PlantUML/PlantUMLWriterTests.cs index 3d5e4c7..754debe 100644 --- a/Structurizr.PlantUML.Tests/IO/PlantUML/PlantUMLWriterTests.cs +++ b/Structurizr.PlantUML.Tests/IO/PlantUML/PlantUMLWriterTests.cs @@ -101,7 +101,7 @@ title Web Application - Dynamic @enduml @startuml -title Software System - Deployment +title Software System - Deployment - Default node ""Database Server"" <> as 23 { node ""MySQL"" <> as 24 { artifact ""Database"" <> as 25 @@ -254,7 +254,7 @@ public void test_writeDeploymentView() Assert.Equal( @"@startuml -title Software System - Deployment +title Software System - Deployment - Default node ""Database Server"" <> as 23 { node ""MySQL"" <> as 24 { artifact ""Database"" <> as 25 diff --git a/Structurizr.PlantUML.Tests/Structurizr.PlantUML.Tests.csproj b/Structurizr.PlantUML.Tests/Structurizr.PlantUML.Tests.csproj index 1152688..99d79e0 100644 --- a/Structurizr.PlantUML.Tests/Structurizr.PlantUML.Tests.csproj +++ b/Structurizr.PlantUML.Tests/Structurizr.PlantUML.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp1.1 + netcoreapp2.1 false diff --git a/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs b/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs new file mode 100644 index 0000000..3172a58 --- /dev/null +++ b/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; + +namespace Structurizr.IO.C4PlantUML.ModelExtensions +{ + public static class ModelExtensions + { + /// + /// new impl. of CanonicalName starts with "{ElementType}://" or "{ElementType}://{DeploymentName}/{DeploymentName}/" instead of "/" therefore it can be optionally ignored + /// Additional / in staticNames have to be converted to . + /// + /// the canonical name with elementType prefix (e.g. Container://SoftwareSystem/Container) or without elementType prefix (e.g. /SoftwareSystem/Container) + /// + public static Element GetElementWithCanonicalOrStaticalName(this Model model, string canonicalName, bool compareOnlyLastPart=true) + { + if (string.IsNullOrWhiteSpace(canonicalName)) + throw new ArgumentException("A canonical name must be specified."); + var found = model.GetElements().FirstOrDefault((Func) (x => + { + if (compareOnlyLastPart) + return x.CanonicalName.EndsWith(canonicalName); + else + return x.CanonicalName == canonicalName; + })); + + if (found == null) + { + var all = model.GetElements().Select(e => e.CanonicalName).ToList(); + var combined = string.Join("\n", all); + combined = combined; + } + + return found; + } + } +} \ No newline at end of file diff --git a/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/RelationshipViewExtensions.cs b/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/RelationshipViewExtensions.cs index 1d4e309..39decbd 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/RelationshipViewExtensions.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/RelationshipViewExtensions.cs @@ -1,9 +1,15 @@ +using System.Collections.Generic; + namespace Structurizr.IO.C4PlantUML.ModelExtensions { + /// + /// WORKAROUND: RelationshipView supports no properties anymore, therefore the direction is stored in Position + /// public static class RelationshipViewExtensions { /// /// Get a direction of the relation which should be used in a specific C4PlantUML views + /// (direction is stored in Position) /// /// /// returns true if it is defined via the view specific RelationshipView and false if it is defined via the underlying Relationship @@ -11,7 +17,7 @@ public static class RelationshipViewExtensions public static string GetDirection(this RelationshipView relationshipView, out bool viewSpecific) { string value = DirectionValues.NotSet; - if (relationshipView.Properties?.TryGetValue(Properties.Direction, out value) == true) + if (relationshipView.Position.HasValue && Position2Direction.TryGetValue(relationshipView.Position.Value, out value) == true) { viewSpecific = true; return value; @@ -24,15 +30,32 @@ public static string GetDirection(this RelationshipView relationshipView, out bo /// /// Set a direction of the relation which should be used in a specific C4PlantUML views + /// (direction is internal stored in Position) /// /// /// one of public static void SetDirection(this RelationshipView relationshipView, string direction) { if (string.IsNullOrWhiteSpace(direction)) // direction DirectionValues.NotSet - relationshipView.Properties.Remove(Properties.Direction); + relationshipView.Position = null; else - relationshipView.Properties[Properties.Direction] = direction; + relationshipView.Position = Direction2Position[direction]; } + + private static Dictionary Direction2Position = new Dictionary + { + [DirectionValues.Up] = 1, + [DirectionValues.Down] = 2, + [DirectionValues.Left] = 3, + [DirectionValues.Right] = 4 + }; + + private static Dictionary Position2Direction = new Dictionary + { + [1] = DirectionValues.Up, + [2] = DirectionValues.Down, + [3] = DirectionValues.Left, + [4] = DirectionValues.Right + }; } } \ No newline at end of file diff --git a/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs b/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs index ee2285e..66dd12d 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs @@ -165,13 +165,20 @@ protected string TokenizeName(string s, int? hash = null) { if (String.IsNullOrWhiteSpace(s)) return ""; - s = s - .Trim('/') - .Replace(" ", "") - .Replace("-", "") - .Replace("[", "") - .Replace("]", "") - .Replace("/", "__"); + // canonically name calculation changed + // a) instead of "/" starts with "{ElementType}://"; remove it that it is compatible with old impl. + // b) deployment namespaces are added with "/"; remove it that it is shorter (unique parts created via hash) + // c) orig "/" in static namespaces replaced with "."; replace with "__" that it is compatible with old impl. + var p = s.LastIndexOf('/'); + if (p >= 0) + s = s.Substring(p + 1); + + s = s.Replace(" ", "") + .Replace("-", "") + .Replace("[", "") + .Replace("]", "") + .Replace(".", "__"); + if (hash.HasValue) { s = s + "__" + hash.Value.ToString("x"); diff --git a/Structurizr.PlantUML/Structurizr.PlantUML.csproj b/Structurizr.PlantUML/Structurizr.PlantUML.csproj index 8c3e4da..2bb6043 100644 --- a/Structurizr.PlantUML/Structurizr.PlantUML.csproj +++ b/Structurizr.PlantUML/Structurizr.PlantUML.csproj @@ -16,11 +16,11 @@ - netstandard1.3;net45 + netstandard2.0;net45 - + \ No newline at end of file diff --git a/Structurizr.Reflection.Examples/Structurizr.Reflection.Examples.csproj b/Structurizr.Reflection.Examples/Structurizr.Reflection.Examples.csproj index 8be178a..a143cdf 100644 --- a/Structurizr.Reflection.Examples/Structurizr.Reflection.Examples.csproj +++ b/Structurizr.Reflection.Examples/Structurizr.Reflection.Examples.csproj @@ -7,7 +7,7 @@ false - + diff --git a/Structurizr.Reflection/Structurizr.Reflection.csproj b/Structurizr.Reflection/Structurizr.Reflection.csproj index 92fc045..cfc4bdf 100644 --- a/Structurizr.Reflection/Structurizr.Reflection.csproj +++ b/Structurizr.Reflection/Structurizr.Reflection.csproj @@ -20,7 +20,7 @@ - + From b07a8406dd6e0eacda01dd4df227aafca562de60 Mon Sep 17 00:00:00 2001 From: KIRCHSTH Date: Tue, 22 Jun 2021 22:55:30 +0200 Subject: [PATCH 2/7] Structurizr.Reflection - remove obsolete System.Runtime reference --- Structurizr.Reflection/Structurizr.Reflection.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Structurizr.Reflection/Structurizr.Reflection.csproj b/Structurizr.Reflection/Structurizr.Reflection.csproj index cfc4bdf..2a5596f 100644 --- a/Structurizr.Reflection/Structurizr.Reflection.csproj +++ b/Structurizr.Reflection/Structurizr.Reflection.csproj @@ -23,10 +23,6 @@ - - - - From b0c4b00af1745836635eac942c6fa0c33c7afea1 Mon Sep 17 00:00:00 2001 From: KIRCHSTH Date: Wed, 23 Jun 2021 10:23:33 +0200 Subject: [PATCH 3/7] C4PlantUml: throw exception if Element cannot be found --- .../IO/C4PlantUML/C4PlantUmlException.cs | 9 +++++++++ .../IO/C4PlantUML/ModelExtensions/ModelExtensions.cs | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlException.cs diff --git a/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlException.cs b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlException.cs new file mode 100644 index 0000000..71ba6d4 --- /dev/null +++ b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlException.cs @@ -0,0 +1,9 @@ +using System; + +namespace Structurizr.PlantUML.IO.C4PlantUML +{ + public class C4PlantUmlException : Exception + { + public C4PlantUmlException(string message) : base(message) { } + } +} diff --git a/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs b/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs index 3172a58..4d02454 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/ModelExtensions/ModelExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Structurizr.PlantUML.IO.C4PlantUML; namespace Structurizr.IO.C4PlantUML.ModelExtensions { @@ -27,9 +28,9 @@ public static Element GetElementWithCanonicalOrStaticalName(this Model model, st { var all = model.GetElements().Select(e => e.CanonicalName).ToList(); var combined = string.Join("\n", all); - combined = combined; + throw new C4PlantUmlException( + $"Element {canonicalName} could not be found. Following elements exist:\n{combined}"); } - return found; } } From ab21d062958351c1f0c20c55bd7aba7a6b14cd5e Mon Sep 17 00:00:00 2001 From: KIRCHSTH Date: Wed, 23 Jun 2021 12:48:10 +0200 Subject: [PATCH 4/7] C4PlantUmlWriter: update generated source to new C4PlantUml stdlib v2.2.0 - all obsolete "C4_Dynamic" and "C4_Deployment" calls are removed - new SHOW_LEGEND() call is used (instead of LAYOUT_WITH_LEGEND) - new RelIndex() call is used (instead of Interact2) - new kirchsth extsions are stored in https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended (not master anymore) - new stdlib Node()-impl supports automatic line breaks (BlockText calculation is obsolete, no \n) --- .../IO/C4PlantUML/C4PlantUmlWriterTests.cs | 596 +++--------------- .../IO/C4PlantUML/C4PlantUmlWriter.cs | 266 +------- .../IO/C4PlantUML/PlantUMLWriterBase.cs | 39 -- docs/c4-plantuml.md | 2 +- 4 files changed, 100 insertions(+), 803 deletions(-) diff --git a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs index 1fa0d38..19c5518 100644 --- a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs +++ b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs @@ -46,8 +46,6 @@ public void test_writeBigBankPlcWorkspace() ' Structurizr.SystemLandscapeView: SystemLandscape title System Landscape for Big Bank plc -LAYOUT_WITH_LEGEND() - Person_Ext(PersonalBankingCustomer__9bc576, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") Enterprise_Boundary(BigBankplc, ""Big Bank plc"") { Person(BackOfficeStaff__5f761d, ""Back Office Staff"", ""Administration and support staff within the bank."") @@ -66,6 +64,8 @@ public void test_writeBigBankPlcWorkspace() Rel(PersonalBankingCustomer__9bc576, ATM__22fc739, ""Withdraws cash using"") Rel(PersonalBankingCustomer__9bc576, CustomerServiceStaff__a35be5, ""Asks questions to"", ""Telephone"") Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Uses"") + +SHOW_LEGEND() @enduml @startuml @@ -74,8 +74,6 @@ public void test_writeBigBankPlcWorkspace() ' Structurizr.SystemContextView: SystemContext title Internet Banking System - System Context -LAYOUT_WITH_LEGEND() - System(EmailSystem__2908eb9, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."") System(InternetBankingSystem__2aef74c, ""Internet Banking System"", ""Allows customers to view information about their bank accounts, and make payments."") System(MainframeBankingSystem__f50ffa, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") @@ -84,6 +82,8 @@ title Internet Banking System - System Context Rel_Right(InternetBankingSystem__2aef74c, EmailSystem__2908eb9, ""Sends e-mail using"") Rel(InternetBankingSystem__2aef74c, MainframeBankingSystem__f50ffa, ""Uses"") Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Uses"") + +SHOW_LEGEND() @enduml @startuml @@ -92,8 +92,6 @@ title Internet Banking System - System Context ' Structurizr.ContainerView: Containers title Internet Banking System - Containers -LAYOUT_WITH_LEGEND() - System(EmailSystem__2908eb9, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."") System(MainframeBankingSystem__f50ffa, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") Person_Ext(PersonalBankingCustomer__9bc576, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") @@ -114,6 +112,8 @@ title Internet Banking System - Containers Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__WebApplication__1bb919c, ""Uses"", ""HTTPS"") Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__2c36bed, ""Makes API calls to"", ""JSON/HTTPS"") Rel_Right(InternetBankingSystem__WebApplication__1bb919c, InternetBankingSystem__SinglePageApplication__1414c79, ""Delivers to the customer's web browser"") + +SHOW_LEGEND() @enduml @startuml @@ -122,8 +122,6 @@ title Internet Banking System - Containers ' Structurizr.ComponentView: Components title Internet Banking System - API Application - Components -LAYOUT_WITH_LEGEND() - ContainerDb(InternetBankingSystem__Database__18307f7, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") System(EmailSystem__2908eb9, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."") System(MainframeBankingSystem__f50ffa, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") @@ -150,244 +148,36 @@ title Internet Banking System - API Application - Components Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__AccountsSummaryController__3f81fb2, ""Makes API calls to"", ""JSON/HTTPS"") Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__ResetPasswordController__23f0eac, ""Makes API calls to"", ""JSON/HTTPS"") Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Makes API calls to"", ""JSON/HTTPS"") + +SHOW_LEGEND() @enduml @startuml -!include -' C4_Dynamic.puml is missing, simulate it with following definitions -' Scope: Interactions in an enterprise, software system or container. -' Primary and supporting elements: Depends on the diagram scope - -' enterprise - people and software systems related to the enterprise in scope -' software system - see system context or container diagrams, -' container - see component diagram. -' Intended audience: Technical and non-technical people, inside and outside of the software development team. - -' Dynamic diagram introduces (automatically) numbered interactions: -' Interact(): used automatic calculated index, -' Interact2(): index can be explicit defined, -' SetIndex(): set the next index, -' GetIndex(): get the index and automatically increase index - -' Index -' ################################## - -!function $inc_($value, $step=1) - !return $value + $step -!endfunction - -!$index=1 - -!function SetIndex($new_index) - !$index=$new_index -!endfunction - -!function GetIndex($auto_increase=1) - !$old = $index - !$index=$inc_($index, $auto_increase) - !return $old -!endfunction - -' Interact -' ################################## -!define Interact2(e_index, e_from, e_to, e_label) Rel(e_from, e_to, ""e_index: e_label"") -!define Interact2(e_index, e_from, e_to, e_label, e_techn) Rel(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_Back(e_index, e_from, e_to, e_label) Rel_Back(e_from, e_to, ""e_index: e_label"") -!define Interact2_Back(e_index, e_from, e_to, e_label, e_techn) Rel_Back(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_Neighbor(e_index, e_from, e_to, e_label) Rel_Neighbor(e_from, e_to, ""e_index: e_label"") -!define Interact2_Neighbor(e_index, e_from, e_to, e_label, e_techn) Rel_Neighbor(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_Back_Neighbor(e_index, e_from, e_to, e_label) Rel_Back_Neighbor(e_from, e_to, ""e_index: e_label"") -!define Interact2_Back_Neighbor(e_index, e_from, e_to, e_label, e_techn) Rel_Back_Neighbor(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_D(e_index, e_from, e_to, e_label) Rel_D(e_from, e_to, ""e_index: e_label"") -!define Interact2_D(e_index, e_from, e_to, e_label, e_techn) Rel_D(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Down(e_index, e_from, e_to, e_label) Rel_Down(e_from, e_to, ""e_index: e_label"") -!define Interact2_Down(e_index, e_from, e_to, e_label, e_techn) Rel_Down(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_U(e_index, e_from, e_to, e_label) Rel_U(e_from, e_to, ""e_index: e_label"") -!define Interact2_U(e_index, e_from, e_to, e_label, e_techn) Rel_U(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Up(e_index, e_from, e_to, e_label) Rel_Up(e_from, e_to, ""e_index: e_label"") -!define Interact2_Up(e_index, e_from, e_to, e_label, e_techn) Rel_Up(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_L(e_index, e_from, e_to, e_label) Rel_L(e_from, e_to, ""e_index: e_label"") -!define Interact2_L(e_index, e_from, e_to, e_label, e_techn) Rel_L(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Left(e_index, e_from, e_to, e_label) Rel_Left(e_from, e_to, ""e_index: e_label"") -!define Interact2_Left(e_index, e_from, e_to, e_label, e_techn) Rel_Left(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_R(e_index, e_from, e_to, e_label) Rel_R(e_from, e_to, ""e_index: e_label"") -!define Interact2_R(e_index, e_from, e_to, e_label, e_techn) Rel_R(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Right(e_index, e_from, e_to, e_label) Rel_Right(e_from, e_to, ""e_index: e_label"") -!define Interact2_Right(e_index, e_from, e_to, e_label, e_techn) Rel_Right(e_from, e_to, ""e_index: e_label"", e_techn) - -!unquoted function Interact($e_from, $e_to, $e_label) - Interact2($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact($e_from, $e_to, $e_label, $e_techn) - Interact2($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_Back($e_from, $e_to, $e_label) - Interact2_Back($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Back($e_from, $e_to, $e_label, $e_techn) - Interact2_Back($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_Neighbor($e_from, $e_to, $e_label) - Interact2_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Neighbor($e_from, $e_to, $e_label, $e_techn) - Interact2_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_Back_Neighbor($e_from, $e_to, $e_label) - Interact2_Back_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Back_Neighbor($e_from, $e_to, $e_label, $e_techn) - Interact2_Back_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_D($e_from, $e_to, $e_label) - Interact2_D($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_D($e_from, $e_to, $e_label, $e_techn) - Interact2_D($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Down($e_from, $e_to, $e_label) - Interact2_Down($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Down($e_from, $e_to, $e_label, $e_techn) - Interact2_Down($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_U($e_from, $e_to, $e_label) - Interact2_U($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_U($e_from, $e_to, $e_label, $e_techn) - Interact2_U($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Up($e_from, $e_to, $e_label) - Interact2_Up($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Up($e_from, $e_to, $e_label, $e_techn) - Interact2_Up($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_L($e_from, $e_to, $e_label) - Interact2_L($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_L($e_from, $e_to, $e_label, $e_techn) - Interact2_L($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Left($e_from, $e_to, $e_label) - Interact2_Left($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Left($e_from, $e_to, $e_label, $e_techn) - Interact2_Left($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_R($e_from, $e_to, $e_label) - Interact2_R($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_R($e_from, $e_to, $e_label, $e_techn) - Interact2_R($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Right($e_from, $e_to, $e_label) - Interact2_Right($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Right($e_from, $e_to, $e_label, $e_techn) - Interact2_Right($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction +!include ' Structurizr.DynamicView: SignIn title API Application - Dynamic -LAYOUT_WITH_LEGEND() - ContainerDb(InternetBankingSystem__Database__18307f7, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") Container(InternetBankingSystem__SinglePageApplication__1414c79, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") Container_Boundary(InternetBankingSystem__APIApplication__2c36bed, ""API Application"") { Component(InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Security Component"", ""Spring Bean"", ""Provides functionality related to signing in, changing passwords, etc."") Component(InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Sign In Controller"", ""Spring MVC Rest Controller"", ""Allows users to sign in to the Internet Banking System."") } -Interact2_Right(""1"", InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Submits credentials to"", ""JSON/HTTPS"") -Interact2(""2"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Calls isAuthenticated() on"") -Interact2_Right(""3"", InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__Database__18307f7, ""select * from users where username = ?"", ""JDBC"") +RelIndex_Right(""1"", InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Submits credentials to"", ""JSON/HTTPS"") +RelIndex(""2"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Calls isAuthenticated() on"") +RelIndex_Right(""3"", InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__Database__18307f7, ""select * from users where username = ?"", ""JDBC"") + +SHOW_LEGEND() @enduml @startuml -!include -' C4_Deployment.puml is missing, simulate it with following definitions -' Scope: A single software system. -' Primary elements: Deployment nodes and containers within the software system in scope. -' Intended audience: Technical people inside and outside of the software development team; including software architects, developers and operations/support staff. - -' Colors -' ################################## -!define NODE_FONT_COLOR #444444 -!define NODE_BG_COLOR #FFFFFF - -' Styling -' ################################## - -skinparam rectangle<> { - Shadowing false - StereotypeFontSize 0 - FontColor NODE_FONT_COLOR - BackgroundColor NODE_BG_COLOR - BorderColor #444444 -} - -' Layout -' ################################## - -!definelong LAYOUT_WITH_LEGEND -hide stereotype -legend right -|= |= Type | -| | deployment node | -| | deployment container | -endlegend -!enddefinelong - -' Nodes -' ################################## -' PlantUML does not support automatic line breaks of container, if e_techn is very long insert line breaks with -' ""\n"" -!define Node(e_alias, e_label, e_techn) rectangle ""==e_label\n[e_techn]"" <> as e_alias +!include ' Structurizr.DeploymentView: DevelopmentDeployment title Internet Banking System - Deployment - Development -LAYOUT_WITH_LEGEND() - -Node(DeveloperLaptop__389f399, ""Developer Laptop"", ""Microsoft Windows 10 or Apple \nmacOS"") { +Node(DeveloperLaptop__389f399, ""Developer Laptop"", ""Microsoft Windows 10 or Apple macOS"") { Node(DockerContainerWebServer__1b73d2e, ""Docker Container - Web Server"", ""Docker"") { Node(ApacheTomcat__1cc9f55, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { Container(InternetBankingSystem__WebApplication1__28f79f6, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") @@ -399,61 +189,23 @@ title Internet Banking System - Deployment - Development ContainerDb(InternetBankingSystem__Database1__3296ca6, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") } } - Node(WebBrowser__3930fd, ""Web Browser"", ""Google Chrome, Mozilla \nFirefox, Apple Safari or \nMicrosoft Edge"") { + Node(WebBrowser__3930fd, ""Web Browser"", ""Google Chrome, Mozilla Firefox, Apple Safari or Microsoft Edge"") { Container(InternetBankingSystem__SinglePageApplication1__bbe85d, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") } } Rel(InternetBankingSystem__APIApplication1__1f227f4, InternetBankingSystem__Database1__3296ca6, ""Reads from and writes to"", ""JDBC"") Rel(InternetBankingSystem__SinglePageApplication1__bbe85d, InternetBankingSystem__APIApplication1__1f227f4, ""Makes API calls to"", ""JSON/HTTPS"") Rel_Up(InternetBankingSystem__WebApplication1__28f79f6, InternetBankingSystem__SinglePageApplication1__bbe85d, ""Delivers to the customer's web browser"") + +SHOW_LEGEND() @enduml @startuml -!include -' C4_Deployment.puml is missing, simulate it with following definitions -' Scope: A single software system. -' Primary elements: Deployment nodes and containers within the software system in scope. -' Intended audience: Technical people inside and outside of the software development team; including software architects, developers and operations/support staff. - -' Colors -' ################################## -!define NODE_FONT_COLOR #444444 -!define NODE_BG_COLOR #FFFFFF - -' Styling -' ################################## - -skinparam rectangle<> { - Shadowing false - StereotypeFontSize 0 - FontColor NODE_FONT_COLOR - BackgroundColor NODE_BG_COLOR - BorderColor #444444 -} - -' Layout -' ################################## - -!definelong LAYOUT_WITH_LEGEND -hide stereotype -legend right -|= |= Type | -| | deployment node | -| | deployment container | -endlegend -!enddefinelong - -' Nodes -' ################################## -' PlantUML does not support automatic line breaks of container, if e_techn is very long insert line breaks with -' ""\n"" -!define Node(e_alias, e_label, e_techn) rectangle ""==e_label\n[e_techn]"" <> as e_alias +!include ' Structurizr.DeploymentView: LiveDeployment title Internet Banking System - Deployment - Live -LAYOUT_WITH_LEGEND() - Node(BigBankplc__3ffe15e, ""Big Bank plc"", ""Big Bank plc data center"") { Node(bigbankweb***__3f92e18, ""bigbank-web*** (x4)"", ""Ubuntu 16.04 LTS"") { Node(ApacheTomcat__27b4383, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { @@ -476,8 +228,8 @@ title Internet Banking System - Deployment - Live } } } -Node(Customer'scomputer__2510bf3, ""Customer's computer"", ""Microsoft Windows or Apple \nmacOS"") { - Node(WebBrowser__ba951, ""Web Browser"", ""Google Chrome, Mozilla \nFirefox, Apple Safari or \nMicrosoft Edge"") { +Node(Customer'scomputer__2510bf3, ""Customer's computer"", ""Microsoft Windows or Apple macOS"") { + Node(WebBrowser__ba951, ""Web Browser"", ""Google Chrome, Mozilla Firefox, Apple Safari or Microsoft Edge"") { Container(InternetBankingSystem__SinglePageApplication1__298b31c, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") } } @@ -490,6 +242,8 @@ title Internet Banking System - Deployment - Live Rel_Left(OraclePrimary__19fd8f, OracleSecondary__1c4ec22, ""Replicates data to"") Rel(InternetBankingSystem__SinglePageApplication1__298b31c, InternetBankingSystem__APIApplication1__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") Rel_Up(InternetBankingSystem__WebApplication1__1720850, InternetBankingSystem__SinglePageApplication1__298b31c, ""Delivers to the customer's web browser"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -618,17 +372,15 @@ public void test_writeWorkspace_WithCustomBaseUrl() { PopulateWorkspace(); - _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/"; + _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; _plantUMLWriter.Write(_workspace, _stringWriter); Assert.Equal( @"@startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Context.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Context.puml ' Structurizr.SystemLandscapeView: enterpriseContext title System Landscape for Some Enterprise -LAYOUT_WITH_LEGEND() - System_Ext(EmailSystem__1127701, ""E-mail System"") Enterprise_Boundary(SomeEnterprise, ""Some Enterprise"") { Person(User__387cc75, ""User"") @@ -637,16 +389,16 @@ public void test_writeWorkspace_WithCustomBaseUrl() Rel(EmailSystem__1127701, User__387cc75, ""Delivers e-mails to"") Rel(SoftwareSystem__31d545b, EmailSystem__1127701, ""Sends e-mail using"") Rel(User__387cc75, SoftwareSystem__31d545b, ""Uses"") + +SHOW_LEGEND() @enduml @startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Context.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Context.puml ' Structurizr.SystemContextView: systemContext title Software System - System Context -LAYOUT_WITH_LEGEND() - Enterprise_Boundary(SomeEnterprise, ""Some Enterprise"") { System_Ext(EmailSystem__1127701, ""E-mail System"") System(SoftwareSystem__31d545b, ""Software System"") @@ -655,16 +407,16 @@ title Software System - System Context Rel(SoftwareSystem__31d545b, EmailSystem__1127701, ""Sends e-mail using"") Rel(User__387cc75, SoftwareSystem__31d545b, ""Uses"") } + +SHOW_LEGEND() @enduml @startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Container.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Container.puml ' Structurizr.ContainerView: containers title Software System - Containers -LAYOUT_WITH_LEGEND() - System_Ext(EmailSystem__1127701, ""E-mail System"") Person(User__387cc75, ""User"") System_Boundary(SoftwareSystem__31d545b, ""Software System"") { @@ -675,16 +427,16 @@ title Software System - Containers Rel(User__387cc75, SoftwareSystem__WebApplication__d2a342, """", ""HTTP"") Rel(SoftwareSystem__WebApplication__d2a342, SoftwareSystem__Database__39bccb8, ""Reads from and writes to"", ""JDBC"") Rel(SoftwareSystem__WebApplication__d2a342, EmailSystem__1127701, ""Sends e-mail using"") + +SHOW_LEGEND() @enduml @startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Component.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Component.puml ' Structurizr.ComponentView: components title Software System - Web Application - Components -LAYOUT_WITH_LEGEND() - ContainerDb(SoftwareSystem__Database__39bccb8, ""Database"", ""Relational Database Schema"", ""Stores information"") System_Ext(EmailSystem__1127701, ""E-mail System"") Person(User__387cc75, ""User"") @@ -699,35 +451,35 @@ title Software System - Web Application - Components Rel(SoftwareSystem__WebApplication__SomeController__341621c, SoftwareSystem__WebApplication__SomeRepository__6d9009, ""Uses"") Rel(SoftwareSystem__WebApplication__SomeRepository__6d9009, SoftwareSystem__Database__39bccb8, ""Reads from and writes to"", ""JDBC"") Rel(User__387cc75, SoftwareSystem__WebApplication__SomeController__341621c, ""Uses"", ""HTTP"") + +SHOW_LEGEND() @enduml @startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Dynamic.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Dynamic.puml ' Structurizr.DynamicView: dynamic title Web Application - Dynamic -LAYOUT_WITH_LEGEND() - ContainerDb(SoftwareSystem__Database__39bccb8, ""Database"", ""Relational Database Schema"", ""Stores information"") Person(User__387cc75, ""User"") Container_Boundary(SoftwareSystem__WebApplication__d2a342, ""Web Application"") { Component(SoftwareSystem__WebApplication__SomeController__341621c, ""SomeController"", ""Spring MVC Controller"") Component(SoftwareSystem__WebApplication__SomeRepository__6d9009, ""SomeRepository"", ""Spring Data"") } -Interact2(""1"", User__387cc75, SoftwareSystem__WebApplication__SomeController__341621c, ""Requests /something"", ""HTTP"") -Interact2(""2"", SoftwareSystem__WebApplication__SomeController__341621c, SoftwareSystem__WebApplication__SomeRepository__6d9009, """") -Interact2(""3"", SoftwareSystem__WebApplication__SomeRepository__6d9009, SoftwareSystem__Database__39bccb8, ""select * from something"", ""JDBC"") +RelIndex(""1"", User__387cc75, SoftwareSystem__WebApplication__SomeController__341621c, ""Requests /something"", ""HTTP"") +RelIndex(""2"", SoftwareSystem__WebApplication__SomeController__341621c, SoftwareSystem__WebApplication__SomeRepository__6d9009, """") +RelIndex(""3"", SoftwareSystem__WebApplication__SomeRepository__6d9009, SoftwareSystem__Database__39bccb8, ""select * from something"", ""JDBC"") + +SHOW_LEGEND() @enduml @startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Deployment.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Deployment.puml ' Structurizr.DeploymentView: deployment title Software System - Deployment - Default -LAYOUT_WITH_LEGEND() - Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") @@ -739,6 +491,8 @@ title Software System - Deployment - Default } } Rel(SoftwareSystem__WebApplication1__31f1f25, SoftwareSystem__Database1__bb9c73, ""Reads from and writes to"", ""JDBC"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -759,8 +513,6 @@ public void test_writeEnterpriseContextView() ' Structurizr.SystemLandscapeView: enterpriseContext title System Landscape for Some Enterprise -LAYOUT_WITH_LEGEND() - System_Ext(EmailSystem__1934cbe, ""E-mail System"") Enterprise_Boundary(SomeEnterprise, ""Some Enterprise"") { Person(User__3b843b5, ""User"") @@ -769,6 +521,8 @@ public void test_writeEnterpriseContextView() Rel(EmailSystem__1934cbe, User__3b843b5, ""Delivers e-mails to"") Rel(SoftwareSystem__7134f, EmailSystem__1934cbe, ""Sends e-mail using"") Rel(User__3b843b5, SoftwareSystem__7134f, ""Uses"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -781,18 +535,16 @@ public void test_writeEnterpriseContextView_WithCustomBaseUrl() PopulateWorkspace(); SystemLandscapeView systemLandscapeView = _workspace.Views.SystemLandscapeViews.First(); - _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/"; + _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; _plantUMLWriter.Write(systemLandscapeView, _stringWriter); Assert.Equal( @"@startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Context.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Context.puml ' Structurizr.SystemLandscapeView: enterpriseContext title System Landscape for Some Enterprise -LAYOUT_WITH_LEGEND() - System_Ext(EmailSystem__1934cbe, ""E-mail System"") Enterprise_Boundary(SomeEnterprise, ""Some Enterprise"") { Person(User__3b843b5, ""User"") @@ -801,6 +553,8 @@ public void test_writeEnterpriseContextView_WithCustomBaseUrl() Rel(EmailSystem__1934cbe, User__3b843b5, ""Delivers e-mails to"") Rel(SoftwareSystem__7134f, EmailSystem__1934cbe, ""Sends e-mail using"") Rel(User__3b843b5, SoftwareSystem__7134f, ""Uses"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -822,8 +576,6 @@ public void test_writeSystemContextView() ' Structurizr.SystemContextView: systemContext title Software System - System Context -LAYOUT_WITH_LEGEND() - Enterprise_Boundary(SomeEnterprise, ""Some Enterprise"") { System_Ext(EmailSystem__1127701, ""E-mail System"") System(SoftwareSystem__31d545b, ""Software System"") @@ -832,6 +584,8 @@ title Software System - System Context Rel(SoftwareSystem__31d545b, EmailSystem__1127701, ""Sends e-mail using"") Rel(User__387cc75, SoftwareSystem__31d545b, ""Uses"") } + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -852,8 +606,6 @@ public void test_writeContainerView() ' Structurizr.ContainerView: containers title Software System - Containers -LAYOUT_WITH_LEGEND() - System_Ext(EmailSystem__1934cbe, ""E-mail System"") Person(User__3b843b5, ""User"") System_Boundary(SoftwareSystem__7134f, ""Software System"") { @@ -864,6 +616,8 @@ title Software System - Containers Rel(User__3b843b5, SoftwareSystem__WebApplication__1cc1659, """", ""HTTP"") Rel(SoftwareSystem__WebApplication__1cc1659, SoftwareSystem__Database__270f9f2, ""Reads from and writes to"", ""JDBC"") Rel(SoftwareSystem__WebApplication__1cc1659, EmailSystem__1934cbe, ""Sends e-mail using"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -884,8 +638,6 @@ public void test_writeComponentsView() ' Structurizr.ComponentView: components title Software System - Web Application - Components -LAYOUT_WITH_LEGEND() - ContainerDb(SoftwareSystem__Database__270f9f2, ""Database"", ""Relational Database Schema"", ""Stores information"") System_Ext(EmailSystem__1934cbe, ""E-mail System"") Person(User__3b843b5, ""User"") @@ -900,6 +652,8 @@ title Software System - Web Application - Components Rel(SoftwareSystem__WebApplication__SomeController__327a713, SoftwareSystem__WebApplication__SomeRepository__23f6823, ""Uses"") Rel(SoftwareSystem__WebApplication__SomeRepository__23f6823, SoftwareSystem__Database__270f9f2, ""Reads from and writes to"", ""JDBC"") Rel(User__3b843b5, SoftwareSystem__WebApplication__SomeController__327a713, ""Uses"", ""HTTP"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -916,192 +670,22 @@ public void test_writeDynamicView() // Dynamic diagrams can be drawn with Components Assert.Equal( @"@startuml -!include -' C4_Dynamic.puml is missing, simulate it with following definitions -' Scope: Interactions in an enterprise, software system or container. -' Primary and supporting elements: Depends on the diagram scope - -' enterprise - people and software systems related to the enterprise in scope -' software system - see system context or container diagrams, -' container - see component diagram. -' Intended audience: Technical and non-technical people, inside and outside of the software development team. - -' Dynamic diagram introduces (automatically) numbered interactions: -' Interact(): used automatic calculated index, -' Interact2(): index can be explicit defined, -' SetIndex(): set the next index, -' GetIndex(): get the index and automatically increase index - -' Index -' ################################## - -!function $inc_($value, $step=1) - !return $value + $step -!endfunction - -!$index=1 - -!function SetIndex($new_index) - !$index=$new_index -!endfunction - -!function GetIndex($auto_increase=1) - !$old = $index - !$index=$inc_($index, $auto_increase) - !return $old -!endfunction - -' Interact -' ################################## -!define Interact2(e_index, e_from, e_to, e_label) Rel(e_from, e_to, ""e_index: e_label"") -!define Interact2(e_index, e_from, e_to, e_label, e_techn) Rel(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_Back(e_index, e_from, e_to, e_label) Rel_Back(e_from, e_to, ""e_index: e_label"") -!define Interact2_Back(e_index, e_from, e_to, e_label, e_techn) Rel_Back(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_Neighbor(e_index, e_from, e_to, e_label) Rel_Neighbor(e_from, e_to, ""e_index: e_label"") -!define Interact2_Neighbor(e_index, e_from, e_to, e_label, e_techn) Rel_Neighbor(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_Back_Neighbor(e_index, e_from, e_to, e_label) Rel_Back_Neighbor(e_from, e_to, ""e_index: e_label"") -!define Interact2_Back_Neighbor(e_index, e_from, e_to, e_label, e_techn) Rel_Back_Neighbor(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_D(e_index, e_from, e_to, e_label) Rel_D(e_from, e_to, ""e_index: e_label"") -!define Interact2_D(e_index, e_from, e_to, e_label, e_techn) Rel_D(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Down(e_index, e_from, e_to, e_label) Rel_Down(e_from, e_to, ""e_index: e_label"") -!define Interact2_Down(e_index, e_from, e_to, e_label, e_techn) Rel_Down(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_U(e_index, e_from, e_to, e_label) Rel_U(e_from, e_to, ""e_index: e_label"") -!define Interact2_U(e_index, e_from, e_to, e_label, e_techn) Rel_U(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Up(e_index, e_from, e_to, e_label) Rel_Up(e_from, e_to, ""e_index: e_label"") -!define Interact2_Up(e_index, e_from, e_to, e_label, e_techn) Rel_Up(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_L(e_index, e_from, e_to, e_label) Rel_L(e_from, e_to, ""e_index: e_label"") -!define Interact2_L(e_index, e_from, e_to, e_label, e_techn) Rel_L(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Left(e_index, e_from, e_to, e_label) Rel_Left(e_from, e_to, ""e_index: e_label"") -!define Interact2_Left(e_index, e_from, e_to, e_label, e_techn) Rel_Left(e_from, e_to, ""e_index: e_label"", e_techn) - -!define Interact2_R(e_index, e_from, e_to, e_label) Rel_R(e_from, e_to, ""e_index: e_label"") -!define Interact2_R(e_index, e_from, e_to, e_label, e_techn) Rel_R(e_from, e_to, ""e_index: e_label"", e_techn) -!define Interact2_Right(e_index, e_from, e_to, e_label) Rel_Right(e_from, e_to, ""e_index: e_label"") -!define Interact2_Right(e_index, e_from, e_to, e_label, e_techn) Rel_Right(e_from, e_to, ""e_index: e_label"", e_techn) - -!unquoted function Interact($e_from, $e_to, $e_label) - Interact2($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact($e_from, $e_to, $e_label, $e_techn) - Interact2($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_Back($e_from, $e_to, $e_label) - Interact2_Back($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Back($e_from, $e_to, $e_label, $e_techn) - Interact2_Back($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_Neighbor($e_from, $e_to, $e_label) - Interact2_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Neighbor($e_from, $e_to, $e_label, $e_techn) - Interact2_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_Back_Neighbor($e_from, $e_to, $e_label) - Interact2_Back_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Back_Neighbor($e_from, $e_to, $e_label, $e_techn) - Interact2_Back_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_D($e_from, $e_to, $e_label) - Interact2_D($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_D($e_from, $e_to, $e_label, $e_techn) - Interact2_D($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Down($e_from, $e_to, $e_label) - Interact2_Down($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Down($e_from, $e_to, $e_label, $e_techn) - Interact2_Down($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_U($e_from, $e_to, $e_label) - Interact2_U($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_U($e_from, $e_to, $e_label, $e_techn) - Interact2_U($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Up($e_from, $e_to, $e_label) - Interact2_Up($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Up($e_from, $e_to, $e_label, $e_techn) - Interact2_Up($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_L($e_from, $e_to, $e_label) - Interact2_L($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_L($e_from, $e_to, $e_label, $e_techn) - Interact2_L($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Left($e_from, $e_to, $e_label) - Interact2_Left($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Left($e_from, $e_to, $e_label, $e_techn) - Interact2_Left($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction - -!unquoted function Interact_R($e_from, $e_to, $e_label) - Interact2_R($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_R($e_from, $e_to, $e_label, $e_techn) - Interact2_R($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Right($e_from, $e_to, $e_label) - Interact2_Right($index, ""$e_from"", ""$e_to"", ""$e_label"") - !$index=$inc_($index) -!endfunction -!unquoted function Interact_Right($e_from, $e_to, $e_label, $e_techn) - Interact2_Right($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn) - !$index=$inc_($index) -!endfunction +!include ' Structurizr.DynamicView: dynamic title Web Application - Dynamic -LAYOUT_WITH_LEGEND() - ContainerDb(SoftwareSystem__Database__39bccb8, ""Database"", ""Relational Database Schema"", ""Stores information"") Person(User__387cc75, ""User"") Container_Boundary(SoftwareSystem__WebApplication__d2a342, ""Web Application"") { Component(SoftwareSystem__WebApplication__SomeController__341621c, ""SomeController"", ""Spring MVC Controller"") Component(SoftwareSystem__WebApplication__SomeRepository__6d9009, ""SomeRepository"", ""Spring Data"") } -Interact2(""1"", User__387cc75, SoftwareSystem__WebApplication__SomeController__341621c, ""Requests /something"", ""HTTP"") -Interact2(""2"", SoftwareSystem__WebApplication__SomeController__341621c, SoftwareSystem__WebApplication__SomeRepository__6d9009, """") -Interact2(""3"", SoftwareSystem__WebApplication__SomeRepository__6d9009, SoftwareSystem__Database__39bccb8, ""select * from something"", ""JDBC"") +RelIndex(""1"", User__387cc75, SoftwareSystem__WebApplication__SomeController__341621c, ""Requests /something"", ""HTTP"") +RelIndex(""2"", SoftwareSystem__WebApplication__SomeController__341621c, SoftwareSystem__WebApplication__SomeRepository__6d9009, """") +RelIndex(""3"", SoftwareSystem__WebApplication__SomeRepository__6d9009, SoftwareSystem__Database__39bccb8, ""select * from something"", ""JDBC"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -1117,51 +701,11 @@ public void test_writeDeploymentView() Assert.Equal( @"@startuml -!include -' C4_Deployment.puml is missing, simulate it with following definitions -' Scope: A single software system. -' Primary elements: Deployment nodes and containers within the software system in scope. -' Intended audience: Technical people inside and outside of the software development team; including software architects, developers and operations/support staff. - -' Colors -' ################################## -!define NODE_FONT_COLOR #444444 -!define NODE_BG_COLOR #FFFFFF - -' Styling -' ################################## - -skinparam rectangle<> { - Shadowing false - StereotypeFontSize 0 - FontColor NODE_FONT_COLOR - BackgroundColor NODE_BG_COLOR - BorderColor #444444 -} - -' Layout -' ################################## - -!definelong LAYOUT_WITH_LEGEND -hide stereotype -legend right -|= |= Type | -| | deployment node | -| | deployment container | -endlegend -!enddefinelong - -' Nodes -' ################################## -' PlantUML does not support automatic line breaks of container, if e_techn is very long insert line breaks with -' ""\n"" -!define Node(e_alias, e_label, e_techn) rectangle ""==e_label\n[e_techn]"" <> as e_alias +!include ' Structurizr.DeploymentView: deployment title Software System - Deployment - Default -LAYOUT_WITH_LEGEND() - Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") @@ -1173,6 +717,8 @@ title Software System - Deployment - Default } } Rel(SoftwareSystem__WebApplication1__31f1f25, SoftwareSystem__Database1__bb9c73, ""Reads from and writes to"", ""JDBC"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); @@ -1184,18 +730,16 @@ public void test_writeDeploymentView_WithCustomBaseUrl() PopulateWorkspace(); DeploymentView deploymentView = _workspace.Views.DeploymentViews.First(); - _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/"; + _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; _plantUMLWriter.Write(deploymentView, _stringWriter); Assert.Equal( @"@startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/C4_Deployment.puml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Deployment.puml ' Structurizr.DeploymentView: deployment title Software System - Deployment - Default -LAYOUT_WITH_LEGEND() - Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") @@ -1207,6 +751,8 @@ title Software System - Deployment - Default } } Rel(SoftwareSystem__WebApplication1__31f1f25, SoftwareSystem__Database1__bb9c73, ""Reads from and writes to"", ""JDBC"") + +SHOW_LEGEND() @enduml ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); diff --git a/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs index 186f336..995b4ed 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs @@ -25,10 +25,10 @@ public enum LayoutDirection /// /// PlantUML-stdlib or https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/release/1-0/ does not support /// dynamic or deployment diagrams. They can be used via the PlantUML-stdlib and in the diagram added definitions - /// or use a pull-request version which is available at https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/ + /// or use a pull-request version which is available at https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ /// (if the value is empty/null then PlantUML-stdlib with added definitions is used) /// - public string CustomBaseUrl { get; set; } = ""; // @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/"; + public string CustomBaseUrl { get; set; } = ""; // @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; protected override void Write(SystemLandscapeView view, TextWriter writer) { @@ -307,6 +307,9 @@ private bool HasValue(string s) protected override void WriteProlog(View view, TextWriter writer) { + if (view == null) throw new ArgumentNullException(nameof(view)); + if (writer == null) throw new ArgumentNullException(nameof(writer)); + writer.WriteLine("@startuml"); switch (view) @@ -325,237 +328,15 @@ protected override void WriteProlog(View view, TextWriter writer) break; case DynamicView _: - if (!string.IsNullOrWhiteSpace(CustomBaseUrl)) - { - writer.WriteLine($"!includeurl {CustomBaseUrl}C4_Dynamic.puml"); - } - else - { - writer.WriteLine(@"!include "); - // Add missing deployment nodes (until they are part of the plantuml macros) - writer.WriteLine(@"' C4_Dynamic.puml is missing, simulate it with following definitions"); - - writer.WriteLine(@"' Scope: Interactions in an enterprise, software system or container."); - writer.WriteLine(@"' Primary and supporting elements: Depends on the diagram scope - "); - writer.WriteLine(@"' enterprise - people and software systems related to the enterprise in scope "); - writer.WriteLine(@"' software system - see system context or container diagrams, "); - writer.WriteLine(@"' container - see component diagram."); - writer.WriteLine(@"' Intended audience: Technical and non-technical people, inside and outside of the software development team."); - writer.WriteLine(@""); - writer.WriteLine(@"' Dynamic diagram introduces (automatically) numbered interactions: "); - writer.WriteLine(@"' Interact(): used automatic calculated index, "); - writer.WriteLine(@"' Interact2(): index can be explicit defined,"); - writer.WriteLine(@"' SetIndex(): set the next index, "); - writer.WriteLine(@"' GetIndex(): get the index and automatically increase index"); - writer.WriteLine(@""); - writer.WriteLine(@"' Index"); - writer.WriteLine(@"' ##################################"); - writer.WriteLine(@""); - writer.WriteLine(@"!function $inc_($value, $step=1)"); - writer.WriteLine(@" !return $value + $step"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!$index=1"); - writer.WriteLine(@""); - writer.WriteLine(@"!function SetIndex($new_index)"); - writer.WriteLine(@" !$index=$new_index"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!function GetIndex($auto_increase=1)"); - writer.WriteLine(@" !$old = $index"); - writer.WriteLine(@" !$index=$inc_($index, $auto_increase)"); - writer.WriteLine(@" !return $old"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"' Interact"); - writer.WriteLine(@"' ##################################"); - writer.WriteLine(@"!define Interact2(e_index, e_from, e_to, e_label) Rel(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2(e_index, e_from, e_to, e_label, e_techn) Rel(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!define Interact2_Back(e_index, e_from, e_to, e_label) Rel_Back(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_Back(e_index, e_from, e_to, e_label, e_techn) Rel_Back(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!define Interact2_Neighbor(e_index, e_from, e_to, e_label) Rel_Neighbor(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_Neighbor(e_index, e_from, e_to, e_label, e_techn) Rel_Neighbor(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!define Interact2_Back_Neighbor(e_index, e_from, e_to, e_label) Rel_Back_Neighbor(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_Back_Neighbor(e_index, e_from, e_to, e_label, e_techn) Rel_Back_Neighbor(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!define Interact2_D(e_index, e_from, e_to, e_label) Rel_D(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_D(e_index, e_from, e_to, e_label, e_techn) Rel_D(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@"!define Interact2_Down(e_index, e_from, e_to, e_label) Rel_Down(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_Down(e_index, e_from, e_to, e_label, e_techn) Rel_Down(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!define Interact2_U(e_index, e_from, e_to, e_label) Rel_U(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_U(e_index, e_from, e_to, e_label, e_techn) Rel_U(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@"!define Interact2_Up(e_index, e_from, e_to, e_label) Rel_Up(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_Up(e_index, e_from, e_to, e_label, e_techn) Rel_Up(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!define Interact2_L(e_index, e_from, e_to, e_label) Rel_L(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_L(e_index, e_from, e_to, e_label, e_techn) Rel_L(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@"!define Interact2_Left(e_index, e_from, e_to, e_label) Rel_Left(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_Left(e_index, e_from, e_to, e_label, e_techn) Rel_Left(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!define Interact2_R(e_index, e_from, e_to, e_label) Rel_R(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_R(e_index, e_from, e_to, e_label, e_techn) Rel_R(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@"!define Interact2_Right(e_index, e_from, e_to, e_label) Rel_Right(e_from, e_to, ""e_index: e_label"")"); - writer.WriteLine(@"!define Interact2_Right(e_index, e_from, e_to, e_label, e_techn) Rel_Right(e_from, e_to, ""e_index: e_label"", e_techn)"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact_Back($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_Back($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Back($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_Back($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact_Neighbor($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Neighbor($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact_Back_Neighbor($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_Back_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Back_Neighbor($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_Back_Neighbor($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact_D($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_D($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_D($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_D($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Down($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_Down($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Down($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_Down($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact_U($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_U($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_U($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_U($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Up($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_Up($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Up($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_Up($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact_L($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_L($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_L($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_L($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Left($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_Left($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Left($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_Left($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@""); - writer.WriteLine(@"!unquoted function Interact_R($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_R($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_R($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_R($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Right($e_from, $e_to, $e_label) "); - writer.WriteLine(@" Interact2_Right($index, ""$e_from"", ""$e_to"", ""$e_label"")"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - writer.WriteLine(@"!unquoted function Interact_Right($e_from, $e_to, $e_label, $e_techn) "); - writer.WriteLine(@" Interact2_Right($index, ""$e_from"", ""$e_to"", ""$e_label"", $e_techn)"); - writer.WriteLine(@" !$index=$inc_($index)"); - writer.WriteLine(@"!endfunction"); - } + writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) + ? $"!includeurl {CustomBaseUrl}C4_Dynamic.puml" + : $"!include "); break; case DeploymentView _: - if (!string.IsNullOrWhiteSpace(CustomBaseUrl)) - { - writer.WriteLine($"!includeurl {CustomBaseUrl}C4_Deployment.puml"); - } - else - { - writer.WriteLine(@"!include "); - // Add missing deployment nodes (until they are part of the plantuml macros) - writer.WriteLine(@"' C4_Deployment.puml is missing, simulate it with following definitions"); - - writer.WriteLine(@"' Scope: A single software system."); - writer.WriteLine(@"' Primary elements: Deployment nodes and containers within the software system in scope."); - writer.WriteLine(@"' Intended audience: Technical people inside and outside of the software development team; including software architects, developers and operations/support staff."); - writer.WriteLine(@""); - writer.WriteLine(@"' Colors"); - writer.WriteLine(@"' ##################################"); - writer.WriteLine(@"!define NODE_FONT_COLOR #444444"); - writer.WriteLine(@"!define NODE_BG_COLOR #FFFFFF"); - writer.WriteLine(@""); - writer.WriteLine(@"' Styling"); - writer.WriteLine(@"' ##################################"); - writer.WriteLine(@""); - writer.WriteLine(@"skinparam rectangle<> {"); - writer.WriteLine(@" Shadowing false"); - writer.WriteLine(@" StereotypeFontSize 0"); - writer.WriteLine(@" FontColor NODE_FONT_COLOR"); - writer.WriteLine(@" BackgroundColor NODE_BG_COLOR"); - writer.WriteLine(@" BorderColor #444444"); - writer.WriteLine(@"}"); - writer.WriteLine(@""); - writer.WriteLine(@"' Layout"); - writer.WriteLine(@"' ##################################"); - writer.WriteLine(@""); - writer.WriteLine(@"!definelong LAYOUT_WITH_LEGEND"); - writer.WriteLine(@"hide stereotype"); - writer.WriteLine(@"legend right"); - writer.WriteLine(@"|= |= Type |"); - writer.WriteLine(@"| | deployment node |"); - writer.WriteLine(@"| | deployment container |"); - writer.WriteLine(@"endlegend"); - writer.WriteLine(@"!enddefinelong"); - writer.WriteLine(@""); - writer.WriteLine(@"' Nodes"); - writer.WriteLine(@"' ##################################"); - writer.WriteLine(@"' PlantUML does not support automatic line breaks of container, if e_techn is very long insert line breaks with "); - writer.WriteLine(@"' ""\n"""); - writer.WriteLine(@"!define Node(e_alias, e_label, e_techn) rectangle ""==e_label\n[e_techn]"" <> as e_alias"); - } + writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) + ? $"!includeurl {CustomBaseUrl}C4_Deployment.puml" + : $"!include "); break; default: @@ -570,8 +351,6 @@ protected override void WriteProlog(View view, TextWriter writer) writer.WriteLine("title " + GetTitle(view)); writer.WriteLine(); - if (LayoutWithLegend) - writer.WriteLine("LAYOUT_WITH_LEGEND()"); // C4 PlantUML workaround add () if (LayoutAsSketch) writer.WriteLine("LAYOUT_AS_SKETCH()"); // C4 PlantUML workaround add () if (Layout.HasValue) @@ -588,10 +367,25 @@ protected override void WriteProlog(View view, TextWriter writer) throw new InvalidOperationException($"Unknown {nameof(LayoutDirection)} value"); } } - if (LayoutWithLegend || LayoutAsSketch || Layout.HasValue) + if (LayoutAsSketch || Layout.HasValue) writer.WriteLine(); } + protected virtual void WriteEpilog(View view, TextWriter writer) + { + if (view == null) throw new ArgumentNullException(nameof(view)); + if (writer == null) throw new ArgumentNullException(nameof(writer)); + + if (LayoutWithLegend) + { + writer.WriteLine(); + writer.WriteLine("SHOW_LEGEND()"); // C4 PlantUML workaround add () + } + + writer.WriteLine("@enduml"); + writer.WriteLine(""); + } + protected virtual void Write(Element element, TextWriter writer, int indentLevel = 0, bool asBoundary = false) { var indent = indentLevel == 0 ? "" : new string(' ', indentLevel * 2); @@ -617,10 +411,6 @@ protected virtual void Write(Element element, TextWriter writer, int indentLevel macro = "Node"; title = deploymentNode.Name + (deploymentNode.Instances > 1 ? $" (x{deploymentNode.Instances})" : ""); technology = deploymentNode.Technology; - // PlantUML supports no automatic line breaks of titles, if it belongs to a surrounding object - // make workaround with html tags (they are not working via multiple lines too) - if (technology.Length > 30) - technology = BlockText(technology, 30, @"\n"); break; default: throw new NotSupportedException($"{element.GetType()} not supported boundary type"); @@ -730,7 +520,7 @@ protected virtual void WriteDynamicInteraction(RelationshipView relationshipView tech = !string.IsNullOrWhiteSpace(relationship.Technology) ? relationship.Technology : null; var macro = GetSpecificLayoutMacro(relationshipView); - macro = "Interact2" + macro.Substring("Rel".Length); + macro = "RelIndex" + macro.Substring("Rel".Length); writer.Write($"{macro}(\"{order}\", {source}, {dest}, \"{EscapeText(label)}\""); if (tech != null) diff --git a/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs b/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs index 66dd12d..262911b 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs @@ -198,45 +198,6 @@ protected virtual string GetTitle(View view) => ? String.IsNullOrWhiteSpace(view.Title) ? view.Name : view.Title : throw new ArgumentNullException(nameof(view)); - protected string BlockText(string s, int blockWidth, string formattedLineBreak) - { - var block = s; - - if (blockWidth > 0 && !s.Contains("\n") && !s.Contains("\r")) - { - var formatted = new StringBuilder(); - int pos = 0; - string word = ""; - - foreach (var c in s) - { - word += c; - if (c == ' ') - { - if (pos != 0 && pos + word.Length > blockWidth) - { - formatted.Append(formattedLineBreak); - pos = 0; - } - formatted.Append(word); - pos += word.Length; - word = ""; - } - } - - if (word.Length > 0) - { - if (pos != 0 && pos + word.Length > blockWidth) - formatted.Append(formattedLineBreak); - formatted.Append(word); - } - - block = formatted.ToString(); - } - - return block; - } - protected string EscapeText(string s) => s.Replace("\"", """); } } \ No newline at end of file diff --git a/docs/c4-plantuml.md b/docs/c4-plantuml.md index 0141c60..3c53c93 100644 --- a/docs/c4-plantuml.md +++ b/docs/c4-plantuml.md @@ -10,7 +10,7 @@ Structurizr for .NET also includes a simple exporter that can create diagram def - Deployment* *..Dynamic and Deployment diagrams are part of an open pull request (from https://github.com/kirchsth/C4-PlantUML). The diagrams can use the definitions via -CustomBaseUrl=https://raw.githubusercontent.com/kirchsth/C4-PlantUML/master/ or if it is not set then the definition is merged in each diagram) +CustomBaseUrl=https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ or if it is not set then the definition is merged in each diagram) Simply create your software architecture model and views as usual, and use the [C4PlantUMLWriter](../Structurizr.PlantUML/IO/C4PlantUML/C4PlantUMLWriter.cs) class to export the views. [For example](../Structurizr.Examples/C4PlantUML.cs): From 86836c654d73b6b7693c9dd19480abd00931df4d Mon Sep 17 00:00:00 2001 From: KIRCHSTH Date: Wed, 23 Jun 2021 13:37:46 +0200 Subject: [PATCH 5/7] C4PlantUmlWriter: update generated source to new C4PlantUml stdlib v2.2.0 - Update c4-plantuml.md docu --- docs/c4-plantuml.md | 18 ++++++++---------- docs/images/c4-plantuml-getting-started.png | Bin 13355 -> 17492 bytes docs/images/c4-plantuml-getting-started2.png | Bin 23269 -> 28989 bytes 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/c4-plantuml.md b/docs/c4-plantuml.md index 3c53c93..ddf30ba 100644 --- a/docs/c4-plantuml.md +++ b/docs/c4-plantuml.md @@ -1,16 +1,14 @@ # C4-PlantUML -Structurizr for .NET also includes a simple exporter that can create diagram definitions compatible with [C4-PlantUML](https://github.com/RicardoNiepel/C4-PlantUML). The following diagram types are supported: +Structurizr for .NET also includes a simple exporter that can create diagram definitions compatible with [C4-PlantUML v2.2.0](https://github.com/plantuml-stdlib/C4-PlantUML). +Following diagram types are supported: - Enterprise Context - System Context - Container - Component -- Dynamic* -- Deployment* - -*..Dynamic and Deployment diagrams are part of an open pull request (from https://github.com/kirchsth/C4-PlantUML). The diagrams can use the definitions via -CustomBaseUrl=https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ or if it is not set then the definition is merged in each diagram) +- Dynamic +- Deployment Simply create your software architecture model and views as usual, and use the [C4PlantUMLWriter](../Structurizr.PlantUML/IO/C4PlantUML/C4PlantUMLWriter.cs) class to export the views. [For example](../Structurizr.Examples/C4PlantUML.cs): @@ -53,13 +51,13 @@ This code will generate and output a PlantUML diagram definition that looks like ' Structurizr.SystemContextView: SystemContext title Software System - System Context -LAYOUT_WITH_LEGEND() - Enterprise_Boundary(SomeEnterprise, "Some Enterprise") { System(SoftwareSystem__33c0d9d, "Software System", "My software system.") Person(User__378734a, "User", "A user of my software system.") Rel_Right(User__378734a, SoftwareSystem__33c0d9d, "Uses") } + +SHOW_LEGEND() @enduml ``` @@ -100,8 +98,6 @@ This code will generate and output a PlantUML diagram definition that looks like ' Structurizr.ContainerView: containers title Software System - Containers -LAYOUT_WITH_LEGEND() - Person(User__378734a, "User", "A user of my software system.") System_Boundary(SoftwareSystem__33c0d9d, "Software System") { ContainerDb(SoftwareSystem__Database__202c666, "Database", "Relational Database Schema", "Stores information") @@ -109,6 +105,8 @@ System_Boundary(SoftwareSystem__33c0d9d, "Software System") { } Rel(User__378734a, SoftwareSystem__WebApplication__2004eee, "uses", "HTTP") Rel_Right(SoftwareSystem__WebApplication__2004eee, SoftwareSystem__Database__202c666, "Reads from and writes to", "JDBC") + +SHOW_LEGEND() @enduml ``` diff --git a/docs/images/c4-plantuml-getting-started.png b/docs/images/c4-plantuml-getting-started.png index cd16e3770e0a8a878c49d1c7c05f03d0b2623be8..0e9afddaf75a5c5424025906a48f523d4c9613b6 100644 GIT binary patch literal 17492 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 literal 13355 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 diff --git a/docs/images/c4-plantuml-getting-started2.png b/docs/images/c4-plantuml-getting-started2.png index b3b63d4bdddfc4e41c7026df274932dae03c1d75..dbf8db0a7e6ae4994fe96842da38bb886bdfec42 100644 GIT binary patch literal 28989 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 $OmWXYuY}d2=w`xn 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 literal 23269 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 KPUfmx-uxdyNo)%M From cc7e5aa476862d6aaa833d54ac49393233a8fda8 Mon Sep 17 00:00:00 2001 From: KIRCHSTH Date: Wed, 23 Jun 2021 18:11:20 +0200 Subject: [PATCH 6/7] BigBankPlc example updated from Structurizr.dotnet C4PlantUmlWriter - RelationshipView.Response (back calls) supported - DeplymentView supports SoftwareSystemInstances --- Structurizr.Examples/BigBankPlc.cs | 488 ++++++++++-------- .../IO/C4PlantUML/C4PlantUmlWriterTests.cs | 77 ++- .../IO/C4PlantUML/C4PlantUmlWriter.cs | 40 +- 3 files changed, 348 insertions(+), 257 deletions(-) diff --git a/Structurizr.Examples/BigBankPlc.cs b/Structurizr.Examples/BigBankPlc.cs index 005e661..b9e7d32 100644 --- a/Structurizr.Examples/BigBankPlc.cs +++ b/Structurizr.Examples/BigBankPlc.cs @@ -5,7 +5,7 @@ namespace Structurizr.Examples { - + ///

/// This is an example workspace to illustrate the key features of Structurizr, /// based around a fictional Internet Banking System for Big Bank plc. @@ -28,238 +28,270 @@ public class BigBankPlc public static Workspace Create() { - Workspace workspace = new Workspace("Big Bank plc", "This is an example workspace to illustrate the key features of Structurizr, based around a fictional online banking system."); - Model model = workspace.Model; - ViewSet views = workspace.Views; - - model.Enterprise = new Enterprise("Big Bank plc"); - - // people and software systems - Person customer = model.AddPerson(Location.External, "Personal Banking Customer", "A customer of the bank, with personal bank accounts."); - - SoftwareSystem internetBankingSystem = model.AddSoftwareSystem(Location.Internal, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments."); - customer.Uses(internetBankingSystem, "Uses"); - - SoftwareSystem mainframeBankingSystem = model.AddSoftwareSystem(Location.Internal, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc."); - mainframeBankingSystem.AddTags(ExistingSystemTag); - internetBankingSystem.Uses(mainframeBankingSystem, "Uses"); - - SoftwareSystem emailSystem = model.AddSoftwareSystem(Location.Internal, "E-mail System", "The internal Microsoft Exchange e-mail system."); - internetBankingSystem.Uses(emailSystem, "Sends e-mail using"); - emailSystem.AddTags(ExistingSystemTag); - emailSystem.Delivers(customer, "Sends e-mails to"); - - SoftwareSystem atm = model.AddSoftwareSystem(Location.Internal, "ATM", "Allows customers to withdraw cash."); - atm.AddTags(ExistingSystemTag); - atm.Uses(mainframeBankingSystem, "Uses"); - customer.Uses(atm, "Withdraws cash using"); - - Person customerServiceStaff = model.AddPerson(Location.Internal, "Customer Service Staff", "Customer service staff within the bank."); - customerServiceStaff.AddTags(BankStaffTag); - customerServiceStaff.Uses(mainframeBankingSystem, "Uses"); - customer.InteractsWith(customerServiceStaff, "Asks questions to", "Telephone"); - - Person backOfficeStaff = model.AddPerson(Location.Internal, "Back Office Staff", "Administration and support staff within the bank."); - backOfficeStaff.AddTags(BankStaffTag); - backOfficeStaff.Uses(mainframeBankingSystem, "Uses"); - - // containers - Container singlePageApplication = internetBankingSystem.AddContainer("Single-Page Application", "Provides all of the Internet banking functionality to customers via their web browser.", "JavaScript and Angular"); - singlePageApplication.AddTags(WebBrowserTag); - Container mobileApp = internetBankingSystem.AddContainer("Mobile App", "Provides a limited subset of the Internet banking functionality to customers via their mobile device.", "Xamarin"); - mobileApp.AddTags(MobileAppTag); - Container webApplication = internetBankingSystem.AddContainer("Web Application", "Delivers the static content and the Internet banking single page application.", "Java and Spring MVC"); - Container apiApplication = internetBankingSystem.AddContainer("API Application", "Provides Internet banking functionality via a JSON/HTTPS API.", "Java and Spring MVC"); - Container database = internetBankingSystem.AddContainer("Database", "Stores user registration information, hashed authentication credentials, access logs, etc.", "Relational Database Schema"); - database.AddTags(DatabaseTag); - - customer.Uses(webApplication, "Uses", "HTTPS"); - customer.Uses(singlePageApplication, "Uses", ""); - customer.Uses(mobileApp, "Uses", ""); - webApplication.Uses(singlePageApplication, "Delivers to the customer's web browser", ""); - apiApplication.Uses(database, "Reads from and writes to", "JDBC"); - apiApplication.Uses(mainframeBankingSystem, "Uses", "XML/HTTPS"); - apiApplication.Uses(emailSystem, "Sends e-mail using", "SMTP"); - - // components - // - for a real-world software system, you would probably want to extract the components using - // - static analysis/reflection rather than manually specifying them all - Component signinController = apiApplication.AddComponent("Sign In Controller", "Allows users to sign in to the Internet Banking System.", "Spring MVC Rest Controller"); - Component accountsSummaryController = apiApplication.AddComponent("Accounts Summary Controller", "Provides customers with a summary of their bank accounts.", "Spring MVC Rest Controller"); - Component resetPasswordController = apiApplication.AddComponent("Reset Password Controller", "Allows users to reset their passwords with a single use URL.", "Spring MVC Rest Controller"); - Component securityComponent = apiApplication.AddComponent("Security Component", "Provides functionality related to signing in, changing passwords, etc.", "Spring Bean"); - Component mainframeBankingSystemFacade = apiApplication.AddComponent("Mainframe Banking System Facade", "A facade onto the mainframe banking system.", "Spring Bean"); - Component emailComponent = apiApplication.AddComponent("E-mail Component", "Sends e-mails to users.", "Spring Bean"); - - apiApplication.Components.Where(c => "Spring MVC Rest Controller".Equals(c.Technology)).ToList().ForEach(c => singlePageApplication.Uses(c, "Makes API calls to", "JSON/HTTPS")); - apiApplication.Components.Where(c => "Spring MVC Rest Controller".Equals(c.Technology)).ToList().ForEach(c => mobileApp.Uses(c, "Makes API calls to", "JSON/HTTPS")); - signinController.Uses(securityComponent, "Uses"); - accountsSummaryController.Uses(mainframeBankingSystemFacade, "Uses"); - resetPasswordController.Uses(securityComponent, "Uses"); - resetPasswordController.Uses(emailComponent, "Uses"); - securityComponent.Uses(database, "Reads from and writes to", "JDBC"); - mainframeBankingSystemFacade.Uses(mainframeBankingSystem, "Uses", "XML/HTTPS"); - emailComponent.Uses(emailSystem, "Sends e-mail using"); - - model.AddImplicitRelationships(); - - // deployment nodes and container instances - DeploymentNode developerLaptop = model.AddDeploymentNode("Development", "Developer Laptop", "A developer laptop.", "Microsoft Windows 10 or Apple macOS"); - DeploymentNode apacheTomcat = developerLaptop.AddDeploymentNode("Docker Container - Web Server", "A Docker container.", "Docker") - .AddDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, DictionaryUtils.Create("Xmx=512M", "Xms=1024M", "Java Version=8")); - apacheTomcat.Add(webApplication); - apacheTomcat.Add(apiApplication); - - developerLaptop.AddDeploymentNode("Docker Container - Database Server", "A Docker container.", "Docker") - .AddDeploymentNode("Database Server", "A development database.", "Oracle 12c") - .Add(database); - - developerLaptop.AddDeploymentNode("Web Browser", "", "Google Chrome, Mozilla Firefox, Apple Safari or Microsoft Edge").Add(singlePageApplication); - - DeploymentNode customerMobileDevice = model.AddDeploymentNode("Live", "Customer's mobile device", "", "Apple iOS or Android"); - customerMobileDevice.Add(mobileApp); - - DeploymentNode customerComputer = model.AddDeploymentNode("Live", "Customer's computer", "", "Microsoft Windows or Apple macOS"); - customerComputer.AddDeploymentNode("Web Browser", "", "Google Chrome, Mozilla Firefox, Apple Safari or Microsoft Edge").Add(singlePageApplication); - - DeploymentNode bigBankDataCenter = model.AddDeploymentNode("Live", "Big Bank plc", "", "Big Bank plc data center"); - - DeploymentNode liveWebServer = bigBankDataCenter.AddDeploymentNode("bigbank-web***", "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", "Ubuntu 16.04 LTS", 4, DictionaryUtils.Create("Location=London and Reading")); - liveWebServer.AddDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, DictionaryUtils.Create("Xmx=512M", "Xms=1024M", "Java Version=8")) - .Add(webApplication); - - DeploymentNode liveApiServer = bigBankDataCenter.AddDeploymentNode("bigbank-api***", "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", "Ubuntu 16.04 LTS", 8, DictionaryUtils.Create("Location=London and Reading")); - liveApiServer.AddDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, DictionaryUtils.Create("Xmx=512M", "Xms=1024M", "Java Version=8")) - .Add(apiApplication); - - DeploymentNode primaryDatabaseServer = bigBankDataCenter.AddDeploymentNode("bigbank-db01", "The primary database server.", "Ubuntu 16.04 LTS", 1, DictionaryUtils.Create("Location=London")) - .AddDeploymentNode("Oracle - Primary", "The primary, live database server.", "Oracle 12c"); - primaryDatabaseServer.Add(database); - - DeploymentNode secondaryDatabaseServer = bigBankDataCenter.AddDeploymentNode("bigbank-db02", "The secondary database server.", "Ubuntu 16.04 LTS", 1, DictionaryUtils.Create("Location=Reading")) - .AddDeploymentNode("Oracle - Secondary", "A secondary, standby database server, used for failover purposes only.", "Oracle 12c"); - ContainerInstance secondaryDatabase = secondaryDatabaseServer.Add(database); - - model.Relationships.Where(r=>r.Destination.Equals(secondaryDatabase)).ToList().ForEach(r=>r.AddTags(FailoverTag)); - Relationship dataReplicationRelationship = primaryDatabaseServer.Uses(secondaryDatabaseServer, "Replicates data to", ""); - secondaryDatabase.AddTags(FailoverTag); - - // views/diagrams - SystemLandscapeView systemLandscapeView = views.CreateSystemLandscapeView("SystemLandscape", "The system landscape diagram for Big Bank plc."); - systemLandscapeView.AddAllElements(); - systemLandscapeView.PaperSize = PaperSize.A5_Landscape; - - SystemContextView systemContextView = views.CreateSystemContextView(internetBankingSystem, "SystemContext", "The system context diagram for the Internet Banking System."); - systemContextView.EnterpriseBoundaryVisible = false; - systemContextView.AddNearestNeighbours(internetBankingSystem); - systemContextView.PaperSize = PaperSize.A5_Landscape; - - ContainerView containerView = views.CreateContainerView(internetBankingSystem, "Containers", "The container diagram for the Internet Banking System."); - containerView.Add(customer); - containerView.AddAllContainers(); - containerView.Add(mainframeBankingSystem); - containerView.Add(emailSystem); - containerView.PaperSize = PaperSize.A5_Landscape; - - ComponentView componentView = views.CreateComponentView(apiApplication, "Components", "The component diagram for the API Application."); - componentView.Add(mobileApp); - componentView.Add(singlePageApplication); - componentView.Add(database); - componentView.AddAllComponents(); - componentView.Add(mainframeBankingSystem); - componentView.Add(emailSystem); - componentView.PaperSize = PaperSize.A5_Landscape; - - systemLandscapeView.AddAnimation(internetBankingSystem, customer, mainframeBankingSystem, emailSystem); - systemLandscapeView.AddAnimation(atm); - systemLandscapeView.AddAnimation(customerServiceStaff, backOfficeStaff); - - systemContextView.AddAnimation(internetBankingSystem); - systemContextView.AddAnimation(customer); - systemContextView.AddAnimation(mainframeBankingSystem); - systemContextView.AddAnimation(emailSystem); - - containerView.AddAnimation(customer, mainframeBankingSystem, emailSystem); - containerView.AddAnimation(webApplication); - containerView.AddAnimation(singlePageApplication); - containerView.AddAnimation(mobileApp); - containerView.AddAnimation(apiApplication); - containerView.AddAnimation(database); - - componentView.AddAnimation(singlePageApplication, mobileApp); - componentView.AddAnimation(signinController, securityComponent, database); - componentView.AddAnimation(accountsSummaryController, mainframeBankingSystemFacade, mainframeBankingSystem); - componentView.AddAnimation(resetPasswordController, emailComponent, database); - - // dynamic diagrams and deployment diagrams are not available with the Free Plan - DynamicView dynamicView = views.CreateDynamicView(apiApplication, "SignIn", "Summarises how the sign in feature works in the single-page application."); - dynamicView.Add(singlePageApplication, "Submits credentials to", signinController); - dynamicView.Add(signinController, "Calls isAuthenticated() on", securityComponent); - dynamicView.Add(securityComponent, "select * from users where username = ?", database); - dynamicView.PaperSize = PaperSize.A5_Landscape; - - DeploymentView developmentDeploymentView = views.CreateDeploymentView(internetBankingSystem, "DevelopmentDeployment", "An example development deployment scenario for the Internet Banking System."); - developmentDeploymentView.Environment = "Development"; - developmentDeploymentView.Add(developerLaptop); - developmentDeploymentView.PaperSize = PaperSize.A5_Landscape; - - DeploymentView liveDeploymentView = views.CreateDeploymentView(internetBankingSystem, "LiveDeployment", "An example live deployment scenario for the Internet Banking System."); - liveDeploymentView.Environment = "Live"; - liveDeploymentView.Add(bigBankDataCenter); - liveDeploymentView.Add(customerMobileDevice); - liveDeploymentView.Add(customerComputer); - liveDeploymentView.Add(dataReplicationRelationship); - liveDeploymentView.PaperSize = PaperSize.A5_Landscape; - - // colours, shapes and other diagram styling - Styles styles = views.Configuration.Styles; - styles.Add(new ElementStyle(Tags.Element) { Color = "#ffffff" }); - styles.Add(new ElementStyle(Tags.SoftwareSystem) { Background = "#1168bd" }); - styles.Add(new ElementStyle(Tags.Container) { Background = "#438dd5" }); - styles.Add(new ElementStyle(Tags.Component) { Background = "#85bbf0", Color = "#000000" }); - styles.Add(new ElementStyle(Tags.Person) { Background = "#08427b", Shape = Shape.Person, FontSize = 22}); - styles.Add(new ElementStyle(ExistingSystemTag) { Background = "#999999"}); - styles.Add(new ElementStyle(BankStaffTag) { Background = "#999999" }); - styles.Add(new ElementStyle(WebBrowserTag) { Shape = Shape.WebBrowser }); - styles.Add(new ElementStyle(MobileAppTag) { Shape = Shape.MobileDeviceLandscape }); - styles.Add(new ElementStyle(DatabaseTag) { Shape = Shape.Cylinder }); - styles.Add(new ElementStyle(FailoverTag) { Opacity = 25 }); - styles.Add(new RelationshipStyle(FailoverTag) { Opacity = 25, Position = 70}); - - // documentation - // - usually the documentation would be included from separate Markdown/AsciiDoc files, but this is just an example - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - template.AddContextSection(internetBankingSystem, Format.Markdown, - "Here is some context about the Internet Banking System...\n" + - "![](embed:SystemLandscape)\n" + - "![](embed:SystemContext)\n" + - "### Internet Banking System\n...\n" + - "### Mainframe Banking System\n...\n"); - template.AddContainersSection(internetBankingSystem, Format.Markdown, - "Here is some information about the containers within the Internet Banking System...\n" + - "![](embed:Containers)\n" + - "### Web Application\n...\n" + - "### Database\n...\n"); - template.AddComponentsSection(webApplication, Format.Markdown, - "Here is some information about the API Application...\n" + - "![](embed:Components)\n" + - "### Sign in process\n" + - "Here is some information about the Sign In Controller, including how the sign in process works...\n" + - "![](embed:SignIn)"); - template.AddDevelopmentEnvironmentSection(internetBankingSystem, Format.AsciiDoc, - "Here is some information about how to set up a development environment for the Internet Banking System...\n" + - "image::embed:DevelopmentDeployment[]"); - template.AddDeploymentSection(internetBankingSystem, Format.AsciiDoc, - "Here is some information about the live deployment environment for the Internet Banking System...\n" + - "image::embed:LiveDeployment[]"); - - return workspace; + Workspace workspace = new Workspace("Big Bank plc", "This is an example workspace to illustrate the key features of Structurizr, based around a fictional online banking system."); + Model model = workspace.Model; + model.ImpliedRelationshipsStrategy = new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy(); + ViewSet views = workspace.Views; + + model.Enterprise = new Enterprise("Big Bank plc"); + + // people and software systems + SoftwareSystem internetBankingSystem = model.AddSoftwareSystem(Location.Internal, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments."); + Person customer = model.AddPerson(Location.External, "Personal Banking Customer", "A customer of the bank, with personal bank accounts."); + + customer.Uses(internetBankingSystem, "Views account balances, and makes payments using"); + + SoftwareSystem mainframeBankingSystem = model.AddSoftwareSystem(Location.Internal, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc."); + mainframeBankingSystem.AddTags(ExistingSystemTag); + internetBankingSystem.Uses(mainframeBankingSystem, "Gets account information from, and makes payments using"); + + SoftwareSystem emailSystem = model.AddSoftwareSystem(Location.Internal, "E-mail System", "The internal Microsoft Exchange e-mail system."); + internetBankingSystem.Uses(emailSystem, "Sends e-mail using"); + emailSystem.AddTags(ExistingSystemTag); + emailSystem.Delivers(customer, "Sends e-mails to"); + + SoftwareSystem atm = model.AddSoftwareSystem(Location.Internal, "ATM", "Allows customers to withdraw cash."); + atm.AddTags(ExistingSystemTag); + atm.Uses(mainframeBankingSystem, "Uses"); + customer.Uses(atm, "Withdraws cash using"); + + Person customerServiceStaff = model.AddPerson(Location.Internal, "Customer Service Staff", "Customer service staff within the bank."); + customerServiceStaff.AddTags(BankStaffTag); + customerServiceStaff.Uses(mainframeBankingSystem, "Uses"); + customer.InteractsWith(customerServiceStaff, "Asks questions to", "Telephone"); + + Person backOfficeStaff = model.AddPerson(Location.Internal, "Back Office Staff", "Administration and support staff within the bank."); + backOfficeStaff.AddTags(BankStaffTag); + backOfficeStaff.Uses(mainframeBankingSystem, "Uses"); + + // containers + Container singlePageApplication = internetBankingSystem.AddContainer("Single-Page Application", "Provides all of the Internet banking functionality to customers via their web browser.", "JavaScript and Angular"); + singlePageApplication.AddTags(WebBrowserTag); + Container mobileApp = internetBankingSystem.AddContainer("Mobile App", "Provides a limited subset of the Internet banking functionality to customers via their mobile device.", "Xamarin"); + mobileApp.AddTags(MobileAppTag); + Container webApplication = internetBankingSystem.AddContainer("Web Application", "Delivers the static content and the Internet banking single page application.", "Java and Spring MVC"); + Container apiApplication = internetBankingSystem.AddContainer("API Application", "Provides Internet banking functionality via a JSON/HTTPS API.", "Java and Spring MVC"); + Container database = internetBankingSystem.AddContainer("Database", "Stores user registration information, hashed authentication credentials, access logs, etc.", "Relational Database Schema"); + database.AddTags(DatabaseTag); + + customer.Uses(webApplication, "Visits bigbank.com/ib using", "HTTPS"); + customer.Uses(singlePageApplication, "Views account balances, and makes payments using", ""); + customer.Uses(mobileApp, "Views account balances, and makes payments using", ""); + webApplication.Uses(singlePageApplication, "Delivers to the customer's web browser", ""); + apiApplication.Uses(database, "Reads from and writes to", "JDBC"); + apiApplication.Uses(mainframeBankingSystem, "Makes API calls to", "XML/HTTPS"); + apiApplication.Uses(emailSystem, "Sends e-mail using", "SMTP"); + + // components + // - for a real-world software system, you would probably want to extract the components using + // - static analysis/reflection rather than manually specifying them all + Component signinController = apiApplication.AddComponent("Sign In Controller", "Allows users to sign in to the Internet Banking System.", "Spring MVC Rest Controller"); + Component accountsSummaryController = apiApplication.AddComponent("Accounts Summary Controller", "Provides customers with a summary of their bank accounts.", "Spring MVC Rest Controller"); + Component resetPasswordController = apiApplication.AddComponent("Reset Password Controller", "Allows users to reset their passwords with a single use URL.", "Spring MVC Rest Controller"); + Component securityComponent = apiApplication.AddComponent("Security Component", "Provides functionality related to signing in, changing passwords, etc.", "Spring Bean"); + Component mainframeBankingSystemFacade = apiApplication.AddComponent("Mainframe Banking System Facade", "A facade onto the mainframe banking system.", "Spring Bean"); + Component emailComponent = apiApplication.AddComponent("E-mail Component", "Sends e-mails to users.", "Spring Bean"); + + apiApplication.Components.Where(c => "Spring MVC Rest Controller".Equals(c.Technology)).ToList().ForEach(c => singlePageApplication.Uses(c, "Makes API calls to", "JSON/HTTPS")); + apiApplication.Components.Where(c => "Spring MVC Rest Controller".Equals(c.Technology)).ToList().ForEach(c => mobileApp.Uses(c, "Makes API calls to", "JSON/HTTPS")); + signinController.Uses(securityComponent, "Uses"); + accountsSummaryController.Uses(mainframeBankingSystemFacade, "Uses"); + resetPasswordController.Uses(securityComponent, "Uses"); + resetPasswordController.Uses(emailComponent, "Uses"); + securityComponent.Uses(database, "Reads from and writes to", "JDBC"); + mainframeBankingSystemFacade.Uses(mainframeBankingSystem, "Uses", "XML/HTTPS"); + emailComponent.Uses(emailSystem, "Sends e-mail using"); + + // deployment nodes and container instances + DeploymentNode developerLaptop = model.AddDeploymentNode("Development", "Developer Laptop", "A developer laptop.", "Microsoft Windows 10 or Apple macOS"); + DeploymentNode apacheTomcat = developerLaptop + .AddDeploymentNode("Docker Container - Web Server", "A Docker container.", "Docker") + .AddDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, DictionaryUtils.Create("Xmx=512M", "Xms=1024M", "Java Version=8")); + ContainerInstance developmentWebApplication = apacheTomcat.Add(webApplication); + ContainerInstance developmentApiApplication = apacheTomcat.Add(apiApplication); + + DeploymentNode bigBankDataCenterForDevelopment = model.AddDeploymentNode("Development", "Big Bank plc", "", "Big Bank plc data center"); + SoftwareSystemInstance developmentMainframeBankingSystem = bigBankDataCenterForDevelopment + .AddDeploymentNode("bigbank-dev001").Add(mainframeBankingSystem); + + ContainerInstance developmentDatabase = developerLaptop + .AddDeploymentNode("Docker Container - Database Server", "A Docker container.", "Docker") + .AddDeploymentNode("Database Server", "A development database.", "Oracle 12c") + .Add(database); + + ContainerInstance developmentSinglePageApplication = developerLaptop + .AddDeploymentNode("Web Browser", "", "Chrome, Firefox, Safari, or Edge") + .Add(singlePageApplication); + + DeploymentNode customerMobileDevice = model.AddDeploymentNode("Live", "Customer's mobile device", "", "Apple iOS or Android"); + ContainerInstance liveMobileApp = customerMobileDevice.Add(mobileApp); + + DeploymentNode customerComputer = model.AddDeploymentNode("Live", "Customer's computer", "", "Microsoft Windows or Apple macOS"); + ContainerInstance liveSinglePageApplication = customerComputer + .AddDeploymentNode("Web Browser", "", "Chrome, Firefox, Safari, or Edge") + .Add(singlePageApplication); + + DeploymentNode bigBankDataCenterForLive = + model.AddDeploymentNode("Live", "Big Bank plc", "", "Big Bank plc data center"); + SoftwareSystemInstance liveMainframeBankingSystem = bigBankDataCenterForLive + .AddDeploymentNode("bigbank-prod001").Add(mainframeBankingSystem); + + DeploymentNode liveWebServer = bigBankDataCenterForLive.AddDeploymentNode("bigbank-web***", "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", "Ubuntu 16.04 LTS", 4, DictionaryUtils.Create("Location=London and Reading")); + ContainerInstance liveWebApplication = liveWebServer.AddDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, DictionaryUtils.Create("Xmx=512M", "Xms=1024M", "Java Version=8")) + .Add(webApplication); + + DeploymentNode liveApiServer = bigBankDataCenterForLive.AddDeploymentNode("bigbank-api***", "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", "Ubuntu 16.04 LTS", 8, DictionaryUtils.Create("Location=London and Reading")); + ContainerInstance liveApiApplication = liveApiServer.AddDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, DictionaryUtils.Create("Xmx=512M", "Xms=1024M", "Java Version=8")) + .Add(apiApplication); + + DeploymentNode primaryDatabaseServer = bigBankDataCenterForLive + .AddDeploymentNode("bigbank-db01", "The primary database server.", "Ubuntu 16.04 LTS", 1, DictionaryUtils.Create("Location=London")) + .AddDeploymentNode("Oracle - Primary", "The primary, live database server.", "Oracle 12c"); + ContainerInstance livePrimaryDatabase = primaryDatabaseServer.Add(database); + + DeploymentNode bigBankdb02 = bigBankDataCenterForLive.AddDeploymentNode("bigbank-db02", "The secondary database server.", "Ubuntu 16.04 LTS", 1, DictionaryUtils.Create("Location=Reading")); + bigBankdb02.AddTags(FailoverTag); + DeploymentNode secondaryDatabaseServer = bigBankdb02.AddDeploymentNode("Oracle - Secondary", "A secondary, standby database server, used for failover purposes only.", "Oracle 12c"); + secondaryDatabaseServer.AddTags(FailoverTag); + ContainerInstance liveSecondaryDatabase = secondaryDatabaseServer.Add(database); + + model.Relationships.Where(r => r.Destination.Equals(liveSecondaryDatabase)).ToList().ForEach(r => r.AddTags(FailoverTag)); + Relationship dataReplicationRelationship = primaryDatabaseServer.Uses(secondaryDatabaseServer, "Replicates data to", ""); + liveSecondaryDatabase.AddTags(FailoverTag); + + // views/diagrams + SystemLandscapeView systemLandscapeView = views.CreateSystemLandscapeView("SystemLandscape", "The system landscape diagram for Big Bank plc."); + systemLandscapeView.AddAllElements(); + systemLandscapeView.PaperSize = PaperSize.A5_Landscape; + + SystemContextView systemContextView = views.CreateSystemContextView(internetBankingSystem, "SystemContext", "The system context diagram for the Internet Banking System."); + systemContextView.EnterpriseBoundaryVisible = false; + systemContextView.AddNearestNeighbours(internetBankingSystem); + systemContextView.PaperSize = PaperSize.A5_Landscape; + + ContainerView containerView = views.CreateContainerView(internetBankingSystem, "Containers", "The container diagram for the Internet Banking System."); + containerView.Add(customer); + containerView.AddAllContainers(); + containerView.Add(mainframeBankingSystem); + containerView.Add(emailSystem); + containerView.PaperSize = PaperSize.A5_Landscape; + + ComponentView componentView = views.CreateComponentView(apiApplication, "Components", "The component diagram for the API Application."); + componentView.Add(mobileApp); + componentView.Add(singlePageApplication); + componentView.Add(database); + componentView.AddAllComponents(); + componentView.Add(mainframeBankingSystem); + componentView.Add(emailSystem); + componentView.PaperSize = PaperSize.A5_Landscape; + + systemLandscapeView.AddAnimation(internetBankingSystem, customer, mainframeBankingSystem, emailSystem); + systemLandscapeView.AddAnimation(atm); + systemLandscapeView.AddAnimation(customerServiceStaff, backOfficeStaff); + + systemContextView.AddAnimation(internetBankingSystem); + systemContextView.AddAnimation(customer); + systemContextView.AddAnimation(mainframeBankingSystem); + systemContextView.AddAnimation(emailSystem); + + containerView.AddAnimation(customer, mainframeBankingSystem, emailSystem); + containerView.AddAnimation(webApplication); + containerView.AddAnimation(singlePageApplication); + containerView.AddAnimation(mobileApp); + containerView.AddAnimation(apiApplication); + containerView.AddAnimation(database); + + componentView.AddAnimation(singlePageApplication, mobileApp); + componentView.AddAnimation(signinController, securityComponent, database); + componentView.AddAnimation(accountsSummaryController, mainframeBankingSystemFacade, mainframeBankingSystem); + componentView.AddAnimation(resetPasswordController, emailComponent, database); + + // dynamic diagrams and deployment diagrams are not available with the Free Plan + DynamicView dynamicView = views.CreateDynamicView(apiApplication, "SignIn", "Summarises how the sign in feature works in the single-page application."); + dynamicView.Add(singlePageApplication, "Submits credentials to", signinController); + dynamicView.Add(signinController, "Validates credentials using", securityComponent); + dynamicView.Add(securityComponent, "select * from users where username = ?", database); + dynamicView.Add(database, "Returns user data to", securityComponent); + dynamicView.Add(securityComponent, "Returns true if the hashed password matches", signinController); + dynamicView.Add(signinController, "Sends back an authentication token to", singlePageApplication); + dynamicView.PaperSize = PaperSize.A5_Landscape; + + DeploymentView developmentDeploymentView = views.CreateDeploymentView(internetBankingSystem, "DevelopmentDeployment", "An example development deployment scenario for the Internet Banking System."); + developmentDeploymentView.Environment = "Development"; + developmentDeploymentView.Add(developerLaptop); + developmentDeploymentView.Add(bigBankDataCenterForDevelopment); + developmentDeploymentView.PaperSize = PaperSize.A5_Landscape; + + developmentDeploymentView.AddAnimation(developmentSinglePageApplication); + developmentDeploymentView.AddAnimation(developmentWebApplication, developmentApiApplication); + developmentDeploymentView.AddAnimation(developmentDatabase); + developmentDeploymentView.AddAnimation(developmentMainframeBankingSystem); + + DeploymentView liveDeploymentView = views.CreateDeploymentView(internetBankingSystem, "LiveDeployment", "An example live deployment scenario for the Internet Banking System."); + liveDeploymentView.Environment = "Live"; + liveDeploymentView.Add(bigBankDataCenterForLive); + liveDeploymentView.Add(customerMobileDevice); + liveDeploymentView.Add(customerComputer); + liveDeploymentView.Add(dataReplicationRelationship); + liveDeploymentView.PaperSize = PaperSize.A5_Landscape; + + liveDeploymentView.AddAnimation(liveSinglePageApplication); + liveDeploymentView.AddAnimation(liveMobileApp); + liveDeploymentView.AddAnimation(liveWebApplication, liveApiApplication); + liveDeploymentView.AddAnimation(livePrimaryDatabase); + liveDeploymentView.AddAnimation(liveSecondaryDatabase); + liveDeploymentView.AddAnimation(liveMainframeBankingSystem); + + // colours, shapes and other diagram styling + Styles styles = views.Configuration.Styles; + styles.Add(new ElementStyle(Tags.SoftwareSystem) { Background = "#1168bd", Color = "#ffffff" }); + styles.Add(new ElementStyle(Tags.Container) { Background = "#438dd5", Color = "#ffffff" }); + styles.Add(new ElementStyle(Tags.Component) { Background = "#85bbf0", Color = "#000000" }); + styles.Add(new ElementStyle(Tags.Person) { Background = "#08427b", Color = "#ffffff", Shape = Shape.Person, FontSize = 22 }); + styles.Add(new ElementStyle(ExistingSystemTag) { Background = "#999999", Color = "#ffffff" }); + styles.Add(new ElementStyle(BankStaffTag) { Background = "#999999", Color = "#ffffff" }); + styles.Add(new ElementStyle(WebBrowserTag) { Shape = Shape.WebBrowser }); + styles.Add(new ElementStyle(MobileAppTag) { Shape = Shape.MobileDeviceLandscape }); + styles.Add(new ElementStyle(DatabaseTag) { Shape = Shape.Cylinder }); + styles.Add(new ElementStyle(FailoverTag) { Opacity = 25 }); + styles.Add(new RelationshipStyle(FailoverTag) {Opacity = 25, Position = 70 }); + + // documentation + // - usually the documentation would be included from separate Markdown/AsciiDoc files, but this is just an example + StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); + template.AddContextSection(internetBankingSystem, Format.Markdown, + "Here is some context about the Internet Banking System...\n" + + "![](embed:SystemLandscape)\n" + + "![](embed:SystemContext)\n" + + "### Internet Banking System\n...\n" + + "### Mainframe Banking System\n...\n"); + template.AddContainersSection(internetBankingSystem, Format.Markdown, + "Here is some information about the containers within the Internet Banking System...\n" + + "![](embed:Containers)\n" + + "### Web Application\n...\n" + + "### Database\n...\n"); + template.AddComponentsSection(webApplication, Format.Markdown, + "Here is some information about the API Application...\n" + + "![](embed:Components)\n" + + "### Sign in process\n" + + "Here is some information about the Sign In Controller, including how the sign in process works...\n" + + "![](embed:SignIn)"); + template.AddDevelopmentEnvironmentSection(internetBankingSystem, Format.AsciiDoc, + "Here is some information about how to set up a development environment for the Internet Banking System...\n" + + "image::embed:DevelopmentDeployment[]"); + template.AddDeploymentSection(internetBankingSystem, Format.AsciiDoc, + "Here is some information about the live deployment environment for the Internet Banking System...\n" + + "image::embed:LiveDeployment[]"); + + return workspace; } - + static void Main() { StructurizrClient structurizrClient = new StructurizrClient(ApiKey, ApiSecret); structurizrClient.PutWorkspace(WorkspaceId, Create()); } + } + } \ No newline at end of file diff --git a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs index 19c5518..22b43f4 100644 --- a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs +++ b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs @@ -29,14 +29,14 @@ public void test_writeBigBankPlcWorkspace() _workspace = BigBankPlc.Create(); AddLayoutDetails(_workspace); -/* - using (var writer = new StringWriter()) - { - new Structurizr.IO.Json.JsonWriter(true).Write(_workspace, writer); - var json = writer.GetStringBuilder().ToString(); - json = json; - } -*/ + /* + using (var writer = new StringWriter()) + { + new Structurizr.IO.Json.JsonWriter(true).Write(_workspace, writer); + var json = writer.GetStringBuilder().ToString(); + json = json; + } + */ _plantUMLWriter.Write(_workspace, _stringWriter); Assert.Equal( @@ -60,10 +60,10 @@ public void test_writeBigBankPlcWorkspace() Rel_Down(CustomerServiceStaff__a35be5, MainframeBankingSystem__f50ffa, ""Uses"") Rel_Up(EmailSystem__2908eb9, PersonalBankingCustomer__9bc576, ""Sends e-mails to"") Rel_Down(InternetBankingSystem__2aef74c, EmailSystem__2908eb9, ""Sends e-mail using"") -Rel_Down(InternetBankingSystem__2aef74c, MainframeBankingSystem__f50ffa, ""Uses"") +Rel_Down(InternetBankingSystem__2aef74c, MainframeBankingSystem__f50ffa, ""Gets account information from, and makes payments using"") Rel(PersonalBankingCustomer__9bc576, ATM__22fc739, ""Withdraws cash using"") Rel(PersonalBankingCustomer__9bc576, CustomerServiceStaff__a35be5, ""Asks questions to"", ""Telephone"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Uses"") +Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Views account balances, and makes payments using"") SHOW_LEGEND() @enduml @@ -80,8 +80,8 @@ title Internet Banking System - System Context Person_Ext(PersonalBankingCustomer__9bc576, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") Rel_Up(EmailSystem__2908eb9, PersonalBankingCustomer__9bc576, ""Sends e-mails to"") Rel_Right(InternetBankingSystem__2aef74c, EmailSystem__2908eb9, ""Sends e-mail using"") -Rel(InternetBankingSystem__2aef74c, MainframeBankingSystem__f50ffa, ""Uses"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Uses"") +Rel(InternetBankingSystem__2aef74c, MainframeBankingSystem__f50ffa, ""Gets account information from, and makes payments using"") +Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Views account balances, and makes payments using"") SHOW_LEGEND() @enduml @@ -104,12 +104,12 @@ title Internet Banking System - Containers } Rel_Left(InternetBankingSystem__APIApplication__2c36bed, InternetBankingSystem__Database__18307f7, ""Reads from and writes to"", ""JDBC"") Rel_Up(InternetBankingSystem__APIApplication__2c36bed, EmailSystem__2908eb9, ""Sends e-mail using"", ""SMTP"") -Rel_Left(InternetBankingSystem__APIApplication__2c36bed, MainframeBankingSystem__f50ffa, ""Uses"", ""XML/HTTPS"") +Rel_Left(InternetBankingSystem__APIApplication__2c36bed, MainframeBankingSystem__f50ffa, ""Makes API calls to"", ""XML/HTTPS"") Rel_Up(EmailSystem__2908eb9, PersonalBankingCustomer__9bc576, ""Sends e-mails to"") Rel(InternetBankingSystem__MobileApp__38a070b, InternetBankingSystem__APIApplication__2c36bed, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__MobileApp__38a070b, ""Uses"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__SinglePageApplication__1414c79, ""Uses"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__WebApplication__1bb919c, ""Uses"", ""HTTPS"") +Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__MobileApp__38a070b, ""Views account balances, and makes payments using"") +Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__SinglePageApplication__1414c79, ""Views account balances, and makes payments using"") +Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__WebApplication__1bb919c, ""Visits bigbank.com/ib using"", ""HTTPS"") Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__2c36bed, ""Makes API calls to"", ""JSON/HTTPS"") Rel_Right(InternetBankingSystem__WebApplication__1bb919c, InternetBankingSystem__SinglePageApplication__1414c79, ""Delivers to the customer's web browser"") @@ -165,8 +165,11 @@ title API Application - Dynamic Component(InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Sign In Controller"", ""Spring MVC Rest Controller"", ""Allows users to sign in to the Internet Banking System."") } RelIndex_Right(""1"", InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Submits credentials to"", ""JSON/HTTPS"") -RelIndex(""2"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Calls isAuthenticated() on"") +RelIndex(""2"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Validates credentials using"") RelIndex_Right(""3"", InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__Database__18307f7, ""select * from users where username = ?"", ""JDBC"") +RelIndex_Left(""4"", InternetBankingSystem__Database__18307f7, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Returns user data to"", ""JDBC"") +RelIndex(""5"", InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Returns true if the hashed password matches"") +RelIndex_Left(""6"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__SinglePageApplication__1414c79, ""Sends back an authentication token to"", ""JSON/HTTPS"") SHOW_LEGEND() @enduml @@ -177,6 +180,11 @@ title API Application - Dynamic ' Structurizr.DeploymentView: DevelopmentDeployment title Internet Banking System - Deployment - Development +Node(BigBankplc__13491b2, ""Big Bank plc"", ""Big Bank plc data center"") { + Node(bigbankdev001__2f4813f, ""bigbank-dev001"") { + System(MainframeBankingSystem1__1b2a42c, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") + } +} Node(DeveloperLaptop__389f399, ""Developer Laptop"", ""Microsoft Windows 10 or Apple macOS"") { Node(DockerContainerWebServer__1b73d2e, ""Docker Container - Web Server"", ""Docker"") { Node(ApacheTomcat__1cc9f55, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { @@ -189,11 +197,12 @@ title Internet Banking System - Deployment - Development ContainerDb(InternetBankingSystem__Database1__3296ca6, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") } } - Node(WebBrowser__3930fd, ""Web Browser"", ""Google Chrome, Mozilla Firefox, Apple Safari or Microsoft Edge"") { + Node(WebBrowser__3930fd, ""Web Browser"", ""Chrome, Firefox, Safari, or Edge"") { Container(InternetBankingSystem__SinglePageApplication1__bbe85d, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") } } Rel(InternetBankingSystem__APIApplication1__1f227f4, InternetBankingSystem__Database1__3296ca6, ""Reads from and writes to"", ""JDBC"") +Rel(InternetBankingSystem__APIApplication1__1f227f4, MainframeBankingSystem1__1b2a42c, ""Makes API calls to"", ""XML/HTTPS"") Rel(InternetBankingSystem__SinglePageApplication1__bbe85d, InternetBankingSystem__APIApplication1__1f227f4, ""Makes API calls to"", ""JSON/HTTPS"") Rel_Up(InternetBankingSystem__WebApplication1__28f79f6, InternetBankingSystem__SinglePageApplication1__bbe85d, ""Delivers to the customer's web browser"") @@ -207,6 +216,9 @@ title Internet Banking System - Deployment - Development title Internet Banking System - Deployment - Live Node(BigBankplc__3ffe15e, ""Big Bank plc"", ""Big Bank plc data center"") { + Node(bigbankprod001__f5e94d, ""bigbank-prod001"") { + System(MainframeBankingSystem1__3db6dd2, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") + } Node(bigbankweb***__3f92e18, ""bigbank-web*** (x4)"", ""Ubuntu 16.04 LTS"") { Node(ApacheTomcat__27b4383, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { Container(InternetBankingSystem__WebApplication1__1720850, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") @@ -229,7 +241,7 @@ title Internet Banking System - Deployment - Live } } Node(Customer'scomputer__2510bf3, ""Customer's computer"", ""Microsoft Windows or Apple macOS"") { - Node(WebBrowser__ba951, ""Web Browser"", ""Google Chrome, Mozilla Firefox, Apple Safari or Microsoft Edge"") { + Node(WebBrowser__ba951, ""Web Browser"", ""Chrome, Firefox, Safari, or Edge"") { Container(InternetBankingSystem__SinglePageApplication1__298b31c, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") } } @@ -238,6 +250,7 @@ title Internet Banking System - Deployment - Live } Rel(InternetBankingSystem__APIApplication1__1408a33, InternetBankingSystem__Database1__1c974ec, ""Reads from and writes to"", ""JDBC"") Rel(InternetBankingSystem__APIApplication1__1408a33, InternetBankingSystem__Database1__d89394, ""Reads from and writes to"", ""JDBC"") +Rel(InternetBankingSystem__APIApplication1__1408a33, MainframeBankingSystem1__3db6dd2, ""Makes API calls to"", ""XML/HTTPS"") Rel(InternetBankingSystem__MobileApp1__d004b3, InternetBankingSystem__APIApplication1__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") Rel_Left(OraclePrimary__19fd8f, OracleSecondary__1c4ec22, ""Replicates data to"") Rel(InternetBankingSystem__SinglePageApplication1__298b31c, InternetBankingSystem__APIApplication1__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") @@ -288,8 +301,10 @@ private void AddLayoutDetails(Workspace workspace) .SetDirection(DirectionValues.Down); // DynamicView - // Rel_Right(InternetBankingSystem__SinglePageApplication, InternetBankingSystem__APIApplication__SignInController, ...) - // Rel_Right(InternetBankingSystem__APIApplication__SecurityComponent, InternetBankingSystem__Database, ...) + // RelIndex_Right(""1"", InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Submits credentials to"", ""JSON/HTTPS"") + // RelIndex_Right(""3"", InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__Database__18307f7, ""select * from users where username = ?"", ""JDBC"") + // Response switch displayed order - RelIndex_Left(""4"", InternetBankingSystem__Database__18307f7, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Returns user data to"", ""JDBC"") + // Response switch displayed order - RelIndex_Left(""6"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__SinglePageApplication__1414c79, ""Sends back an authentication token to"", ""JSON/HTTPS"") var singlePageApplication = workspace.Model.GetElementWithCanonicalOrStaticalName("Container://Internet Banking System.Single-Page Application"); var signInController = @@ -300,12 +315,22 @@ private void AddLayoutDetails(Workspace workspace) database.SetIsDatabase(true); var dynamicView = workspace.Views.DynamicViews.First(); dynamicView.Relationships - .First(r => r.Relationship.SourceId == singlePageApplication.Id && - r.Relationship.DestinationId == signInController.Id) + .First(r => + !(r.Response ?? false) && r.Relationship.SourceId == securityComponent.Id && r.Relationship.DestinationId == database.Id) .SetDirection(DirectionValues.Right); dynamicView.Relationships - .First(r => r.Relationship.SourceId == securityComponent.Id && r.Relationship.DestinationId == database.Id) + .First(r => + !(r.Response ?? false) && r.Relationship.SourceId == singlePageApplication.Id && r.Relationship.DestinationId == signInController.Id) .SetDirection(DirectionValues.Right); + // response swaps display order + dynamicView.Relationships + .First(r => + (r.Response ?? false) && r.Relationship.SourceId == securityComponent.Id && r.Relationship.DestinationId == database.Id) + .SetDirection(DirectionValues.Left); + dynamicView.Relationships + .First(r => + (r.Response ?? false) && r.Relationship.SourceId == singlePageApplication.Id && r.Relationship.DestinationId == signInController.Id) + .SetDirection(DirectionValues.Left); // ContainerView // Rel_Up(InternetBankingSystem__WebApplication, InternetBankingSystem__SinglePageApplication, "Delivers to the customer's web browser") @@ -339,7 +364,7 @@ private void AddLayoutDetails(Workspace workspace) // Rel_Left(Live__BigBankplc__bigbankdb01__OraclePrimary, Live__BigBankplc__bigbankdb02__OracleSecondary, "Replicates data to") // Model is changed that instances are counted per parent orig ...[2] cannot be used anymore, separate per view, full names have to be used - var developmentWebApplication = + var developmentWebApplication = workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Development/Developer Laptop/Docker Container - Web Server/Apache Tomcat/Internet Banking System.Web Application[1]"); var developmentSinglePageApplication = workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Development/Developer Laptop/Web Browser/Internet Banking System.Single-Page Application[1]"); @@ -349,7 +374,7 @@ private void AddLayoutDetails(Workspace workspace) r.Relationship.DestinationId == developmentSinglePageApplication.Id) .SetDirection(DirectionValues.Up); - var liveWebApplication = + var liveWebApplication = workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Live/Big Bank plc/bigbank-web***/Apache Tomcat/Internet Banking System.Web Application[1]"); var liveSinglePageApplication = workspace.Model.GetElementWithCanonicalOrStaticalName("ContainerInstance://Live/Customer's computer/Web Browser/Internet Banking System.Single-Page Application[1]"); diff --git a/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs index 995b4ed..bb5c1ec 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs @@ -6,6 +6,9 @@ // Source base version copied from https://gist.github.com/coldacid/465fa8f3a4cd3fdd7b640a65ad5b86f4 (https://github.com/structurizr/dotnet/issues/47) // kirchsth: Extended with dynamic and deployment view +// kirchsth: updated to update generated source to new C4PlantUml stdlib v2.2.0 (no additional dynamic and deployment view macros are required anymore, calls updated) +// kirchsth: Add tags/styles support +// kirchsth: next planed C4PlantUml stdlib v2.3.0 features can be used with CustomBaseUrl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ namespace Structurizr.IO.C4PlantUML { public class C4PlantUmlWriter : PlantUMLWriterBase @@ -23,12 +26,13 @@ public enum LayoutDirection public LayoutDirection? Layout { get; set; } /// - /// PlantUML-stdlib or https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/release/1-0/ does not support - /// dynamic or deployment diagrams. They can be used via the PlantUML-stdlib and in the diagram added definitions - /// or use a pull-request version which is available at https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ + /// C4PlantUml stdlib v2.2.0 () supports dynamic or deployment diagrams. They can be used via the PlantUML-stdlib and no + /// special CustomBaseUrl is required. + /// Only next stdlib features (like Person shapes) has to be defined via CustomBaseUrl=https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ /// (if the value is empty/null then PlantUML-stdlib with added definitions is used) /// public string CustomBaseUrl { get; set; } = ""; // @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; + public bool EnableNextFeatures { get; set; } = false; protected override void Write(SystemLandscapeView view, TextWriter writer) { @@ -258,6 +262,11 @@ private void Write(DeploymentNode deploymentNode, TextWriter writer, int indentL Write(containerInstance, writer, indentLevel + 1); } + foreach (SoftwareSystemInstance systemInstance in deploymentNode.SoftwareSystemInstances) + { + Write(systemInstance, writer, indentLevel + 1); + } + writer.WriteLine($"{indent}}}"); } @@ -296,6 +305,11 @@ private string TypeOf(Element e) return "Container"; } + if (e is SoftwareSystemInstance) + { + return "Software System"; + } + return ""; } @@ -449,6 +463,12 @@ protected virtual void Write(Element element, TextWriter writer, int indentLevel technology = cmp.Technology ?? ""; isDatabase = cmp.GetIsDatabase(); break; + case SoftwareSystemInstance sysIn: + macro = "System"; + title = sysIn.SoftwareSystem.Name; + description = sysIn.SoftwareSystem.Description; + external = sysIn.SoftwareSystem.Location == Location.External; + break; case ContainerInstance cntIn: macro = "Container"; title = cntIn.Container.Name; @@ -496,6 +516,13 @@ protected virtual void Write(RelationshipView relationshipView, TextWriter write label = advancedDescription ?? relationship.Description ?? "", tech = !string.IsNullOrWhiteSpace(relationship.Technology) ? relationship.Technology : null; + if (relationshipView.Response ?? false) + { + var swap = source; + source = dest; + dest = swap; + } + var macro = GetSpecificLayoutMacro(relationshipView); writer.Write($"{macro}({source}, {dest}, \"{EscapeText(label)}\""); @@ -519,6 +546,13 @@ protected virtual void WriteDynamicInteraction(RelationshipView relationshipView dest = TokenizeName(relationship.Destination), tech = !string.IsNullOrWhiteSpace(relationship.Technology) ? relationship.Technology : null; + if (relationshipView.Response ?? false) + { + var swap = source; + source = dest; + dest = swap; + } + var macro = GetSpecificLayoutMacro(relationshipView); macro = "RelIndex" + macro.Substring("Rel".Length); From adebc2561f3ff9361f3074cca186e60c5ef79bf1 Mon Sep 17 00:00:00 2001 From: KIRCHSTH Date: Mon, 28 Jun 2021 17:05:57 +0200 Subject: [PATCH 7/7] C4PlantUmlWriter: Update with v2.2.0 tags and styles (and with EnableNextFeatures the v2.3.0 person shape; rounded boxes, dotted lines...) --- Structurizr.Examples/C4PlantUML.cs | 33 +- .../IO/C4PlantUML/C4PlantUmlWriterTests.cs | 388 ++++++++++-------- .../IO/C4PlantUML/C4PlantUmlWriter.cs | 253 ++++++++++-- .../IO/C4PlantUML/IPlantUMLWriter.cs | 3 +- .../IO/C4PlantUML/PlantUMLWriterBase.cs | 47 ++- Structurizr.sln | 5 +- docs/c4-plantuml.md | 77 +++- docs/images/c4-plantuml-getting-started2.png | Bin 28989 -> 33142 bytes docs/images/c4-plantuml-getting-started3.png | Bin 0 -> 37785 bytes 9 files changed, 560 insertions(+), 246 deletions(-) create mode 100644 docs/images/c4-plantuml-getting-started3.png diff --git a/Structurizr.Examples/C4PlantUML.cs b/Structurizr.Examples/C4PlantUML.cs index 44fdfc3..7ea8656 100644 --- a/Structurizr.Examples/C4PlantUML.cs +++ b/Structurizr.Examples/C4PlantUML.cs @@ -42,21 +42,48 @@ static void Main() Console.WriteLine(stringWriter.ToString()); } - + // + // Mark containers or components as database or via tags + // Container webApplication = softwareSystem.AddContainer("Web Application", "Delivers content", "Java and spring MVC"); + // Additional tag element + webApplication.Tags = "Single Page App"; + Container database = softwareSystem.AddContainer("Database", "Stores information", "Relational Database Schema"); // Additional mark it as database database.SetIsDatabase(true); - user.Uses(webApplication, "uses", "HTTP"); + + var httpCall = user.Uses(webApplication, "uses", "HTTP"); + // Additional tag relationship + httpCall.Tags = "via firewall"; + webApplication.Uses(database, "Reads from and writes to", "JDBC").SetDirection(DirectionValues.Right); + // add corresponding styles + var styles = views.Configuration.Styles; + styles.Add(new ElementStyle("Single Page App") {Background = "#5F9061", Stroke = "#2E4F2E", Color = "#FFFFFF", Shape = Shape.RoundedBox }); // rounded box is supported with next version see below + styles.Add(new RelationshipStyle("via firewall") {Color = "#B40404", Dashed = true }); // dashed is supported with next version see below + var containerView = views.CreateContainerView(softwareSystem, "containers", ""); containerView.AddAllElements(); using (var stringWriter = new StringWriter()) { var plantUmlWriter = new C4PlantUmlWriter(); - plantUmlWriter.Write(containerView, stringWriter); + plantUmlWriter.Write(containerView, workspace.Views.Configuration, stringWriter); + Console.WriteLine(stringWriter.ToString()); + } + + // + // Use features of the next planned C4-PlantUML version (v2.3.0 ?) + // + using (var stringWriter = new StringWriter()) + { + var plantUmlWriter = new C4PlantUmlWriter(); + plantUmlWriter.EnableNextFeatures = true; + plantUmlWriter.CustomBaseUrl = "https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; + + plantUmlWriter.Write(containerView, workspace.Views.Configuration, stringWriter); Console.WriteLine(stringWriter.ToString()); } } diff --git a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs index 22b43f4..e595fc4 100644 --- a/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs +++ b/Structurizr.PlantUML.Tests/IO/C4PlantUML/C4PlantUmlWriterTests.cs @@ -46,24 +46,31 @@ public void test_writeBigBankPlcWorkspace() ' Structurizr.SystemLandscapeView: SystemLandscape title System Landscape for Big Bank plc -Person_Ext(PersonalBankingCustomer__9bc576, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") +UpdateElementStyle(system, $bgColor = ""#1168bd"", $fontColor = ""#ffffff"", $borderColor = ""#1168bd"") +UpdateElementStyle(container, $bgColor = ""#438dd5"", $fontColor = ""#ffffff"", $borderColor = ""#438dd5"") +UpdateElementStyle(component, $bgColor = ""#85bbf0"", $fontColor = ""#000000"", $borderColor = ""#85bbf0"") +UpdateElementStyle(person, $bgColor = ""#08427b"", $fontColor = ""#ffffff"", $borderColor = ""#08427b"") +AddElementTag(Existing System, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") +AddElementTag(Bank Staff, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") + +Person_Ext(PersonalBankingCustomer__HASH0, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") Enterprise_Boundary(BigBankplc, ""Big Bank plc"") { - Person(BackOfficeStaff__5f761d, ""Back Office Staff"", ""Administration and support staff within the bank."") - Person(CustomerServiceStaff__a35be5, ""Customer Service Staff"", ""Customer service staff within the bank."") - System(ATM__22fc739, ""ATM"", ""Allows customers to withdraw cash."") - System(EmailSystem__2908eb9, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."") - System(InternetBankingSystem__2aef74c, ""Internet Banking System"", ""Allows customers to view information about their bank accounts, and make payments."") - System(MainframeBankingSystem__f50ffa, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") + Person(BackOfficeStaff__HASH1, ""Back Office Staff"", ""Administration and support staff within the bank."", $tags=""Bank Staff"") + Person(CustomerServiceStaff__HASH2, ""Customer Service Staff"", ""Customer service staff within the bank."", $tags=""Bank Staff"") + System(ATM__HASH3, ""ATM"", ""Allows customers to withdraw cash."", $tags=""Existing System"") + System(EmailSystem__HASH4, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."", $tags=""Existing System"") + System(InternetBankingSystem__HASH5, ""Internet Banking System"", ""Allows customers to view information about their bank accounts, and make payments."") + System(MainframeBankingSystem__HASH6, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."", $tags=""Existing System"") } -Rel_Down(ATM__22fc739, MainframeBankingSystem__f50ffa, ""Uses"") -Rel_Down(BackOfficeStaff__5f761d, MainframeBankingSystem__f50ffa, ""Uses"") -Rel_Down(CustomerServiceStaff__a35be5, MainframeBankingSystem__f50ffa, ""Uses"") -Rel_Up(EmailSystem__2908eb9, PersonalBankingCustomer__9bc576, ""Sends e-mails to"") -Rel_Down(InternetBankingSystem__2aef74c, EmailSystem__2908eb9, ""Sends e-mail using"") -Rel_Down(InternetBankingSystem__2aef74c, MainframeBankingSystem__f50ffa, ""Gets account information from, and makes payments using"") -Rel(PersonalBankingCustomer__9bc576, ATM__22fc739, ""Withdraws cash using"") -Rel(PersonalBankingCustomer__9bc576, CustomerServiceStaff__a35be5, ""Asks questions to"", ""Telephone"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Views account balances, and makes payments using"") +Rel_Down(ATM__HASH3, MainframeBankingSystem__HASH6, ""Uses"") +Rel_Down(BackOfficeStaff__HASH1, MainframeBankingSystem__HASH6, ""Uses"") +Rel_Down(CustomerServiceStaff__HASH2, MainframeBankingSystem__HASH6, ""Uses"") +Rel_Up(EmailSystem__HASH4, PersonalBankingCustomer__HASH0, ""Sends e-mails to"") +Rel_Down(InternetBankingSystem__HASH5, EmailSystem__HASH4, ""Sends e-mail using"") +Rel_Down(InternetBankingSystem__HASH5, MainframeBankingSystem__HASH6, ""Gets account information from, and makes payments using"") +Rel(PersonalBankingCustomer__HASH0, ATM__HASH3, ""Withdraws cash using"") +Rel(PersonalBankingCustomer__HASH0, CustomerServiceStaff__HASH2, ""Asks questions to"", ""Telephone"") +Rel(PersonalBankingCustomer__HASH0, InternetBankingSystem__HASH5, ""Views account balances, and makes payments using"") SHOW_LEGEND() @enduml @@ -74,14 +81,21 @@ public void test_writeBigBankPlcWorkspace() ' Structurizr.SystemContextView: SystemContext title Internet Banking System - System Context -System(EmailSystem__2908eb9, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."") -System(InternetBankingSystem__2aef74c, ""Internet Banking System"", ""Allows customers to view information about their bank accounts, and make payments."") -System(MainframeBankingSystem__f50ffa, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") -Person_Ext(PersonalBankingCustomer__9bc576, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") -Rel_Up(EmailSystem__2908eb9, PersonalBankingCustomer__9bc576, ""Sends e-mails to"") -Rel_Right(InternetBankingSystem__2aef74c, EmailSystem__2908eb9, ""Sends e-mail using"") -Rel(InternetBankingSystem__2aef74c, MainframeBankingSystem__f50ffa, ""Gets account information from, and makes payments using"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__2aef74c, ""Views account balances, and makes payments using"") +UpdateElementStyle(system, $bgColor = ""#1168bd"", $fontColor = ""#ffffff"", $borderColor = ""#1168bd"") +UpdateElementStyle(container, $bgColor = ""#438dd5"", $fontColor = ""#ffffff"", $borderColor = ""#438dd5"") +UpdateElementStyle(component, $bgColor = ""#85bbf0"", $fontColor = ""#000000"", $borderColor = ""#85bbf0"") +UpdateElementStyle(person, $bgColor = ""#08427b"", $fontColor = ""#ffffff"", $borderColor = ""#08427b"") +AddElementTag(Existing System, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") +AddElementTag(Bank Staff, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") + +System(EmailSystem__HASH4, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."", $tags=""Existing System"") +System(InternetBankingSystem__HASH5, ""Internet Banking System"", ""Allows customers to view information about their bank accounts, and make payments."") +System(MainframeBankingSystem__HASH6, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."", $tags=""Existing System"") +Person_Ext(PersonalBankingCustomer__HASH0, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") +Rel_Up(EmailSystem__HASH4, PersonalBankingCustomer__HASH0, ""Sends e-mails to"") +Rel_Right(InternetBankingSystem__HASH5, EmailSystem__HASH4, ""Sends e-mail using"") +Rel(InternetBankingSystem__HASH5, MainframeBankingSystem__HASH6, ""Gets account information from, and makes payments using"") +Rel(PersonalBankingCustomer__HASH0, InternetBankingSystem__HASH5, ""Views account balances, and makes payments using"") SHOW_LEGEND() @enduml @@ -92,26 +106,33 @@ title Internet Banking System - System Context ' Structurizr.ContainerView: Containers title Internet Banking System - Containers -System(EmailSystem__2908eb9, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."") -System(MainframeBankingSystem__f50ffa, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") -Person_Ext(PersonalBankingCustomer__9bc576, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") -System_Boundary(InternetBankingSystem__2aef74c, ""Internet Banking System"") { - Container(InternetBankingSystem__APIApplication__2c36bed, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."") - ContainerDb(InternetBankingSystem__Database__18307f7, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") - Container(InternetBankingSystem__MobileApp__38a070b, ""Mobile App"", ""Xamarin"", ""Provides a limited subset of the Internet banking functionality to customers via their mobile device."") - Container(InternetBankingSystem__SinglePageApplication__1414c79, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") - Container(InternetBankingSystem__WebApplication__1bb919c, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") +UpdateElementStyle(system, $bgColor = ""#1168bd"", $fontColor = ""#ffffff"", $borderColor = ""#1168bd"") +UpdateElementStyle(container, $bgColor = ""#438dd5"", $fontColor = ""#ffffff"", $borderColor = ""#438dd5"") +UpdateElementStyle(component, $bgColor = ""#85bbf0"", $fontColor = ""#000000"", $borderColor = ""#85bbf0"") +UpdateElementStyle(person, $bgColor = ""#08427b"", $fontColor = ""#ffffff"", $borderColor = ""#08427b"") +AddElementTag(Existing System, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") +AddElementTag(Bank Staff, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") + +System(EmailSystem__HASH4, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."", $tags=""Existing System"") +System(MainframeBankingSystem__HASH6, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."", $tags=""Existing System"") +Person_Ext(PersonalBankingCustomer__HASH0, ""Personal Banking Customer"", ""A customer of the bank, with personal bank accounts."") +System_Boundary(InternetBankingSystem__HASH5, ""Internet Banking System"") { + Container(InternetBankingSystem__APIApplication__HASH7, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."") + ContainerDb(InternetBankingSystem__Database__HASH8, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."", $tags=""Database"") + Container(InternetBankingSystem__MobileApp__HASH9, ""Mobile App"", ""Xamarin"", ""Provides a limited subset of the Internet banking functionality to customers via their mobile device."", $tags=""Mobile App"") + Container(InternetBankingSystem__SinglePageApplication__HASH10, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."", $tags=""Web Browser"") + Container(InternetBankingSystem__WebApplication__HASH11, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") } -Rel_Left(InternetBankingSystem__APIApplication__2c36bed, InternetBankingSystem__Database__18307f7, ""Reads from and writes to"", ""JDBC"") -Rel_Up(InternetBankingSystem__APIApplication__2c36bed, EmailSystem__2908eb9, ""Sends e-mail using"", ""SMTP"") -Rel_Left(InternetBankingSystem__APIApplication__2c36bed, MainframeBankingSystem__f50ffa, ""Makes API calls to"", ""XML/HTTPS"") -Rel_Up(EmailSystem__2908eb9, PersonalBankingCustomer__9bc576, ""Sends e-mails to"") -Rel(InternetBankingSystem__MobileApp__38a070b, InternetBankingSystem__APIApplication__2c36bed, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__MobileApp__38a070b, ""Views account balances, and makes payments using"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__SinglePageApplication__1414c79, ""Views account balances, and makes payments using"") -Rel(PersonalBankingCustomer__9bc576, InternetBankingSystem__WebApplication__1bb919c, ""Visits bigbank.com/ib using"", ""HTTPS"") -Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__2c36bed, ""Makes API calls to"", ""JSON/HTTPS"") -Rel_Right(InternetBankingSystem__WebApplication__1bb919c, InternetBankingSystem__SinglePageApplication__1414c79, ""Delivers to the customer's web browser"") +Rel_Left(InternetBankingSystem__APIApplication__HASH7, InternetBankingSystem__Database__HASH8, ""Reads from and writes to"", ""JDBC"") +Rel_Up(InternetBankingSystem__APIApplication__HASH7, EmailSystem__HASH4, ""Sends e-mail using"", ""SMTP"") +Rel_Left(InternetBankingSystem__APIApplication__HASH7, MainframeBankingSystem__HASH6, ""Makes API calls to"", ""XML/HTTPS"") +Rel_Up(EmailSystem__HASH4, PersonalBankingCustomer__HASH0, ""Sends e-mails to"") +Rel(InternetBankingSystem__MobileApp__HASH9, InternetBankingSystem__APIApplication__HASH7, ""Makes API calls to"", ""JSON/HTTPS"") +Rel(PersonalBankingCustomer__HASH0, InternetBankingSystem__MobileApp__HASH9, ""Views account balances, and makes payments using"") +Rel(PersonalBankingCustomer__HASH0, InternetBankingSystem__SinglePageApplication__HASH10, ""Views account balances, and makes payments using"") +Rel(PersonalBankingCustomer__HASH0, InternetBankingSystem__WebApplication__HASH11, ""Visits bigbank.com/ib using"", ""HTTPS"") +Rel(InternetBankingSystem__SinglePageApplication__HASH10, InternetBankingSystem__APIApplication__HASH7, ""Makes API calls to"", ""JSON/HTTPS"") +Rel_Right(InternetBankingSystem__WebApplication__HASH11, InternetBankingSystem__SinglePageApplication__HASH10, ""Delivers to the customer's web browser"") SHOW_LEGEND() @enduml @@ -122,32 +143,39 @@ title Internet Banking System - Containers ' Structurizr.ComponentView: Components title Internet Banking System - API Application - Components -ContainerDb(InternetBankingSystem__Database__18307f7, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") -System(EmailSystem__2908eb9, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."") -System(MainframeBankingSystem__f50ffa, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") -Container(InternetBankingSystem__MobileApp__38a070b, ""Mobile App"", ""Xamarin"", ""Provides a limited subset of the Internet banking functionality to customers via their mobile device."") -Container(InternetBankingSystem__SinglePageApplication__1414c79, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") -Container_Boundary(InternetBankingSystem__APIApplication__2c36bed, ""API Application"") { - Component(InternetBankingSystem__APIApplication__AccountsSummaryController__3f81fb2, ""Accounts Summary Controller"", ""Spring MVC Rest Controller"", ""Provides customers with a summary of their bank accounts."") - Component(InternetBankingSystem__APIApplication__EmailComponent__24ec565, ""E-mail Component"", ""Spring Bean"", ""Sends e-mails to users."") - Component(InternetBankingSystem__APIApplication__MainframeBankingSystemFacade__2493dd9, ""Mainframe Banking System Facade"", ""Spring Bean"", ""A facade onto the mainframe banking system."") - Component(InternetBankingSystem__APIApplication__ResetPasswordController__23f0eac, ""Reset Password Controller"", ""Spring MVC Rest Controller"", ""Allows users to reset their passwords with a single use URL."") - Component(InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Security Component"", ""Spring Bean"", ""Provides functionality related to signing in, changing passwords, etc."") - Component(InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Sign In Controller"", ""Spring MVC Rest Controller"", ""Allows users to sign in to the Internet Banking System."") +UpdateElementStyle(system, $bgColor = ""#1168bd"", $fontColor = ""#ffffff"", $borderColor = ""#1168bd"") +UpdateElementStyle(container, $bgColor = ""#438dd5"", $fontColor = ""#ffffff"", $borderColor = ""#438dd5"") +UpdateElementStyle(component, $bgColor = ""#85bbf0"", $fontColor = ""#000000"", $borderColor = ""#85bbf0"") +UpdateElementStyle(person, $bgColor = ""#08427b"", $fontColor = ""#ffffff"", $borderColor = ""#08427b"") +AddElementTag(Existing System, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") +AddElementTag(Bank Staff, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") + +ContainerDb(InternetBankingSystem__Database__HASH8, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."", $tags=""Database"") +System(EmailSystem__HASH4, ""E-mail System"", ""The internal Microsoft Exchange e-mail system."", $tags=""Existing System"") +System(MainframeBankingSystem__HASH6, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."", $tags=""Existing System"") +Container(InternetBankingSystem__MobileApp__HASH9, ""Mobile App"", ""Xamarin"", ""Provides a limited subset of the Internet banking functionality to customers via their mobile device."", $tags=""Mobile App"") +Container(InternetBankingSystem__SinglePageApplication__HASH10, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."", $tags=""Web Browser"") +Container_Boundary(InternetBankingSystem__APIApplication__HASH7, ""API Application"") { + Component(InternetBankingSystem__APIApplication__AccountsSummaryController__HASH12, ""Accounts Summary Controller"", ""Spring MVC Rest Controller"", ""Provides customers with a summary of their bank accounts."") + Component(InternetBankingSystem__APIApplication__EmailComponent__HASH13, ""E-mail Component"", ""Spring Bean"", ""Sends e-mails to users."") + Component(InternetBankingSystem__APIApplication__MainframeBankingSystemFacade__HASH14, ""Mainframe Banking System Facade"", ""Spring Bean"", ""A facade onto the mainframe banking system."") + Component(InternetBankingSystem__APIApplication__ResetPasswordController__HASH15, ""Reset Password Controller"", ""Spring MVC Rest Controller"", ""Allows users to reset their passwords with a single use URL."") + Component(InternetBankingSystem__APIApplication__SecurityComponent__HASH16, ""Security Component"", ""Spring Bean"", ""Provides functionality related to signing in, changing passwords, etc."") + Component(InternetBankingSystem__APIApplication__SignInController__HASH17, ""Sign In Controller"", ""Spring MVC Rest Controller"", ""Allows users to sign in to the Internet Banking System."") } -Rel(InternetBankingSystem__APIApplication__AccountsSummaryController__3f81fb2, InternetBankingSystem__APIApplication__MainframeBankingSystemFacade__2493dd9, ""Uses"") -Rel(InternetBankingSystem__APIApplication__EmailComponent__24ec565, EmailSystem__2908eb9, ""Sends e-mail using"") -Rel(InternetBankingSystem__APIApplication__MainframeBankingSystemFacade__2493dd9, MainframeBankingSystem__f50ffa, ""Uses"", ""XML/HTTPS"") -Rel(InternetBankingSystem__MobileApp__38a070b, InternetBankingSystem__APIApplication__AccountsSummaryController__3f81fb2, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(InternetBankingSystem__MobileApp__38a070b, InternetBankingSystem__APIApplication__ResetPasswordController__23f0eac, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(InternetBankingSystem__MobileApp__38a070b, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(InternetBankingSystem__APIApplication__ResetPasswordController__23f0eac, InternetBankingSystem__APIApplication__EmailComponent__24ec565, ""Uses"") -Rel(InternetBankingSystem__APIApplication__ResetPasswordController__23f0eac, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Uses"") -Rel(InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__Database__18307f7, ""Reads from and writes to"", ""JDBC"") -Rel(InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Uses"") -Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__AccountsSummaryController__3f81fb2, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__ResetPasswordController__23f0eac, ""Makes API calls to"", ""JSON/HTTPS"") -Rel(InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Makes API calls to"", ""JSON/HTTPS"") +Rel(InternetBankingSystem__APIApplication__AccountsSummaryController__HASH12, InternetBankingSystem__APIApplication__MainframeBankingSystemFacade__HASH14, ""Uses"") +Rel(InternetBankingSystem__APIApplication__EmailComponent__HASH13, EmailSystem__HASH4, ""Sends e-mail using"") +Rel(InternetBankingSystem__APIApplication__MainframeBankingSystemFacade__HASH14, MainframeBankingSystem__HASH6, ""Uses"", ""XML/HTTPS"") +Rel(InternetBankingSystem__MobileApp__HASH9, InternetBankingSystem__APIApplication__AccountsSummaryController__HASH12, ""Makes API calls to"", ""JSON/HTTPS"") +Rel(InternetBankingSystem__MobileApp__HASH9, InternetBankingSystem__APIApplication__ResetPasswordController__HASH15, ""Makes API calls to"", ""JSON/HTTPS"") +Rel(InternetBankingSystem__MobileApp__HASH9, InternetBankingSystem__APIApplication__SignInController__HASH17, ""Makes API calls to"", ""JSON/HTTPS"") +Rel(InternetBankingSystem__APIApplication__ResetPasswordController__HASH15, InternetBankingSystem__APIApplication__EmailComponent__HASH13, ""Uses"") +Rel(InternetBankingSystem__APIApplication__ResetPasswordController__HASH15, InternetBankingSystem__APIApplication__SecurityComponent__HASH16, ""Uses"") +Rel(InternetBankingSystem__APIApplication__SecurityComponent__HASH16, InternetBankingSystem__Database__HASH8, ""Reads from and writes to"", ""JDBC"") +Rel(InternetBankingSystem__APIApplication__SignInController__HASH17, InternetBankingSystem__APIApplication__SecurityComponent__HASH16, ""Uses"") +Rel(InternetBankingSystem__SinglePageApplication__HASH10, InternetBankingSystem__APIApplication__AccountsSummaryController__HASH12, ""Makes API calls to"", ""JSON/HTTPS"") +Rel(InternetBankingSystem__SinglePageApplication__HASH10, InternetBankingSystem__APIApplication__ResetPasswordController__HASH15, ""Makes API calls to"", ""JSON/HTTPS"") +Rel(InternetBankingSystem__SinglePageApplication__HASH10, InternetBankingSystem__APIApplication__SignInController__HASH17, ""Makes API calls to"", ""JSON/HTTPS"") SHOW_LEGEND() @enduml @@ -158,18 +186,25 @@ title Internet Banking System - API Application - Components ' Structurizr.DynamicView: SignIn title API Application - Dynamic -ContainerDb(InternetBankingSystem__Database__18307f7, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") -Container(InternetBankingSystem__SinglePageApplication__1414c79, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") -Container_Boundary(InternetBankingSystem__APIApplication__2c36bed, ""API Application"") { - Component(InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Security Component"", ""Spring Bean"", ""Provides functionality related to signing in, changing passwords, etc."") - Component(InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Sign In Controller"", ""Spring MVC Rest Controller"", ""Allows users to sign in to the Internet Banking System."") +UpdateElementStyle(system, $bgColor = ""#1168bd"", $fontColor = ""#ffffff"", $borderColor = ""#1168bd"") +UpdateElementStyle(container, $bgColor = ""#438dd5"", $fontColor = ""#ffffff"", $borderColor = ""#438dd5"") +UpdateElementStyle(component, $bgColor = ""#85bbf0"", $fontColor = ""#000000"", $borderColor = ""#85bbf0"") +UpdateElementStyle(person, $bgColor = ""#08427b"", $fontColor = ""#ffffff"", $borderColor = ""#08427b"") +AddElementTag(Existing System, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") +AddElementTag(Bank Staff, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") + +ContainerDb(InternetBankingSystem__Database__HASH8, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."", $tags=""Database"") +Container(InternetBankingSystem__SinglePageApplication__HASH10, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."", $tags=""Web Browser"") +Container_Boundary(InternetBankingSystem__APIApplication__HASH7, ""API Application"") { + Component(InternetBankingSystem__APIApplication__SecurityComponent__HASH16, ""Security Component"", ""Spring Bean"", ""Provides functionality related to signing in, changing passwords, etc."") + Component(InternetBankingSystem__APIApplication__SignInController__HASH17, ""Sign In Controller"", ""Spring MVC Rest Controller"", ""Allows users to sign in to the Internet Banking System."") } -RelIndex_Right(""1"", InternetBankingSystem__SinglePageApplication__1414c79, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Submits credentials to"", ""JSON/HTTPS"") -RelIndex(""2"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Validates credentials using"") -RelIndex_Right(""3"", InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__Database__18307f7, ""select * from users where username = ?"", ""JDBC"") -RelIndex_Left(""4"", InternetBankingSystem__Database__18307f7, InternetBankingSystem__APIApplication__SecurityComponent__a4474, ""Returns user data to"", ""JDBC"") -RelIndex(""5"", InternetBankingSystem__APIApplication__SecurityComponent__a4474, InternetBankingSystem__APIApplication__SignInController__22cc62b, ""Returns true if the hashed password matches"") -RelIndex_Left(""6"", InternetBankingSystem__APIApplication__SignInController__22cc62b, InternetBankingSystem__SinglePageApplication__1414c79, ""Sends back an authentication token to"", ""JSON/HTTPS"") +RelIndex_Right(""1"", InternetBankingSystem__SinglePageApplication__HASH10, InternetBankingSystem__APIApplication__SignInController__HASH17, ""Submits credentials to"", ""JSON/HTTPS"") +RelIndex(""2"", InternetBankingSystem__APIApplication__SignInController__HASH17, InternetBankingSystem__APIApplication__SecurityComponent__HASH16, ""Validates credentials using"") +RelIndex_Right(""3"", InternetBankingSystem__APIApplication__SecurityComponent__HASH16, InternetBankingSystem__Database__HASH8, ""select * from users where username = ?"", ""JDBC"") +RelIndex_Left(""4"", InternetBankingSystem__Database__HASH8, InternetBankingSystem__APIApplication__SecurityComponent__HASH16, ""Returns user data to"", ""JDBC"", $tags=""Back"") +RelIndex(""5"", InternetBankingSystem__APIApplication__SecurityComponent__HASH16, InternetBankingSystem__APIApplication__SignInController__HASH17, ""Returns true if the hashed password matches"", $tags=""Back"") +RelIndex_Left(""6"", InternetBankingSystem__APIApplication__SignInController__HASH17, InternetBankingSystem__SinglePageApplication__HASH10, ""Sends back an authentication token to"", ""JSON/HTTPS"", $tags=""Back"") SHOW_LEGEND() @enduml @@ -180,31 +215,38 @@ title API Application - Dynamic ' Structurizr.DeploymentView: DevelopmentDeployment title Internet Banking System - Deployment - Development -Node(BigBankplc__13491b2, ""Big Bank plc"", ""Big Bank plc data center"") { - Node(bigbankdev001__2f4813f, ""bigbank-dev001"") { - System(MainframeBankingSystem1__1b2a42c, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") +UpdateElementStyle(system, $bgColor = ""#1168bd"", $fontColor = ""#ffffff"", $borderColor = ""#1168bd"") +UpdateElementStyle(container, $bgColor = ""#438dd5"", $fontColor = ""#ffffff"", $borderColor = ""#438dd5"") +UpdateElementStyle(component, $bgColor = ""#85bbf0"", $fontColor = ""#000000"", $borderColor = ""#85bbf0"") +UpdateElementStyle(person, $bgColor = ""#08427b"", $fontColor = ""#ffffff"", $borderColor = ""#08427b"") +AddElementTag(Existing System, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") +AddElementTag(Bank Staff, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") + +Node(BigBankplc__HASH18, ""Big Bank plc"", ""Big Bank plc data center"") { + Node(bigbankdev001__HASH19, ""bigbank-dev001"") { + System(MainframeBankingSystem1__HASH20, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."", $tags=""Software System Instance"") } } -Node(DeveloperLaptop__389f399, ""Developer Laptop"", ""Microsoft Windows 10 or Apple macOS"") { - Node(DockerContainerWebServer__1b73d2e, ""Docker Container - Web Server"", ""Docker"") { - Node(ApacheTomcat__1cc9f55, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(InternetBankingSystem__WebApplication1__28f79f6, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") - Container(InternetBankingSystem__APIApplication1__1f227f4, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."") +Node(DeveloperLaptop__HASH21, ""Developer Laptop"", ""Microsoft Windows 10 or Apple macOS"") { + Node(DockerContainerWebServer__HASH22, ""Docker Container - Web Server"", ""Docker"") { + Node(ApacheTomcat__HASH23, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { + Container(InternetBankingSystem__WebApplication1__HASH24, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."", $tags=""Container Instance"") + Container(InternetBankingSystem__APIApplication1__HASH25, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."", $tags=""Container Instance"") } } - Node(DockerContainerDatabaseServer__2eae566, ""Docker Container - Database Server"", ""Docker"") { - Node(DatabaseServer__24d13de, ""Database Server"", ""Oracle 12c"") { - ContainerDb(InternetBankingSystem__Database1__3296ca6, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") + Node(DockerContainerDatabaseServer__HASH26, ""Docker Container - Database Server"", ""Docker"") { + Node(DatabaseServer__HASH27, ""Database Server"", ""Oracle 12c"") { + ContainerDb(InternetBankingSystem__Database1__HASH28, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."", $tags=""Container Instance"") } } - Node(WebBrowser__3930fd, ""Web Browser"", ""Chrome, Firefox, Safari, or Edge"") { - Container(InternetBankingSystem__SinglePageApplication1__bbe85d, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") + Node(WebBrowser__HASH29, ""Web Browser"", ""Chrome, Firefox, Safari, or Edge"") { + Container(InternetBankingSystem__SinglePageApplication1__HASH30, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."", $tags=""Container Instance"") } } -Rel(InternetBankingSystem__APIApplication1__1f227f4, InternetBankingSystem__Database1__3296ca6, ""Reads from and writes to"", ""JDBC"") -Rel(InternetBankingSystem__APIApplication1__1f227f4, MainframeBankingSystem1__1b2a42c, ""Makes API calls to"", ""XML/HTTPS"") -Rel(InternetBankingSystem__SinglePageApplication1__bbe85d, InternetBankingSystem__APIApplication1__1f227f4, ""Makes API calls to"", ""JSON/HTTPS"") -Rel_Up(InternetBankingSystem__WebApplication1__28f79f6, InternetBankingSystem__SinglePageApplication1__bbe85d, ""Delivers to the customer's web browser"") +Rel(InternetBankingSystem__APIApplication1__HASH25, InternetBankingSystem__Database1__HASH28, ""Reads from and writes to"", ""JDBC"") +Rel(InternetBankingSystem__APIApplication1__HASH25, MainframeBankingSystem1__HASH20, ""Makes API calls to"", ""XML/HTTPS"") +Rel(InternetBankingSystem__SinglePageApplication1__HASH30, InternetBankingSystem__APIApplication1__HASH25, ""Makes API calls to"", ""JSON/HTTPS"") +Rel_Up(InternetBankingSystem__WebApplication1__HASH24, InternetBankingSystem__SinglePageApplication1__HASH30, ""Delivers to the customer's web browser"") SHOW_LEGEND() @enduml @@ -215,46 +257,53 @@ title Internet Banking System - Deployment - Development ' Structurizr.DeploymentView: LiveDeployment title Internet Banking System - Deployment - Live -Node(BigBankplc__3ffe15e, ""Big Bank plc"", ""Big Bank plc data center"") { - Node(bigbankprod001__f5e94d, ""bigbank-prod001"") { - System(MainframeBankingSystem1__3db6dd2, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."") +UpdateElementStyle(system, $bgColor = ""#1168bd"", $fontColor = ""#ffffff"", $borderColor = ""#1168bd"") +UpdateElementStyle(container, $bgColor = ""#438dd5"", $fontColor = ""#ffffff"", $borderColor = ""#438dd5"") +UpdateElementStyle(component, $bgColor = ""#85bbf0"", $fontColor = ""#000000"", $borderColor = ""#85bbf0"") +UpdateElementStyle(person, $bgColor = ""#08427b"", $fontColor = ""#ffffff"", $borderColor = ""#08427b"") +AddElementTag(Existing System, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") +AddElementTag(Bank Staff, $bgColor = ""#999999"", $fontColor = ""#ffffff"", $borderColor = ""#999999"") + +Node(BigBankplc__HASH31, ""Big Bank plc"", ""Big Bank plc data center"") { + Node(bigbankprod001__HASH32, ""bigbank-prod001"") { + System(MainframeBankingSystem1__HASH33, ""Mainframe Banking System"", ""Stores all of the core banking information about customers, accounts, transactions, etc."", $tags=""Software System Instance"") } - Node(bigbankweb***__3f92e18, ""bigbank-web*** (x4)"", ""Ubuntu 16.04 LTS"") { - Node(ApacheTomcat__27b4383, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(InternetBankingSystem__WebApplication1__1720850, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."") + Node(bigbankweb***__HASH34, ""bigbank-web*** (x4)"", ""Ubuntu 16.04 LTS"") { + Node(ApacheTomcat__HASH35, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { + Container(InternetBankingSystem__WebApplication1__HASH36, ""Web Application"", ""Java and Spring MVC"", ""Delivers the static content and the Internet banking single page application."", $tags=""Container Instance"") } } - Node(bigbankapi***__263d9e8, ""bigbank-api*** (x8)"", ""Ubuntu 16.04 LTS"") { - Node(ApacheTomcat__3b84ab, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(InternetBankingSystem__APIApplication1__1408a33, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."") + Node(bigbankapi***__HASH37, ""bigbank-api*** (x8)"", ""Ubuntu 16.04 LTS"") { + Node(ApacheTomcat__HASH38, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { + Container(InternetBankingSystem__APIApplication1__HASH39, ""API Application"", ""Java and Spring MVC"", ""Provides Internet banking functionality via a JSON/HTTPS API."", $tags=""Container Instance"") } } - Node(bigbankdb01__35ec592, ""bigbank-db01"", ""Ubuntu 16.04 LTS"") { - Node(OraclePrimary__19fd8f, ""Oracle - Primary"", ""Oracle 12c"") { - ContainerDb(InternetBankingSystem__Database1__1c974ec, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") + Node(bigbankdb01__HASH40, ""bigbank-db01"", ""Ubuntu 16.04 LTS"") { + Node(OraclePrimary__HASH41, ""Oracle - Primary"", ""Oracle 12c"") { + ContainerDb(InternetBankingSystem__Database1__HASH42, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."", $tags=""Container Instance"") } } - Node(bigbankdb02__1db08a2, ""bigbank-db02"", ""Ubuntu 16.04 LTS"") { - Node(OracleSecondary__1c4ec22, ""Oracle - Secondary"", ""Oracle 12c"") { - ContainerDb(InternetBankingSystem__Database1__d89394, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."") + Node(bigbankdb02__HASH43, ""bigbank-db02"", ""Ubuntu 16.04 LTS"") { + Node(OracleSecondary__HASH44, ""Oracle - Secondary"", ""Oracle 12c"") { + ContainerDb(InternetBankingSystem__Database1__HASH45, ""Database"", ""Relational Database Schema"", ""Stores user registration information, hashed authentication credentials, access logs, etc."", $tags=""Failover+Container Instance"") } } } -Node(Customer'scomputer__2510bf3, ""Customer's computer"", ""Microsoft Windows or Apple macOS"") { - Node(WebBrowser__ba951, ""Web Browser"", ""Chrome, Firefox, Safari, or Edge"") { - Container(InternetBankingSystem__SinglePageApplication1__298b31c, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."") +Node(Customer'scomputer__HASH46, ""Customer's computer"", ""Microsoft Windows or Apple macOS"") { + Node(WebBrowser__HASH47, ""Web Browser"", ""Chrome, Firefox, Safari, or Edge"") { + Container(InternetBankingSystem__SinglePageApplication1__HASH48, ""Single-Page Application"", ""JavaScript and Angular"", ""Provides all of the Internet banking functionality to customers via their web browser."", $tags=""Container Instance"") } } -Node(Customer'smobiledevice__1d6bcb6, ""Customer's mobile device"", ""Apple iOS or Android"") { - Container(InternetBankingSystem__MobileApp1__d004b3, ""Mobile App"", ""Xamarin"", ""Provides a limited subset of the Internet banking functionality to customers via their mobile device."") +Node(Customer'smobiledevice__HASH49, ""Customer's mobile device"", ""Apple iOS or Android"") { + Container(InternetBankingSystem__MobileApp1__HASH50, ""Mobile App"", ""Xamarin"", ""Provides a limited subset of the Internet banking functionality to customers via their mobile device."", $tags=""Container Instance"") } -Rel(InternetBankingSystem__APIApplication1__1408a33, InternetBankingSystem__Database1__1c974ec, ""Reads from and writes to"", ""JDBC"") -Rel(InternetBankingSystem__APIApplication1__1408a33, InternetBankingSystem__Database1__d89394, ""Reads from and writes to"", ""JDBC"") -Rel(InternetBankingSystem__APIApplication1__1408a33, MainframeBankingSystem1__3db6dd2, ""Makes API calls to"", ""XML/HTTPS"") -Rel(InternetBankingSystem__MobileApp1__d004b3, InternetBankingSystem__APIApplication1__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") -Rel_Left(OraclePrimary__19fd8f, OracleSecondary__1c4ec22, ""Replicates data to"") -Rel(InternetBankingSystem__SinglePageApplication1__298b31c, InternetBankingSystem__APIApplication1__1408a33, ""Makes API calls to"", ""JSON/HTTPS"") -Rel_Up(InternetBankingSystem__WebApplication1__1720850, InternetBankingSystem__SinglePageApplication1__298b31c, ""Delivers to the customer's web browser"") +Rel(InternetBankingSystem__APIApplication1__HASH39, InternetBankingSystem__Database1__HASH42, ""Reads from and writes to"", ""JDBC"") +Rel(InternetBankingSystem__APIApplication1__HASH39, InternetBankingSystem__Database1__HASH45, ""Reads from and writes to"", ""JDBC"", $tags=""Failover"") +Rel(InternetBankingSystem__APIApplication1__HASH39, MainframeBankingSystem1__HASH33, ""Makes API calls to"", ""XML/HTTPS"") +Rel(InternetBankingSystem__MobileApp1__HASH50, InternetBankingSystem__APIApplication1__HASH39, ""Makes API calls to"", ""JSON/HTTPS"") +Rel_Left(OraclePrimary__HASH41, OracleSecondary__HASH44, ""Replicates data to"") +Rel(InternetBankingSystem__SinglePageApplication1__HASH48, InternetBankingSystem__APIApplication1__HASH39, ""Makes API calls to"", ""JSON/HTTPS"") +Rel_Up(InternetBankingSystem__WebApplication1__HASH36, InternetBankingSystem__SinglePageApplication1__HASH48, ""Delivers to the customer's web browser"") SHOW_LEGEND() @enduml @@ -390,14 +439,30 @@ private void AddLayoutDetails(Workspace workspace) liveDeploymentView.Relationships .First(r => r.Relationship.SourceId == liveOraclePrimary.Id && r.Relationship.DestinationId == liveOracleSecondary.Id) .SetDirection(DirectionValues.Left); + + // !!! structrizr has another border color calculation (if no value is set) details unclear; set explicit border color too + workspace.Views.Configuration.Styles.Elements + .Where(e=>!string.IsNullOrWhiteSpace(e.Background) && string.IsNullOrWhiteSpace(e.Stroke)).ToList() + .ForEach(e=>e.Stroke = e.Background); + + /* add/update other styles + workspace.Views.Configuration.Styles.Add(new ElementStyle("Container Instance"){ Background = "#E0E0C0", Stroke= "#E0E0C0", Color = "#000000"}); + workspace.Views.Configuration.Styles.Elements.Where(e=>e.Tag=="Failover").ToList() + .ForEach(e=> { e.Background = "#808080"; e.Stroke="#808080"; e.Color = "#FFFFFF"; }); + + workspace.Views.Configuration.Styles.Add(new RelationshipStyle("Relationship"){ Color = "#000000" }); + workspace.Views.Configuration.Styles.Relationships.Where(r=>r.Tag=="Failover").ToList() + .ForEach(r=> { r.Color = "#808080"; r.Dashed = true; }); + */ } [Fact] - public void test_writeWorkspace_WithCustomBaseUrl() + public void test_writeWorkspace_WithNextFeaturesCustomBaseUrl() { PopulateWorkspace(); _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; + _plantUMLWriter.EnableNextFeatures = true; _plantUMLWriter.Write(_workspace, _stringWriter); Assert.Equal( @"@startuml @@ -406,6 +471,9 @@ public void test_writeWorkspace_WithCustomBaseUrl() ' Structurizr.SystemLandscapeView: enterpriseContext title System Landscape for Some Enterprise +SHOW_PERSON_OUTLINE() +AddRelTag(""Back"", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine()) + System_Ext(EmailSystem__1127701, ""E-mail System"") Enterprise_Boundary(SomeEnterprise, ""Some Enterprise"") { Person(User__387cc75, ""User"") @@ -424,6 +492,9 @@ public void test_writeWorkspace_WithCustomBaseUrl() ' Structurizr.SystemContextView: systemContext title Software System - System Context +SHOW_PERSON_OUTLINE() +AddRelTag(""Back"", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine()) + Enterprise_Boundary(SomeEnterprise, ""Some Enterprise"") { System_Ext(EmailSystem__1127701, ""E-mail System"") System(SoftwareSystem__31d545b, ""Software System"") @@ -442,6 +513,9 @@ title Software System - System Context ' Structurizr.ContainerView: containers title Software System - Containers +SHOW_PERSON_OUTLINE() +AddRelTag(""Back"", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine()) + System_Ext(EmailSystem__1127701, ""E-mail System"") Person(User__387cc75, ""User"") System_Boundary(SoftwareSystem__31d545b, ""Software System"") { @@ -462,6 +536,9 @@ title Software System - Containers ' Structurizr.ComponentView: components title Software System - Web Application - Components +SHOW_PERSON_OUTLINE() +AddRelTag(""Back"", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine()) + ContainerDb(SoftwareSystem__Database__39bccb8, ""Database"", ""Relational Database Schema"", ""Stores information"") System_Ext(EmailSystem__1127701, ""E-mail System"") Person(User__387cc75, ""User"") @@ -486,6 +563,9 @@ title Software System - Web Application - Components ' Structurizr.DynamicView: dynamic title Web Application - Dynamic +SHOW_PERSON_OUTLINE() +AddRelTag(""Back"", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine()) + ContainerDb(SoftwareSystem__Database__39bccb8, ""Database"", ""Relational Database Schema"", ""Stores information"") Person(User__387cc75, ""User"") Container_Boundary(SoftwareSystem__WebApplication__d2a342, ""Web Application"") { @@ -505,14 +585,17 @@ title Web Application - Dynamic ' Structurizr.DeploymentView: deployment title Software System - Deployment - Default +SHOW_PERSON_OUTLINE() +AddRelTag(""Back"", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine()) + Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { - ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") + ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"", $tags=""Container Instance"") } } Node(WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { Node(ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"") + Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"", $tags=""Container Instance"") } } Rel(SoftwareSystem__WebApplication1__31f1f25, SoftwareSystem__Database1__bb9c73, ""Reads from and writes to"", ""JDBC"") @@ -529,7 +612,7 @@ public void test_writeEnterpriseContextView() PopulateWorkspace(); SystemLandscapeView systemLandscapeView = _workspace.Views.SystemLandscapeViews.First(); - _plantUMLWriter.Write(systemLandscapeView, _stringWriter); + _plantUMLWriter.Write(systemLandscapeView, _workspace.Views.Configuration, _stringWriter); Assert.Equal( @"@startuml @@ -561,7 +644,7 @@ public void test_writeEnterpriseContextView_WithCustomBaseUrl() SystemLandscapeView systemLandscapeView = _workspace.Views.SystemLandscapeViews.First(); _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; - _plantUMLWriter.Write(systemLandscapeView, _stringWriter); + _plantUMLWriter.Write(systemLandscapeView, _workspace.Views.Configuration, _stringWriter); Assert.Equal( @"@startuml @@ -592,7 +675,7 @@ public void test_writeSystemContextView() PopulateWorkspace(); SystemContextView systemContextView = _workspace.Views.SystemContextViews.First(); - _plantUMLWriter.Write(systemContextView, _stringWriter); + _plantUMLWriter.Write(systemContextView, _workspace.Views.Configuration, _stringWriter); Assert.Equal( @"@startuml @@ -622,7 +705,7 @@ public void test_writeContainerView() PopulateWorkspace(); ContainerView containerView = _workspace.Views.ContainerViews.First(); - _plantUMLWriter.Write(containerView, _stringWriter); + _plantUMLWriter.Write(containerView, _workspace.Views.Configuration, _stringWriter); Assert.Equal( @"@startuml @@ -654,7 +737,7 @@ public void test_writeComponentsView() PopulateWorkspace(); ComponentView componentView = _workspace.Views.ComponentViews.First(); - _plantUMLWriter.Write(componentView, _stringWriter); + _plantUMLWriter.Write(componentView, _workspace.Views.Configuration, _stringWriter); Assert.Equal( @"@startuml @@ -690,7 +773,7 @@ public void test_writeDynamicView() PopulateWorkspace(); DynamicView dynamicView = _workspace.Views.DynamicViews.First(); - _plantUMLWriter.Write(dynamicView, _stringWriter); + _plantUMLWriter.Write(dynamicView, _workspace.Views.Configuration, _stringWriter); // Dynamic diagrams can be drawn with Components Assert.Equal( @@ -722,7 +805,7 @@ public void test_writeDeploymentView() PopulateWorkspace(); DeploymentView deploymentView = _workspace.Views.DeploymentViews.First(); - _plantUMLWriter.Write(deploymentView, _stringWriter); + _plantUMLWriter.Write(deploymentView, _workspace.Views.Configuration, _stringWriter); Assert.Equal( @"@startuml @@ -733,12 +816,12 @@ title Software System - Deployment - Default Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { - ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") + ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"", $tags=""Container Instance"") } } Node(WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { Node(ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"") + Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"", $tags=""Container Instance"") } } Rel(SoftwareSystem__WebApplication1__31f1f25, SoftwareSystem__Database1__bb9c73, ""Reads from and writes to"", ""JDBC"") @@ -749,41 +832,6 @@ title Software System - Deployment - Default ".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); } - [Fact] - public void test_writeDeploymentView_WithCustomBaseUrl() - { - PopulateWorkspace(); - DeploymentView deploymentView = _workspace.Views.DeploymentViews.First(); - - _plantUMLWriter.CustomBaseUrl = @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; - _plantUMLWriter.Write(deploymentView, _stringWriter); - - Assert.Equal( -@"@startuml -!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Deployment.puml - -' Structurizr.DeploymentView: deployment -title Software System - Deployment - Default - -Node(DatabaseServer__1edef6c, ""Database Server"", ""Ubuntu 12.04 LTS"") { - Node(MySQL__1fa4f18, ""MySQL"", ""MySQL 5.5.x"") { - ContainerDb(SoftwareSystem__Database1__bb9c73, ""Database"", ""Relational Database Schema"", ""Stores information"") - } -} -Node(WebServer__1e2ffe, ""Web Server"", ""Ubuntu 12.04 LTS"") { - Node(ApacheTomcat__2b8afb4, ""Apache Tomcat"", ""Apache Tomcat 8.x"") { - Container(SoftwareSystem__WebApplication1__31f1f25, ""Web Application"", ""Java and spring MVC"", ""Delivers content"") - } -} -Rel(SoftwareSystem__WebApplication1__31f1f25, SoftwareSystem__Database1__bb9c73, ""Reads from and writes to"", ""JDBC"") - -SHOW_LEGEND() -@enduml - -".UnifyNewLine().UnifyHashValues(), _stringWriter.ToString().UnifyHashValues()); - } - - private void PopulateWorkspace() { Model model = _workspace.Model; diff --git a/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs index bb5c1ec..51cadb9 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/C4PlantUmlWriter.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using Structurizr.IO.C4PlantUML.ModelExtensions; // Source base version copied from https://gist.github.com/coldacid/465fa8f3a4cd3fdd7b640a65ad5b86f4 (https://github.com/structurizr/dotnet/issues/47) // kirchsth: Extended with dynamic and deployment view // kirchsth: updated to update generated source to new C4PlantUml stdlib v2.2.0 (no additional dynamic and deployment view macros are required anymore, calls updated) -// kirchsth: Add tags/styles support +// kirchsth: Support ViewConfiguration, tags and styles // kirchsth: next planed C4PlantUml stdlib v2.3.0 features can be used with CustomBaseUrl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ namespace Structurizr.IO.C4PlantUML { @@ -31,14 +32,17 @@ public enum LayoutDirection /// Only next stdlib features (like Person shapes) has to be defined via CustomBaseUrl=https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/ /// (if the value is empty/null then PlantUML-stdlib with added definitions is used) /// - public string CustomBaseUrl { get; set; } = ""; // @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; - public bool EnableNextFeatures { get; set; } = false; + public string CustomBaseUrl { get; set; } =""; // @"https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; + public bool EnableNextFeatures { get; set; } = false; // true; - protected override void Write(SystemLandscapeView view, TextWriter writer) + public string AdditionalProlog { get; set; } + public string AdditionalEpilog { get; set; } + + protected override void Write(SystemLandscapeView view, ViewConfiguration viewConfiguration, TextWriter writer) { var showBoundary = view.EnterpriseBoundaryVisible ?? true; - WriteProlog(view, writer); + WriteProlog(view, viewConfiguration, writer); view.Elements .Select(ev => ev.Element) @@ -75,14 +79,14 @@ protected override void Write(SystemLandscapeView view, TextWriter writer) Write(view.Relationships, writer); - WriteEpilog(view, writer); + WriteEpilog(view, viewConfiguration, writer); } - protected override void Write(SystemContextView view, TextWriter writer) + protected override void Write(SystemContextView view, ViewConfiguration viewConfiguration, TextWriter writer) { var showBoundary = view.EnterpriseBoundaryVisible ?? true; - WriteProlog(view, writer); + WriteProlog(view, viewConfiguration, writer); if (showBoundary) { @@ -99,17 +103,17 @@ protected override void Write(SystemContextView view, TextWriter writer) if (showBoundary) writer.WriteLine("}"); - WriteEpilog(view, writer); + WriteEpilog(view, viewConfiguration, writer); } - protected override void Write(ContainerView view, TextWriter writer) + protected override void Write(ContainerView view, ViewConfiguration viewConfiguration, TextWriter writer) { var externals = view.Elements .Select(ev => ev.Element) .Where(e => !(e is Container)); var showBoundary = externals.Any(); - WriteProlog(view, writer); + WriteProlog(view, viewConfiguration, writer); externals .OrderBy(e => e.Name).ToList() @@ -129,10 +133,10 @@ protected override void Write(ContainerView view, TextWriter writer) Write(view.Relationships, writer); - WriteEpilog(view, writer); + WriteEpilog(view, viewConfiguration, writer); } - protected override void Write(ComponentView view, TextWriter writer) + protected override void Write(ComponentView view, ViewConfiguration viewConfiguration, TextWriter writer) { var nonComponents = view.Elements .Select(ev => ev.Element) @@ -144,7 +148,7 @@ from ev in view.Elements group e by e.Parent; var showBoundary = nonComponents.Any() || nonContainedComponents.Any(); - WriteProlog(view, writer); + WriteProlog(view, viewConfiguration, writer); nonComponents .OrderBy(e => e.Name).ToList() @@ -175,12 +179,12 @@ from ev in view.Elements Write(view.Relationships, writer); - WriteEpilog(view, writer); + WriteEpilog(view, viewConfiguration, writer); } - protected override void Write(DynamicView view, TextWriter writer) + protected override void Write(DynamicView view, ViewConfiguration viewConfiguration, TextWriter writer) { - WriteProlog(view, writer); + WriteProlog(view, viewConfiguration, writer); IList innerElements = new List(); IList outerElements = new List(); @@ -228,12 +232,12 @@ protected override void Write(DynamicView view, TextWriter writer) WriteDynamicInteractions(view.Relationships, writer); - WriteEpilog(view, writer); + WriteEpilog(view, viewConfiguration, writer); } - protected override void Write(DeploymentView view, TextWriter writer) + protected override void Write(DeploymentView view, ViewConfiguration viewConfiguration, TextWriter writer) { - WriteProlog(view, writer); + WriteProlog(view, viewConfiguration, writer); view.Elements .Where(ev => ev.Element is DeploymentNode && ev.Element.Parent == null) @@ -243,7 +247,7 @@ protected override void Write(DeploymentView view, TextWriter writer) Write(view.Relationships, writer); - WriteEpilog(view, writer); + WriteEpilog(view, viewConfiguration, writer); } private void Write(DeploymentNode deploymentNode, TextWriter writer, int indentLevel) @@ -319,54 +323,60 @@ private bool HasValue(string s) return s != null && s.Trim().Length > 0; } - protected override void WriteProlog(View view, TextWriter writer) + protected override void WriteProlog(View view, ViewConfiguration viewConfiguration, TextWriter writer) { if (view == null) throw new ArgumentNullException(nameof(view)); if (writer == null) throw new ArgumentNullException(nameof(writer)); writer.WriteLine("@startuml"); + string diagramType; + HashSet existingLegendTags; // (already mapped) tags (styles) which have to be overwritten not added + switch (view) { case SystemLandscapeView _: case SystemContextView _: - writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) - ? $"!includeurl {CustomBaseUrl}C4_Context.puml" - : $"!include "); + diagramType = "Context"; + existingLegendTags = new HashSet { "person", "system", "external_person", "external_system" }; break; - case ComponentView _: - writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) - ? $"!includeurl {CustomBaseUrl}C4_Component.puml" - : $"!include "); + case ContainerView _: + diagramType = "Container"; + existingLegendTags = new HashSet { "person", "system", "container", "external_person", "external_system", "external_container" }; break; case DynamicView _: - writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) - ? $"!includeurl {CustomBaseUrl}C4_Dynamic.puml" - : $"!include "); + diagramType = "Dynamic"; + existingLegendTags = new HashSet { "person", "system", "container", "component", "external_person", "external_system", "external_container", "external_component" }; break; case DeploymentView _: - writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) - ? $"!includeurl {CustomBaseUrl}C4_Deployment.puml" - : $"!include "); + diagramType = "Deployment"; + existingLegendTags = new HashSet { "person", "system", "container", "external_person", "external_system", "external_container", "node" }; break; default: - writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) - ? $"!includeurl {CustomBaseUrl}C4_Container.puml" - : $"!include "); // as long no stdlib is used the Component diagram definition can be reused + diagramType = "Component"; + existingLegendTags = new HashSet { "person", "system", "container", "component", "external_person", "external_system", "external_container", "external_component" }; break; } + writer.WriteLine(!string.IsNullOrWhiteSpace(CustomBaseUrl) + ? $"!includeurl {CustomBaseUrl}C4_{diagramType}.puml" + : $"!include "); + writer.WriteLine(); writer.WriteLine($"' {view.GetType()}: {view.Key}"); writer.WriteLine("title " + GetTitle(view)); writer.WriteLine(); if (LayoutAsSketch) - writer.WriteLine("LAYOUT_AS_SKETCH()"); // C4 PlantUML workaround add () + writer.WriteLine("LAYOUT_AS_SKETCH()"); + + if (EnableNextFeatures) + writer.WriteLine("SHOW_PERSON_OUTLINE()"); + if (Layout.HasValue) { switch (Layout) @@ -383,13 +393,21 @@ protected override void WriteProlog(View view, TextWriter writer) } if (LayoutAsSketch || Layout.HasValue) writer.WriteLine(); + + WriteExistingStyles(view, existingLegendTags, viewConfiguration, writer); + + if (!string.IsNullOrWhiteSpace(AdditionalProlog)) + writer.WriteLine(AdditionalProlog); } - protected virtual void WriteEpilog(View view, TextWriter writer) + protected override void WriteEpilog(View view, ViewConfiguration viewConfiguration, TextWriter writer) { if (view == null) throw new ArgumentNullException(nameof(view)); if (writer == null) throw new ArgumentNullException(nameof(writer)); + if (!string.IsNullOrWhiteSpace(AdditionalEpilog)) + writer.WriteLine(AdditionalEpilog); + if (LayoutWithLegend) { writer.WriteLine(); @@ -397,9 +415,130 @@ protected virtual void WriteEpilog(View view, TextWriter writer) } writer.WriteLine("@enduml"); - writer.WriteLine(""); + writer.WriteLine(); + } + + protected virtual void WriteExistingStyles(View view, HashSet existingLegendTags, ViewConfiguration viewConfiguration, TextWriter writer) + { + if (view == null) throw new ArgumentNullException(nameof(view)); + if (writer == null) throw new ArgumentNullException(nameof(writer)); + + ElementStyle baseES = viewConfiguration.Styles.Elements.FirstOrDefault(es => es.Tag == "Element"); + RelationshipStyle definedRS = viewConfiguration.Styles.Relationships.FirstOrDefault(rs => rs.Tag == "Relationship"); + + if (EnableNextFeatures) // linestyle + { + // add Back related style (which is typically dotted in Structurizr) (and if defined then it will be overwritten with viewConfiguration.Styles.Relationships) + writer.WriteLine("AddRelTag(\"Back\", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine())"); + writer.WriteLine(); + } + + foreach (var es in viewConfiguration.Styles.Elements) + if (es != baseES) // skip Element + Write(es, baseES, existingLegendTags, writer); + foreach (var rs in viewConfiguration.Styles.Relationships) + Write(rs, definedRS, writer); + + if (viewConfiguration.Styles.Elements.Count > 0 || viewConfiguration.Styles.Relationships.Count > 0) + writer.WriteLine(); + } + + protected virtual void Write(ElementStyle es, ElementStyle baseElementStyle, HashSet existingLegendTags, TextWriter writer) + { + var defined = StructurizrTags2DiagramTags.TryGetValue(es.Tag, out var diagramTag); + if (!defined) + diagramTag = es.Tag; + + // UpdateElementStyle or AddElementTag(elementName, ?bgColor, ?fontColor, ?borderColor, ?shadowing, ?shape) // ?shadowing not used; ?shape only rounded or eight-sided + var allArgs = new StringBuilder(); + WriteColor("$bgColor", es.Background, baseElementStyle?.Background, false, allArgs); + WriteColor("$fontColor", es.Color, baseElementStyle?.Color, false, allArgs); + WriteColor("$borderColor", es.Stroke, baseElementStyle?.Stroke, false, allArgs); + if (EnableNextFeatures) + { + // default shape of element is ignored + WriteShape(es.Shape, allArgs); + } + + if (allArgs.Length > 0) + { + writer.Write(defined ? "UpdateElementStyle" : "AddElementTag"); + writer.WriteLine($"({diagramTag}{allArgs})"); + } + } + + protected virtual void Write(RelationshipStyle rs, RelationshipStyle definedRelationshipStyle, TextWriter writer) + { + // only "Relationship" is predefined (which is defaultRelationshipStyle) + var diagramTag = rs.Tag; + var defined = (rs == definedRelationshipStyle); + + // UpdateRelStyle or AddRelTag(tagStereo, ?textColor, ?lineColor, ?lineStyle) + var allArgs = new StringBuilder(); + WriteColor("$textColor", rs.Color, definedRelationshipStyle?.Color, defined, allArgs); + WriteColor("$lineColor", rs.Color, definedRelationshipStyle?.Color, defined, allArgs); + if (!defined) + { + if (EnableNextFeatures) + { + if (rs.Dashed == true) // C# Structurize does not support all styles + allArgs.Append($", $lineStyle = DashedLine()"); + } + } + + if (allArgs.Length > 0) + { + writer.Write(defined ? "UpdateRelStyle(" : $"AddRelTag({diagramTag}, "); + writer.WriteLine($"{allArgs.Remove(0, 2)})"); // remove first ", " + } + } + + protected void WriteColor(string argName, string elementColor, string defaultColor, bool lineColorsRequired, StringBuilder allArgs) + { + var color = elementColor; + if (string.IsNullOrWhiteSpace(color)) + color = defaultColor; + + if (!string.IsNullOrWhiteSpace(color)) + allArgs.Append($", {argName} = \"{color}\""); + else if (lineColorsRequired) + allArgs.Append($", {argName} = $ARROW_COLOR"); + } + + protected void WriteShape(Shape elementShape, StringBuilder allArgs) + { + if (EnableNextFeatures) + { + switch (elementShape) + { + case Shape.RoundedBox: + allArgs.Append($", $shape = RoundedBoxShape()"); + break; + case Shape.Hexagon: + allArgs.Append($", $shape = EightSidedShape()"); + break; + default: + // all other ignored atm (Database handled via ..Db() extension) + break; + } + } } + protected static Dictionary StructurizrTags2DiagramTags = new Dictionary + { + // Element is handled via defaultElementStyle and is not added as tag + ["Element"] = "", + ["Person"] = "person", + ["Software System"] = "system", + ["Container"] = "container", + ["Component"] = "component", + ["Deployment Node"] = "node" + // ?? how should this tags be mapped -> reused without special mapping atm + // ["Infrastructure Node"] = "", + // ["Software System Instance"] = "", + // ["Container Instance"] = "", + }; + protected virtual void Write(Element element, TextWriter writer, int indentLevel = 0, bool asBoundary = false) { var indent = indentLevel == 0 ? "" : new string(' ', indentLevel * 2); @@ -497,9 +636,20 @@ protected virtual void Write(Element element, TextWriter writer, int indentLevel { writer.Write($", \"{EscapeText(description)}\""); } + WriteTags(element, writer); writer.WriteLine(")"); } + private void WriteTags(Element element, TextWriter writer) + { + var tags = element.GetAllTags().Where(t => !StructurizrTags2DiagramTags.ContainsKey(t)).Reverse().ToList(); + if (tags.Count > 0) + { + var combinedTags = string.Join("+", tags); + writer.Write($", $tags=\"{EscapeText(combinedTags)}\""); + } + } + protected virtual void Write(ISet relationships, TextWriter writer) { relationships @@ -516,7 +666,7 @@ protected virtual void Write(RelationshipView relationshipView, TextWriter write label = advancedDescription ?? relationship.Description ?? "", tech = !string.IsNullOrWhiteSpace(relationship.Technology) ? relationship.Technology : null; - if (relationshipView.Response ?? false) + if (relationshipView.Response == true) { var swap = source; source = dest; @@ -528,9 +678,25 @@ protected virtual void Write(RelationshipView relationshipView, TextWriter write writer.Write($"{macro}({source}, {dest}, \"{EscapeText(label)}\""); if (tech != null) writer.Write($", \"{EscapeText(tech)}\""); + WriteTags(relationshipView, writer); writer.WriteLine(")"); } + private void WriteTags(RelationshipView relationshipView, TextWriter writer) + { + var relationship = relationshipView.Relationship; + var tags = new List(); + if (relationshipView.Response == true) + tags.Add("Back"); + + tags.AddRange(relationship.GetAllTags().Where(t => t != "Relationship").Reverse()); + if (tags.Count > 0) + { + var combinedTags = string.Join("+", tags); + writer.Write($", $tags=\"{EscapeText(combinedTags)}\""); + } + } + protected virtual void WriteDynamicInteractions(ISet relationships, TextWriter writer) { relationships @@ -546,7 +712,7 @@ protected virtual void WriteDynamicInteraction(RelationshipView relationshipView dest = TokenizeName(relationship.Destination), tech = !string.IsNullOrWhiteSpace(relationship.Technology) ? relationship.Technology : null; - if (relationshipView.Response ?? false) + if (relationshipView.Response == true) { var swap = source; source = dest; @@ -559,6 +725,7 @@ protected virtual void WriteDynamicInteraction(RelationshipView relationshipView writer.Write($"{macro}(\"{order}\", {source}, {dest}, \"{EscapeText(label)}\""); if (tech != null) writer.Write($", \"{EscapeText(tech)}\""); + WriteTags(relationshipView, writer); writer.WriteLine(")"); } diff --git a/Structurizr.PlantUML/IO/C4PlantUML/IPlantUMLWriter.cs b/Structurizr.PlantUML/IO/C4PlantUML/IPlantUMLWriter.cs index 525e008..5b47e65 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/IPlantUMLWriter.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/IPlantUMLWriter.cs @@ -2,11 +2,12 @@ // Source base version copied from https://gist.github.com/coldacid/465fa8f3a4cd3fdd7b640a65ad5b86f4 (https://github.com/structurizr/dotnet/issues/47) // kirchsth: Extended with dynamic and deployment view +// kirchsth: Support ViewConfiguration, tags and styles namespace Structurizr.IO.C4PlantUML { public interface IPlantUMLWriter { void Write(Workspace workspace, TextWriter writer); - void Write(View view, TextWriter writer); + void Write(View view, ViewConfiguration viewConfiguration, TextWriter writer); } } \ No newline at end of file diff --git a/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs b/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs index 262911b..d8d2a96 100644 --- a/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs +++ b/Structurizr.PlantUML/IO/C4PlantUML/PlantUMLWriterBase.cs @@ -5,6 +5,8 @@ // Source base version copied from https://gist.github.com/coldacid/465fa8f3a4cd3fdd7b640a65ad5b86f4 (https://github.com/structurizr/dotnet/issues/47) // kirchsth: Extended with dynamic and deployment view +// kirchsth: Support ViewConfiguration, tags and styles + namespace Structurizr.IO.C4PlantUML { /// @@ -25,16 +27,17 @@ public void Write(Workspace workspace, TextWriter writer) CurrentViewModel = workspace.Model; - workspace.Views.SystemLandscapeViews.ToList().ForEach(v => Write(v, writer)); - workspace.Views.SystemContextViews.ToList().ForEach(v => Write(v, writer)); - workspace.Views.ContainerViews.ToList().ForEach(v => Write(v, writer)); - workspace.Views.ComponentViews.ToList().ForEach(v => Write(v, writer)); - workspace.Views.DynamicViews.ToList().ForEach(v => Write(v, writer)); - workspace.Views.DeploymentViews.ToList().ForEach(v => Write(v, writer)); + var viewConfiguration = workspace.Views.Configuration; + workspace.Views.SystemLandscapeViews.ToList().ForEach(v => Write(v, viewConfiguration, writer)); + workspace.Views.SystemContextViews.ToList().ForEach(v => Write(v, viewConfiguration, writer)); + workspace.Views.ContainerViews.ToList().ForEach(v => Write(v, viewConfiguration, writer)); + workspace.Views.ComponentViews.ToList().ForEach(v => Write(v, viewConfiguration, writer)); + workspace.Views.DynamicViews.ToList().ForEach(v => Write(v, viewConfiguration, writer)); + workspace.Views.DeploymentViews.ToList().ForEach(v => Write(v, viewConfiguration, writer)); } /// - public void Write(View view, TextWriter writer) + public void Write(View view, ViewConfiguration viewConfiguration, TextWriter writer) { if (view == null) throw new ArgumentNullException(nameof(view)); if (writer == null) throw new ArgumentNullException(nameof(writer)); @@ -44,22 +47,22 @@ public void Write(View view, TextWriter writer) switch (view) { case SystemLandscapeView sl: - Write(sl, writer); + Write(sl, viewConfiguration, writer); break; case SystemContextView sc: - Write(sc, writer); + Write(sc, viewConfiguration, writer); break; case ContainerView ct: - Write(ct, writer); + Write(ct, viewConfiguration, writer); break; case ComponentView cp: - Write(cp, writer); + Write(cp, viewConfiguration, writer); break; case DynamicView dy: - Write(dy, writer); + Write(dy, viewConfiguration, writer); break; case DeploymentView de: - Write(de, writer); + Write(de, viewConfiguration, writer); break; default: throw new NotSupportedException($"{view.GetType()} not supported for export"); @@ -71,42 +74,42 @@ public void Write(View view, TextWriter writer) /// /// /// - protected abstract void Write(SystemLandscapeView view, TextWriter writer); + protected abstract void Write(SystemLandscapeView view, ViewConfiguration viewConfiguration, TextWriter writer); /// /// Writes a system context view in PlantUML format to the provided writer. /// /// /// - protected abstract void Write(SystemContextView view, TextWriter writer); + protected abstract void Write(SystemContextView view, ViewConfiguration viewConfiguration, TextWriter writer); /// /// Writes a container view in PlantUML format to the provided writer. /// /// /// - protected abstract void Write(ContainerView view, TextWriter writer); + protected abstract void Write(ContainerView view, ViewConfiguration viewConfiguration, TextWriter writer); /// /// Writes a component view in PlantUML format to the provided writer. /// /// /// - protected abstract void Write(ComponentView view, TextWriter writer); + protected abstract void Write(ComponentView view, ViewConfiguration viewConfiguration, TextWriter writer); /// /// Writes a dynamic view in PlantUML format to the provided writer. /// /// /// - protected abstract void Write(DynamicView view, TextWriter writer); + protected abstract void Write(DynamicView view, ViewConfiguration viewConfiguration, TextWriter writer); /// /// Writes a deployment view in PlantUML format to the provided writer. /// /// /// - protected abstract void Write(DeploymentView view, TextWriter writer); + protected abstract void Write(DeploymentView view, ViewConfiguration viewConfiguration, TextWriter writer); /// /// Produces a standard PlantUML diagram prolog for the provided view. @@ -115,7 +118,7 @@ public void Write(View view, TextWriter writer) /// /// is . /// is . - protected virtual void WriteProlog(View view, TextWriter writer) + protected virtual void WriteProlog(View view, ViewConfiguration viewConfiguration, TextWriter writer) { if (view == null) throw new ArgumentNullException(nameof(view)); if (writer == null) throw new ArgumentNullException(nameof(writer)); @@ -132,7 +135,7 @@ protected virtual void WriteProlog(View view, TextWriter writer) /// /// is . /// is . - protected virtual void WriteEpilog(View view, TextWriter writer) + protected virtual void WriteEpilog(View view, ViewConfiguration viewConfiguration, TextWriter writer) { if (view == null) throw new ArgumentNullException(nameof(view)); if (writer == null) throw new ArgumentNullException(nameof(writer)); @@ -177,6 +180,8 @@ protected string TokenizeName(string s, int? hash = null) .Replace("-", "") .Replace("[", "") .Replace("]", "") + .Replace("(", "") + .Replace(")", "") .Replace(".", "__"); if (hash.HasValue) diff --git a/Structurizr.sln b/Structurizr.sln index e62dc9f..ea60d98 100644 --- a/Structurizr.sln +++ b/Structurizr.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.15 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31410.357 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{4ED0E8DB-8A5B-4DF0-9CB1-995D97A04B88}" ProjectSection(SolutionItems) = preProject @@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{4FBA61 ProjectSection(SolutionItems) = preProject docs\images\c4-plantuml-getting-started.png = docs\images\c4-plantuml-getting-started.png docs\images\c4-plantuml-getting-started2.png = docs\images\c4-plantuml-getting-started2.png + docs\images\c4-plantuml-getting-started3.png = docs\images\c4-plantuml-getting-started3.png docs\images\plantuml-getting-started.png = docs\images\plantuml-getting-started.png docs\images\structurizr-annotations-1.png = docs\images\structurizr-annotations-1.png docs\images\structurizr-banner.png = docs\images\structurizr-banner.png diff --git a/docs/c4-plantuml.md b/docs/c4-plantuml.md index ddf30ba..fa51905 100644 --- a/docs/c4-plantuml.md +++ b/docs/c4-plantuml.md @@ -17,7 +17,7 @@ Workspace workspace = new Workspace("Getting Started", "This is a model of my so Model model = workspace.Model; model.Enterprise = new Enterprise("Some Enterprise"); - + Person user = model.AddPerson("User", "A user of my software system."); SoftwareSystem softwareSystem = model.AddSoftwareSystem("Software System", "My software system."); var userUsesSystemRelation = user.Uses(softwareSystem, "Uses"); @@ -65,26 +65,39 @@ If you copy/paste this into [PlantUML online](http://www.plantuml.com/plantuml/) ![A simple C4-PlantUML diagram](images/c4-plantuml-getting-started.png) -__Mark containers or components as Database__ +__Mark containers or components as database or via tags__ Additional to the relation directions (via .SetDirection(), see above) is it possible to activate the database symbols in the diagrams via component and container specific *.IsDatabase(true) calls. +[C4-PlantUML v2.2.0](https://github.com/plantuml-stdlib/C4-PlantUML) supports tags (via `.Tags =`) an styles (`.Styles.Add()`) too. ```c# Container webApplication = softwareSystem.AddContainer("Web Application", "Delivers content", "Java and spring MVC"); +// Additional tag element +webApplication.Tags = "Single Page App"; + Container database = softwareSystem.AddContainer("Database", "Stores information", "Relational Database Schema"); // Additional mark it as database database.SetIsDatabase(true); -user.Uses(webApplication, "uses", "HTTP"); + +var httpCall = user.Uses(webApplication, "uses", "HTTP"); +// Additional tag relationship +httpCall.Tags = "via firewall"; + webApplication.Uses(database, "Reads from and writes to", "JDBC").SetDirection(DirectionValues.Right); +// add corresponding styles +var styles = views.Configuration.Styles; +styles.Add(new ElementStyle("Single Page App") {Background = "#5F9061", Stroke = "#2E4F2E", Color = "#FFFFFF" }); +styles.Add(new RelationshipStyle("via firewall") {Color = "#B40404", Dashed = true }); // dashed is supported with next version see below + var containerView = views.CreateContainerView(softwareSystem, "containers", ""); containerView.AddAllElements(); using (var stringWriter = new StringWriter()) { var plantUmlWriter = new C4PlantUmlWriter(); - plantUmlWriter.Write(containerView, stringWriter); + plantUmlWriter.Write(containerView, workspace.Views.Configuration, stringWriter); Console.WriteLine(stringWriter.ToString()); } ``` @@ -98,12 +111,15 @@ This code will generate and output a PlantUML diagram definition that looks like ' Structurizr.ContainerView: containers title Software System - Containers +AddElementTag(Single Page App, $bgColor = "#5f9061", $fontColor = "#ffffff", $borderColor = "#2e4f2e") +AddRelTag(via firewall, $textColor = "#b40404", $lineColor = "#b40404") + Person(User__378734a, "User", "A user of my software system.") System_Boundary(SoftwareSystem__33c0d9d, "Software System") { ContainerDb(SoftwareSystem__Database__202c666, "Database", "Relational Database Schema", "Stores information") - Container(SoftwareSystem__WebApplication__2004eee, "Web Application", "Java and spring MVC", "Delivers content") + Container(SoftwareSystem__WebApplication__2004eee, "Web Application", "Java and spring MVC", "Delivers content", $tags="Single Page App") } -Rel(User__378734a, SoftwareSystem__WebApplication__2004eee, "uses", "HTTP") +Rel(User__378734a, SoftwareSystem__WebApplication__2004eee, "uses", "HTTP", $tags="via firewall") Rel_Right(SoftwareSystem__WebApplication__2004eee, SoftwareSystem__Database__202c666, "Reads from and writes to", "JDBC") SHOW_LEGEND() @@ -114,6 +130,55 @@ You will get something like this: ![A simple C4-PlantUML diagram](images/c4-plantuml-getting-started2.png) +__Use features of the next planned C4-PlantUML version (v2.3.0 ?)__ + +The next version will support the correct Person shape and e.g. dotted lines. These features can be activated via `.EnableNextFeatures=true` +(until the next version is released please use `.CustomBaseUrl="https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"`) + +```c# +using (var stringWriter = new StringWriter()) +{ + var plantUmlWriter = new C4PlantUmlWriter(); + plantUmlWriter.EnableNextFeatures = true; + plantUmlWriter.CustomBaseUrl = "https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/"; + + plantUmlWriter.Write(containerView, workspace.Views.Configuration, stringWriter); + Console.WriteLine(stringWriter.ToString()); +} +``` + +This code will generate and output a PlantUML diagram definition that looks like this: + +``` +@startuml +!includeurl https://raw.githubusercontent.com/kirchsth/C4-PlantUML/extended/C4_Container.puml + +' Structurizr.ContainerView: containers +title Software System - Containers + +SHOW_PERSON_OUTLINE() +AddRelTag("Back", $textColor=$ARROW_COLOR, $lineColor=$ARROW_COLOR, $lineStyle = DottedLine()) + +AddElementTag(Single Page App, $bgColor = "#5f9061", $fontColor = "#ffffff", $borderColor = "#2e4f2e", $shape = RoundedBoxShape()) +AddRelTag(via firewall, $textColor = "#b40404", $lineColor = "#b40404", $lineStyle = DashedLine()) + +Person(User__378734a, "User", "A user of my software system.") +System_Boundary(SoftwareSystem__33c0d9d, "Software System") { + ContainerDb(SoftwareSystem__Database__202c666, "Database", "Relational Database Schema", "Stores information") + Container(SoftwareSystem__WebApplication__2004eee, "Web Application", "Java and spring MVC", "Delivers content", $tags="Single Page App") +} +Rel(User__378734a, SoftwareSystem__WebApplication__2004eee, "uses", "HTTP", $tags="via firewall") +Rel_Right(SoftwareSystem__WebApplication__2004eee, SoftwareSystem__Database__202c666, "Reads from and writes to", "JDBC") + +SHOW_LEGEND() +@enduml +``` + +You will get something like this: + +![A simple C4-PlantUML diagram](images/c4-plantuml-getting-started3.png) + + ## Benefits of using C4-PlantUML with Structurizr The key benefit of using PlantUML in conjunction with the Structurizr client library is that you can create diagrams from a __model__ of your software system. The model provides a set of rules that must be followed; related to elements, relationships, and how they are exposed using diagrams. This means: diff --git a/docs/images/c4-plantuml-getting-started2.png b/docs/images/c4-plantuml-getting-started2.png index dbf8db0a7e6ae4994fe96842da38bb886bdfec42..a931c99eae95e760133d350ed21b7b84546a2eb5 100644 GIT binary patch literal 33142 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 literal 28989 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 $OmWXYuY}d2=w`xn znI7rn~_<~*+~ zN)~?QJa`l(T1Tr=VH!=BEtE9&*&O|0i#XS~Wq%?U^UXohHVq4>8G4l>PF|tKhXes= zZ0bJ|6Zm^Pz{X9UH_Mdb2xef=D6^L`IpY#cDa}<{OoM1?l9Ex%$pHhCCICiJN@A!- zf>9G>4FSpxK*smB z``njv#PxcZ>=D=H0mv=PHXwBXEcp5$+rB_BfYSo>1j5iu!TXxYE)5cg9NLX0CMDIzF9Ku`M#PmqcdDQYhC!dB z=ghr>Y71sC;}b-aKFE-ZdoLi+0^spS%p`7y7SVk7!EV4L5{3o}W89Oyk^l|C=k%t2 z|Nb4!pSYcFHw$QL&Vu=uLhYu)&Rr6$*|yengaaI86dCIFG|0t6Iu?LB9x|>gutfNP z$Kakm?HYqZ5#F2)&o#KYoorIgG19cJ2I5IM6h}lwDL^1^07L_dsXzT?mX?;b6#8Q+ z_3CuThxT-LmWPqi62w`RT@V~%Rv@4mSF6?kJxy60%T8(`d}dxB(jWu-Z^)wiai zqQa{FKtxxUg1H=U<+U}Ll((9Vk2YHavF_7Q3qXHhCg}wxB_$;QxPsM4?9BnM!trS4 z6o#iBmIhXF(;j_PZTuxzliw{OcI5karwaTRR8(E)*`=kJ?2(CyGay!gOhH#RHacoP zTdOD{QTnd+Ljf_%A|0r?bRrnA)4qQF3T7}vNchw&QIL?_?rzQu z#HFRB6%-Ue$nx;G0ot2)KavXG@t|VB*@LCL(Stk1bh&GQzjvaZu`@S7K&*89WDE=iAjH0o2Bk}GmB}ZQ=E(A@ z!}{jUvAni~l2Jkc{*n3idlolwmZ46~@D?j@ zUYj1@XIN2;IJmf#ploq?35rmZ01*RJki2CH6lG<|U+%*y0Fu1OfkPD4ueI!AR~4xF}34@cF&@X@O4Neup#JnN@6z&qhN?x*ZCAUL}! z{JzLf)4R4ebQ1?-#KZI0%&zF2W=qjAv>MIFl>=%VdmgeiE@Nr4<|^5W&omaIpQ zAK%xY@Mt7C99Y8|?;k`(soIhcnF_3vt`Uh{w;J*{;0ZO5eBa|^f=dz!pK%=b<5<;l z)w5JkcJA{2HnAq0!Li3l{r&e$UeaotnuXrDVjZb(E1a)JfY!4b40Ojb{4U4wajX>L zt>X4|X?7+d!L*v%zNVn>TR0e@hQ5mqh&51u3$~m5%DyyZqhy~v6g3xz-gZKkQ+M*{$ltJ;v(hldJr)g093e8>T&*Sl4LPf zEBSE!@UE>(>FV>DJzA4F3IVQ~n_Md+@miZ2gVDI6Spea$-o#Z}I@bPGYb7_Is)o1x zxTK;fZ4>)L$3fM@dY;&C^J7K^Xp$r+C!><8Vn}7?@RD2@vR)Tuir?_$#9v19eA*JHjP^VbZhNQe&87^5z!4o}Zrs^o$IqdU*`>^>a8l zJ`gy45B*+VikosC)_FX56Zkl$qddhy>)61;XTS+9+|>@!s~%i*s`ea-)D+)!2S?*1 zLw)o#T45x%Z$QmYEh&FsY0D$F^;DB+#H@J*wTuZWQ{NX-w#OKn+<0|5j${)1_0YN@ z{kHI1Xd{Ge{j;{alj5TO5hOS;U=E4saF|;3{-A{~@p78b;m~L4KfNKN15K~Sz%yyW z36F46ev&#chz7=VLGMGXq2|MIHH!s0{skD>A(fnU*y@AkG)=kE*sJWl3f-yOz9hup z&?;#cjJFIsG+PwhtKAoAGT@lg#Jw6%rw2D$4E`+UI((W?m%+Td=DeaRHxdGC>!qiN zv9jDe?~=CAwUZ5wi{)d5;m|7q*lh!93YBDT2N<`!o{7d8gKqBC*_?f8MT)36=G9#?=D&VZV(D)n zakSa}oY{VY-2b{SXVA_>=F_2G1MgLQfQX+bZV!NyDkQANUuIk{k2gLH@K-N%%cAraZt*ARi44 zLKX@>^OHJJQPE&~DvBndIOpX~jHqXXSb?C0CK^s6eD3$?J|+5UbCxsT0WivcxlTEH z4KM(}5bycgN;@-J+cu!-))f0z<;bO3p!Sqv>>st;+}yJtuewb{7u3awz?AdmYs(qVS76#sF7EDkIKNi$$M;?dClo3nQ0Ncl z8L`W_KUZ1E2g%TnT*n1KNZrU_mXj~=7dU$ip}Wk~r4vh>msl&g0#>J5m=g}WSxzw; z9|p>_&DaSDaK^-3EEO^=%4oUIWC$4-q{SmI^I;#fdR7 zv z^r*m~Eh5Kf+667lCw}o}05xVqAeJjN5OQ}_(C_q8*Y>(NG${GNGtC5ABdsr6(Kk`T z-l}a!CnkohfIa{%2XHnm5fZ5HpORM*ima9)U=pMePK>aa+DR!X_9{CwbDfc10=Jrz zxa3t>`XzaHTbyglZV&#rcD#|su;(E5O5lE0i}KdO*sBw#eky;GbBn=!djiFys5KF) zOG^y)W{K9mHyEWfUZY)oeK<(qV}SOToG9t(;QI%fD`Pwxxa}uiNT5radaNwOK-bEs zmLZ29QtDcZ5#70Gt{NCT0@+Pxro%JZe5Sm+@_WQxr<+SeAu2b>%_hh}g{<-8=3b%% zGQJ|1RAG~eGf-%2YZDAWZQBI+wAd9m@NH#^ky4>Xjm}ShfLa0Kgf_HlHsiAxE(mP? zMB`UD9Rgn_Z0((|z31M~MWx}7Ab5)mZU2G-9dbGZ8WyjSms4Kr zct77G<6zLLf4+ME{YybHA2!9XVh$`bLAT_l_(_p~B7`jc-8m_0LErx(tFXHzGbVWL z_iKk$sVq!=)yLi2X7;aOI*6e^tJbsdA^X{Q)F~EkSk&yXS?M-6IZ=;tHd#CgJdNqp^NS4b(YT;x0RW8$&)SG;VkR;sWQI-^Uf4FvFGlOT~5 znVb;u?qBfa?eVy@mJ9Xh_LqLNYeOiir2A-%-Ep+>Y5Qr@$!AU8Ma{wTO!cp)xH-rg zNtmSSpsmV=9k9cZoSbZ0f@uE5-kc~`DppkdZNAZ{qx67M2RWk!t?Kwng2|8`CV2I| zQ|m&0(wM|LN2QRR-(lX*8d9%YEhgxz-7&B*8CPwlVV%Jyw~ zp)I0EOXWyR(tJ#or*!;pw`8PRBJ)W#j(f@q*^V3NLRsi=E=3G>_m8rM{AGaqP$ouJS-oWz77S(@|=iATvQK=Njnl{iRwT~#jk^7f2boGBFBcyqH0 zW0Jbm+{g*#vJyPW@D3d^#6sPElvVEg7IK*>c>rcg*BA6B6AEFv;}MJ$_mre2%JR9WSBg{Gq5TtL*m<>!$-yG2Slett5e^e0Hc6`Cj!X*i)4tc~HSjlI-N`dIaf2#_p|+%c1{# z2TlQBPN|Yo7oxht8u%dn|1>OdMADt&1;P2{nbN2q`_#$59LpipvsF?o9Jt_1j5!tYC=%nhxP>@a`C1{gy~F=}ecPBBMbJ}UL{K9}%ooso>)xnI9I zdM}cF%^NBFQsozm#;>wa?LtdaRzql_ebhRbpsyvSEv4SQ)l-mN=DR8%+{-VS(+}p1 z2wdsuqSDoxWem2Yv=PkosI1z6A6^L~-Kh^Zx2}$_b)D?m&2ynB`_dV%jLzgdlmjhvCO)6RadLIpHy_cWq@88GP@ssRzF|k)Qq2?R zv>-oM!d2ysrxR>))J}D2s+D3CeZ!bE(e3zV*rsmGFVEUBwI8F3L%X{sMOjtR=kXZz znnFwF^njPWk>W09^{r*^jofncVgEX>Py=&#VLTKPXcP#lWqljoSM{I~ec$dT?U}B! zs^UZzWJ9YR7H%yS!re6|ITIP^+|@li$(E4kpxdK89_nL_)`~eBCnywMJ*dvtlPq`G zH*44y6sb6z5sXm1rf?WGL<^QSoa-w5(bCE{{`S%ocB7m(*MrrjB_X{bi59J@a?`%_ zB;tK$DiULXJL`zCl*P@b6Q>G?F9fAsZ{4P+GIjCWiK{J}9@7T<`E_1J#&iU;eQS<` zIA|t*D*M8NxQr(Kn1h$SCh>tkW0GV;pa1UXL0SB#+o95O&XzPsm~H1?HB;*#ej0U8=whb?GQwJqYqTn2^HU(?M_jcE4%o?FMC(v)nx zwKr)L5q42+)K`Ei3$12wAeF>xn(utafZM5w5AZ99wX?ZlGCID40O~2Truc zu$k&o-qBL_t_+snc32mdODo+w)^SE&t+_#-B-zXYpD50pHr(2ZI6`eXlLkEBCQjdG zT@*uWLxn57oe?d~TIEN|mMl!3tugxeEY0ohyS4Kc9|ZEaIHvtOyq1gQe0dJB6K5~w ziH%c!zcY4hwcam4atankG%cKYN)$xD{`yX7pZv-#9DO+#Q8_p8xgzK_0iNz7A!ytH zY$ejm3OWY9w4hljy8l9#Oy~KZi`O${t5sAdb~FsJl-7N7zu5p^0n}%<<41#%{W~F! ztKA}Z^^brpmHevN&5zO}=5tuF5V#S{tR&mx6KDfAyi15e$AFqH&eJNSs&@Eu`dupA zsHs_LdQN$rnu=Ia-WTbJ_DkuBU01`%xMr_?STeflE2JZX_-eTY1hW>@^ZNOO1~ZT+ z9?WR20(Pi|s`BxeI=ATLUTinyg>7q)im<_gzKS1PP?Y%o3AQ zGEb=C%q+jGy;7~7^HiGh5IUyYCc{j@3oFQ*P8i^o;J9D z>@iSTD@JL&Yc~3;&KJZLCKvcn&*qU&e|gcYGIY{VIuSZzTrN8rmG zfI8Sbubnf^rz@rO2MPSyA@xC8gg5K`zb&mr948Ez)6?U9z6*iwUcojaEJZxy`-tn|*(84l~+bkz1hu0L0|oOTEcD@9EQ_1I&wqPE+bt%Py20f{4iH((01dT z*R0-85d|)O2cmUZjfcF|J?Ya>0^y)d4!RU}7+|pv($O&lp0UV^(20a>f-4l0M$jB_ zCUnd$glzv}{^Qtb5Qx^-aD2ros%N%%7r|h{ZYqQDc*!#rTbH)xLm(`2EbLO=VP1}0 zlv#)*Ix=)Bf@1hLb9V^#AL|v6uYP`Vrg}}eRymo>f`uf$Cc9GN%)2>|9U9~u#zYXE z@9xoX0*TyS+WdBvwa$v%Z+gD@^{7XNkmOyrR+Z%BmJTvbKdIYdIR3>-W&^yd09-Ne8oB&oma6_B?i= zFNvM@idlU%&qAji!O4WD{k94}-p5$s)_0E}`kcd@kc8j*C*K8U$8+3i0I0+&=&3 zTu7i~xc-wSV9r8@=4x?b2l_BPpDRYKF!5-)9I^hfYtb%8pn)T!wNay%u5* z`N4(&{WCE$%e}(G!8zHRC-o}Pxc`zkja?*+2p(lO^VJe%`xq`*Nu_M74H@lTsmK;f@~(*t_u{``Rk zlLLV4UF3_48xI(XPC!IL(h&pw%i>97^X@2G@b#X@`QW9stEcDf<;j*`18Dr3kHHjm z@lt+-hjs&b&+O7GS%fQW-k-wPzN30(Un=D}bpJguvxdck2w$ z_J$g$1Z&`abO19Ty7FRTP)RgY)Z6W{f#PDrzIebAZq5n#O6V{-m}0eCg_stvj;H(; zPVKkMc8g~jm039?O0=(vh|4IceoSujN@?GIREU)qHzX-<8h!kRSzItAzwfl%{?HMl z#oFRhe$))fN*!A{_+jPM<7l>%b7d2@HA6+IhH4Wshij$9h=lRLMIOfLiVZnwk@q-- z+tvow>mr}O;XPKiKC>m^WB8~6ZgBs-bD1&%_+KtCWUG2tbQ(1KqY$>1V@#IY{1yq` z=~`M}H-4h?nzvO33^~98pj7YQzlZ)D&6RrI^aeH(){Gr2GX*d2%sdu6?a>Ac{$M&2 zLw0-$%M5grTRG;_U3yM=Z)g`j5b}4P6Q};L2h;LZQajA#T-mnkFR!AcNe~&+rObWb zPR4wdmy=ggq!yhgX;A%^tWA|1{r|M~)lpS-U%RM)2qGY*q?9yBH%OPnfkPfzy1Nks zkp}7RI<(T=h@>DKQbJm!I|T1Sec#_VzWa@P#~tJT$KdR<_g-twIoDisKF@PLo_cM* zx*Tu3RZH$TQ1YXZRbhj~fjCu>PYSnV_M~_1C1V0%djCV!86GGY2><~2U46PiWn|ny z764pHi?57av(fo`0y7L00Sv*0PtI$CyjFUhwq~hYjC0I&7xM)VA&%-dg zchnU1wvbTW&0dgcqYuhzdx~E|uzuq=hL`bQzrkX_LBT(QqVXZDN1i%-aCqzp@!Q9* z`X1r)E5Es2c~{?&LH@e^?BmiK`BS+?7#a=>ZQ_&gc3NYHW+NO=vmw$X5@B5%MFhl| zQo`t4niBXLnb{ ztsfB8U$gk%xj#~ZdN53LBNmQqh$&v)VGjTR0O>3QvMo?a8lzgBH2eI?t)q3IubXL7rZw>}-Xc!`6Q$;RsiS`I;N?;8Uv?$8S)GS*)6t;`OX}RG01DLH>lfBv^_1}X||?l&$u|;W_rc~ zvL%z^FIjI&L(h9TAO@)(`<={-EYFzJ9;Bh_Gx4$Um1kg&1P3v^dtA~fnMUe!CHaFr z4gc0i*`VWUREx?UzPB_ERv_sR+WQEb^h#FDPjtZ+{GIW zi4xG&by|}G+-SXO^XQui8k179^MchSiQSv?56^!in7rAu-Rg*#rslGhpYr58T?=q5 zotT7SvhvkEYSh=N$by28e5DL$T`Gr{KjbO zwVroXShU}yy6^kYplK{A21Y-_v10$Wd>v)VHfemb$3|+!NlFa@$*xWVVbftVH;$xCb7&oiyR@96o+!FU!;gX@bs>5^3w(JMbkGbXzv;9ln1U zI5|S{Ji!ji|Goy5OU*W!Qqc5o=JWeOxUbwf1&1g>- z>I$okj|NT0b~pQji%2TnQp#oy_ahq($2y7WWqEpc4sVpe@Tr(^O$M%thZzy#S>L-$ z4}H|BDaBJ8_MIYy{Pydr=hSsQcCsst^X(respah&+O9fD#U~~4n)Kr}p!393Oifi= z;O3Noc537!HlwBP5Y70CjF?mqYPn4ZAC-pIp#~fs9qsMe=e=Y+c_@zD0T5&>#8TcU zVAwS0%@7kjXvo4Lh4a@9L&rG7b$UzlW&n~YwR`PBxq&Hl*!UU!+I*Tg%WE7~&!DLtY} z^R;AX)5^G)k+tM@h9VyMT)8AXM`BUN#{3Rc67cmH=Nu`j1HN2jxg2saaz z**$kQxnrN90lCgd)2}jh1P=|oeu~{^7F}`Md+UPKh&^-nL)<|D&2Y>6X4maN>-`s$ zir8kfCv)Vf3&9%1XrRtQ-{A6tfHHf@=yJ9p1;4Qi$KvL=Fe?>!eO10pQ#JDTQ`&Of zRzO}8t3v&G7D@Ci{v*VC)oPRZXu#woEUu-wDy^Kuz%X;Uj_qiYJw}}54o69OnbJUo zw!L7L92+GBUDsPwOTk1z$S4k^B)4i$El69m&|bDfVw@PI?dp2jctZ~ec^fv5HVk>N z=IGi*9h@aJ;BWh#vxPJcUG>$J(jPjt;nl>K1TB@mDoq=I=xMi|4r(_`gmdFR8QfE4 zf=c?>LNViai4Z{bMi}6H>g#!r->%)d-D)zB@aI;+U69H%dnP}2C$wueyz5Kdr!hs_yEcu3p9Sv z(TM;K6_nw7I3XWFrZ#O{8CH@WA*|N-)j}tnd{VrRm|8&;{vs>!^USAl?mzX%W8=4l z@)YPDJ*`+ncT1r=8XG4hOg(-**Wjb!ntzWwx7?p1E{R`5xJxjw zFp%D7bIdROwcPE+#X1exFJy*`-tc5~NqioASu`e$OKV;dJ~qKEZ(CTqp92v}zWXY0 zXI-seP3Q!baN*%njPL;Q<2X95U+zzr-*w<5{dP&)B)HTBAXS|o-tXxx zzI%|l1sV$s^k%W=2D7=-#@M-&Fea8p#Ut~64X={CW=z|1=curh0or7KkkqX}39gT; z+hpY*7tIGV_YF%iCsR7(6rHODPhs1-hSuYI)6E6aFO`zx35SGO%_-U}(@II6!yiG; z=@@c}o*A^mA-M$Qns(GVDnm~L6wz-bXc9QNv~Hg{IFI%X4t81^wD~jg$LSjNKMyh3|TFtNaWi zM4}tX0j8zx%+WW5*T{m>?5fy1;nJuP?H{VNL)2s9_jW(_FKoSHTgn4(JXEL-Qm zxJFv1yxB73>#)_1oe$B%{AsGwz)oH3*Z~nZ zcqg@Ui_>;aq=pIoPrnGEYZE8q;N+~C;f5``!if_MH5}ih>}VlBN}2(95g~<9xOW@? za%5#?VGy!#8jWXKawmI5F32n2?j&_EyPr5O6PJw`Q8s?D~n5GT5CW4 ziElXkbV5GS@DNqF8egS1(&;9}244`oJV zUrHP<=~VN(JN<*j_iWR+QdH=iP7%%E-0m5M!WLmpz?K_f8E5CWUAp>nzcM6^6s3%r z)?6BM^-lU8pADA;nFE1TK`kgID1p`kc~9&*Jg%zmlZVQCK?^%e5O3?FIIo^JCdDhF z&Cy+SBpC7-@+}$Z3uwLY+m;P+Xd=5(W-f3LRovG`$_7EikCZ=kR@t5Rze|@SpbrZbE_S;@c*pO8gI= zJhPsxjP2aLp$9CpM1tIk2nNi37i~rb#t&id; z_m3oO%M2~e2)sElC}M0VfFFie;r9H@g=yZC3c^z*Se|zCvN{_-(~b6d3mk<>>CnxwptA@^{UV_ngkB zgEopjq`*`HN)C|NI0m?^Oy~NHZd+5~jC>0vL}f5@(Q?@_bdY4;0_BnD`kD<8YUW~A z9pc4?MoDukq(4}kt9ezd4$!`E#<=I+3@m#wH^>(6iJoY!7hn8AphRPG4enRu4Pf0c z)^*B>4t40Ir6s*EM#DYzTbCVAA2qmlYE1hTzdoMHrJHp+O8{s3%SYHa(xK~^&VR=6 zPs{HS8w0|c9qN>@Uu0tr9UuFG(ZB6HQBXJr^uuFL$=n0ln^NI_D+D1&gdW6z zwPiSp2n9=9cQY%1_b-2ta|0OcF9Wrr-nBd2(s;!x}nb=l+ z`^f#5a!!^UtgW*#$Oyr1VQL4KbgCE6v~h0~HU51!F0|dp2mz~|cGG#icupH%?Jr%S zNB#^{AEHll;1i*QQ-Y6^H&~t$&`A6p_R24*e9lzYZ~I_Z>VJGmrD$BR3Zy9z>v7|y zZAo+e`%A2IFkg^C)6tbgdt@!wp|gIiX*(Ejf!t(cGC^g_ZiT}_Cr_H{8_VZoU@BFg z_6yANwTC~nHqqp^YisLS45b{NB-XUAa7(Ni=;tkKX4jr3(MKJOMqY+sF#-t=+{*q- zKE0uHO>r2LBjF^ztuiD6@Ifn>noxyyx?}hV~kq5}*&Ip+DfG=Rx6&e$$*@X{| z?l30m!`z8UPkRNnSz~uVQ^7=GF3x9yN2!T=+|J=#Np|^UD+3IZFk#XtQE%X3SEMO)aw2GIew8WeVIqXvc&z1B z$!@m@U1G&T#*qaPH${PjF}>c$mwBBZ!w!HF$e%SH8ipHi$bhY@RhQSg;1zt?r|(v6 zVs1NG^(8~%XBzylL~(Hrtb#-}L=7enG>(({5tjObMzgGTK4`^uyfzgekttT~W|k1? z2>N1z{_{m^)(eu=02_KE{1Yvh2CFScs6zjxixh+PdpfLtjdm*>o$XDJDKvmv8#6bj zQL(rfH9JzW`SVaRL}5NrT~C*GzLF$f$C0ia2}3G$)7Gok#Bcj#ZhP?HOF#c3pbO(mOpY+3c!HvWnRoR^G$VtiZxJ*CP{%NwAvzkHO!>ySIfr!L+8( zFeQ`K89fx{iz`rdxh#L7Qk*};2iX2Px5xs{?p*OMg^$jb83yg=yqOt{Aa4_J`HT=m zW2`ka_OG&yX7A` zj}UifRj}prSJZ7=(inlw&v}e(m`Aq(y74*+%#uFXWIZBuL3Z>Q(LtIHCx)icE@%R2 z)`*56%9i7=XA#kSGB{mhs}$AJkVcF*55bPGS=J^lM5Jo_zEdjHvikSR$;lj(uZW$I zcegcY{Zc6SHT`8u-&>(5JE-i4S~52$qX4{EcPx9;8Tok+5sH$W#)%7o+hQ*`F&;obD?q>ZwJB+T zJrKmvSJArE^3HaCYJ&oecY~479Ui>>W2s|sy5GTD=?mPGRjIU2Ga=IhP7K>n%wBRe zBINVA-)Lc`T6cug>%vzR4m9(AKQ0sxj~9=oLaviyQIqbg=aX5>L7&Ul{#f~-VdRh7 z(F&4B$Nt;@?O7lT*z6iB`ZJ@{5e)4F`_PH;c#X+98P-`>8p2u%9*}K4PM(r%j0`J+ zeH~_^l2;{)kM__bf{G_olC9#=ymSHm)YaG3hxfcId1`<^|3uEWp-B$zCoYQG@$eA; zuY0u86#TX|7GdP9k1sF

00XVB$}v|5Bs-K6-t1^@xq7z}7*aDx68d0xGrUVWEtf z9?#^vuR>-%wPq}#5ggG^N=u0QlNJ>3$C*_cIR=n9C*WYUaALlG;Q7s=X}54qLvo%G2j)_! z%qILn-tVvo7Cy){#+!?7Zh)y9SE{q!%9&k0UwQ0ko4Fd2xut*~iyv&=SeC~$;qFtt zZd87Q4#UuYCNB;`!&)#1LE{sM(pAOY5o=Vfdz7qIFbZ(4M2&P>HAIfocNAVfVq1Ily;S^hzx9l|3V~I# z@Qf3inggjA>$egUpXw$G01N*I1|+g9`l`oEK35<}Zwv30&=csDgDSKO&$G(Oed~u| z^e$6FOom4JbuAaRPwvv5^Y6SvT37d~b>prNp`NYCkIq^7ikeLM-W($M2(P!z1#{T; zBggDN7)MUdgfEf{aH9QoJAfS>b%G2@pLJV6ultWL12G;feCOw>Ts`7wA=LTaf>wZk zDYt>?QXv5`{5DcqLt3cLv6I++Z6of!az!q@1X+9LCcqNDX+KeE~^ zhuKLc>3s|uHt_S=%V>#trMo>V>6G~!6-k` z!Jc!`g9`N0&YOOmc1x5}TJZ7u5m+rR>mj+Sw~PGZ83Q1yBMU+QMMfjIP$(b z-tn;?EXkhSb8t@g#V1#o+#9eP4C@R!?yu(Phdl*2+y?-$-9HQ)h@#yM8*WZeNzzcs zzb11h2aJMlWAuUISax|9>NV2ll$uTB{yQ0^}@ZfmSMWkv8p5fGUz?*I>Fr*Qo|!@Uu{mp ziI-9`mS)a5_rs|o>v1-KL+7Z$6h%sG4%@VHzd3b=ezb08d->VMTeFGPEG_hNL$Z3D zF0|-cC-cc~wh|h}d~ROO)2b}W8&p4aNwSS_=35rW>u&;MZVMUaRn_hIY#Yy;21nhv z%*5Qn6&r>I56eu`cCCLD?AOh0eLA6t-e}5JG}M?G47DVF82CVU1MzNyfP+Q6vPt7! zbdW1`G$Trhy!|K7CnSnwL}DiVLm$*%psC^S7;z=NlSvDHwo4HD1X1<}wa-C`KE!LO z%anrG!IDDA@6L6&GPISn;vbh*S@=WTmhGtBE&xy*0ck@t81-d9`2wqih}WvE@D}C_ zX*@-v0iCrkCBKRJOgU^LH-E%Ro-b@YBS$a8Q%bArRanKR|B3WqRztFJLwX=lMfK;w z0gMfxp>P+Zuac}4fQ|9N-d~FDn`lVwa|##CV6q$X2!#Rw=l<_vnK%3v3ZRH7@tfmF z`}Z&5J$ay07V`Xt)b+2D<_*{D4X4EoVMQ#3;O{cL5oG?udIA5;Ab1-thf!LRkE$#3 zQgO{#aSgiuQW10TJAa{LEac{1Vj1ZFhj;nSA^V>n$NB&IwQJv|6Cx5dk6s5IXFsId zv5HyG;mQ^c)f^*_ZPU>a(MXc}1ifAu)vC9ti*Ad~V&kFlh>PXPtQP3xK*kA_Z87QU zjItR}`5!*faSB3~2{zJ(oW^l7SU6K2rMx8UvDF84eT)n;+=T+ZLN_)kg=8N8R#!nicb$vSZ5d z4!FuM%hHnK_$5--y>VRVgCHt4wtXNxZ3kGCK-${qgq^Rfm*wT>vQc3 zi6XNcA_8)9a=`F+>gwt;8%)YBqgPQ;0h08KZqp;!Dk7*zDbIyOfh+)xF3_Le07T#t z^|Cx+`^D#(pJ{cIlanotjE#*!!wKlzmMO(~J}owUXrSBZ9Q|A+5s)v{aXHx8py4Op9JDzeXj0QzyQ!al&B zU}k_P1QF3kfCdr`4UGzb@wGY6xlHP^Bo6GBv^~dF)5ywFskq*$Uyj_=R)O4s+L`}5 z4IKbG#iD=pG)Jq-6i6hS0g(qpAtq2iPIMtx1@zq;9sJF(kkrjrFTS~f<)VYDmucr~ z8&3o2e9&MnmPZg1iEZ{GJ;zm+HOTUpiymeSdA@#IF0R-l}zT$di#J^&pUG>8cFe1Y##rSPrjwC! zC5`SC;fb*Kicq;9#pn*aPcwHRj+g-2i}6_~;b`J>rW|Qzt>O}c>AZ;AZYd&@WQaAE zb93J~^v(457!3Mkfn!`OGDb4?{fG>u!fVSd%FNF^E*=TvzCzb7S_+u3%2)bY9`Or3 z2z0Nu{!4|QG4r{EKTnmf%kR(*xjph`6D(i+Jw>lnZ8epz9Cq$%5B%h6idu=6Tb1rA z6eylM?Pf(ZN)3DNisg%oKrpE9(Kc+I7ET_8{W|MNc6`Uq#L`!&Z#b35gIKNVvl8T* z-(A8tnkz)0frWi9)ke?51YfDotr3i=*VdlFN_BO!If3%&YCALvetEdxe&1$EiQHFm zD8}!6Nybnz{xIEU`tzW>MhgWAULEhq(hRytNJefOZXdUe{Y(69Q7(d;)g86#$HXYk z6yj5|8m*M*8T9$geA%-*cAS5_STxi9N!rW`clLn~pQqf%rQ^$fMpE+azI#orSzpxa zA40KiqMsu$f-e6}ET{68ba^kgM9*VdT3SA_h?J{Vj|6TAIaE6uH{LlAD|6mKqVgP( zsr|M2Ht<#o`QPJ-#Lw_@mV294^0A;i-u_GLP-??wEJpkT*%Yt_{aTxDeO56VuSzrF zJ%jC2J-CFn8$zD1?+<)U(S^ExP}}a1f1sJasy12^Hc+`1MGxi)&F4a)!BCWIsV6>j z6;CDOuFTQoNMjxBG z^nEGdM1KG57Zdk0e1lb#{%7wQgdJi+p0@aE+X`PCQ>~SMWd1KMx1a%;KHSUs(Vt<6 z;Yg!JKEAMwY;br6=5|5gs(|Ty zrH%|Pkx#B&d-(#=FtB`_UU5)*Q4OsbDEM{KTKPq;u9{HCKHmi2LKRAMwMy>MV3wXi zv@psMIm#GW#L~u-P?^8s{+)-at-So4w@@sM?W&~Bb0V(N+$-d3z*pJn$&9*_ga@(} zPP8Fbhmmo-c{~=nt=y9O_ucRX|5}!MfaQ?cIzu{F6*qxk4U^l?Sy~z>^fKr!3zXNg zpnSmBQ)}mEndAjRii?C%U=nBwpTq5c=Z% zc;>)Z5nkpWO5VHUMU|U2eE1hGN2054Lx^AWLv4#OoH6)%XN%5#l5T6Q>{sY9{*?zD z15S6Yno5SdEbspf3Ct*nfC|zCycAdQtzw!q~P4o`0+eph#&aQtB%i07^r=rtnsp$@-5G_8*kLSlK zVFTu35b;X277yx>Ol#7ZmVAHPv6QV^Nh_6>xlPxeIf4x4z1p6~LNi<4NCD6AIvr2+ zP;(S^s&8S5QbLt^ZW(Y49>P(3@&H_sqyFf_-VC}DQwuwza*>ZrM9ANu8;cTulA%DZ zxRw=jL6gYyL%AU>RH~f!X(4Bqfsb@5 zx^vYsuzism{Pj{K7}ejM@c)wd{eQXe*WFY7H-61|baLQ_43T6d6(!2W4E+BWO@j&i diff --git a/docs/images/c4-plantuml-getting-started3.png b/docs/images/c4-plantuml-getting-started3.png new file mode 100644 index 0000000000000000000000000000000000000000..bfbaaf0c296b1817802a88799baf8cde9c3fe45b GIT binary patch literal 37785 zcmdSARal+P)-AXK0RjZK0Kwf|g1fs*fZ!6`9TFf|2*KUm-7P_aOCY!t+}-6A-#&Z) z|2f^y>AvZUz6dMNTJKvmYu1=rqe7JwB#{vC5Fii;lC+eV3Iqbh4gR6Lf&$;1Sg_TC z4;p82EoWmpdk4Hl1zV!!Jw@NX@Nu39DNtirVOXSyFd8FR**cJspij23uO1>9f5S zD#!>Bd-eF<;9)x|XsY=r+)%yo81dXKBlw%*gm>x3KDPN*Ee7y+Yc87C-Q#W&5z9O?JaHh5279Rv8r>;0%DsCsW_(u+z-)y(E479 zn$w&%TWc;}+}(wT7aJF-Zi!mjM!kJrKj;~SMlbnt4UD8$w?p3i!oH={F;YaOBkM~3 z>%e9IO~|XRbEQ9Zw|nfqWNyDSHx51&m$D;n)Pn3Z^kBPh_EPOTPYLn(U&fU%)Hib< z*ZzF-5hO()FybAofPKlv^qtW7By0ttryh$xAcih)46MghfxTyJq; z&j`_{r*+Iqy^|$!*h66yh0O3@e`ShTC)tro;7tMvE>X^3&wr+T)%}58FO!12jcnBQ zJL2n)X1uaI-%vN{f9?B-V6?X9v*hK3PDY2Jb~f9itTcz*3a)6r%FKgy+gO3T&?IUR zTM*4Z!Q#cbE)vVZ7+DN&MU@B~icI~yMeHfa%h1Zr$a4F5d1>}BBofC6CglxE#7*a* zz$!f*iKS=n!$CWMoDLe(!JDZW6Z`=1f_WZV^7JR}>;25w6=56$3JQtY0 z)O|*N4@EG%GIwt4u-zbUY#a6GrRqKOyWgMi;mQSYn;RhRjNZNIO9mvilpzjECJCkBR4!#H~dJFRZgVLniV0m_v-G=>{1s(lqK_)x|XIC zUncGLctGxg!DPp@#RH~i=HHnVp~wzOJR7-ghB#{EgAm$+#S*yv-F108Xv8k zoOTEgq*8{k`%^O~5{d=;pV}mw>FG}!#iQKc(VKk^_NdzK)6Dy#Q{+Z|$TV3(U?bG_ zrh*twG-N6(z))0HQG&B|^W{mp^_vJBR|O-nn5Xw|Th-qR;`5gY6T-Jir0|7i`B)6y zzY%2D@vRj6hHXbG+)7nOyo=JM7rHw7Xzm58u~9v>eNJ9lym*_bEA#ahnq(dB?o;K7 zF&_rLzSC!SMNVOhjzT9_oOAOWH;H*dmeepDU2?1$~7 z!Pa@EJQ9-;t6VZd0*Yvk(l}S*fzK^Q@TC1e6_Q~*rlv&L4jfxG^>K?YTnn7?rGE+P zU3k3ARw?1ad*5HgftReKAo1sc=4yn!PDg$3(=<=<+bo%h{@z%MSJK}0WtyQRxg4K9 zK2-Y`9zs4CL5b*yWtU+K?{j`CKo?Fm6?#p}C!&MaoqrNS4-xdi717B2TlQcfwndC~ z&$n*(qH5BkyPTVRII@Sh9FO-!TQTZ^Xka~mpoC1;i zIYNhnCnGO0F2=Pf@r73V5Kk$Ig=R5MCK6JczalJa^`t(3MUXT}LnJ$xtM_TMnc83D z%x+o`4+e) zLoee>?0F)!@M%WE$by{Ur=-_+<)T-Hh#JeGsMS*yuW3TGOg~Igp^=mbg9e6aUr=8lvvf#k8I*kVh8Idz3aL`p^ zh@SWDHIpOu-NI8^YK_3;j`HdG0wyxlkv%rAB%u>F)3R*_BmEv$2C4VeJpS+cPOru+Zu;-Iby`D#Nz{HG;(zTX9rI1Fn~{>JW&TI>=u;Yn z!8vjMlep#M+UK{GPu4#5LbpD!KhpF97(?C;!o4%~V#h1NEbcQe*}+>L^Jp5sjgfw| z!Vb+9uXPN5U$qp-i|#Ma%EulN@v`?&P`77I!|${%_=j1QlsR3~ix=5m*zhSc%6>vx z;+u>MUqY&57Q#_2ddj6B(&UHhN%n3p#x^1u7yIEgqOgb%+nnsZ$Av}Ujd<31I6 z{X!?833B5~XzVOYqk(E(_58-79mCD1V(}Sy8XMOQxz8YIzJ!c<7fxiXp&jp=Jku<- zpLeTKzz?r^{4(oE z9n0vNx5c;89(Bs4=o1q+!bLU!f%rqD#e~(|_4m`@w6HYidTUawe{?6N58GBtyK=SM z9hh<1)b!hu4&${{C5S~-5o3pid`7DB4cc3n)8>#95=gB4kVN(zyP8b(-rt&uX-b6tpHG(aO%|_XPX4Pu40mss%zzXE$0eVe~u6~wv{tt>2o-|CSsX z8PTn`J~=A>AlhhoxBMO>V^ ztu156#XI;gbX8SVOT(X^VZ3Jv`8|~o^lHv%4E!E0o*tH;sy0eX>2-DInL6&PjeEb< z?x6_YkByH<<8_(aS~SC8Vq;%ETrGFp?v_|fz8+5$566Buu=Dde>A|64#$z>+GBV1} zEPS(}_q)UI37LQ=BvsSGg8CrpvQuMla4;=3l^Q#%*rLGi{x4B568^8NxJs@!-uw)l z7w={bml_>%oVG^(d~I0r40j=O`Mv!+XXl53=T4q*0PM#4`YYMG2#$|aV~6!N^OQ?t zZ#T@vMO9OZ!sX=T3Wtx{?+)*8&i3~9f^fwuohSX=eiujka1<`|k2^`v18>qS8EAS0c9!e;J`9i3D!L-_>}rs;>Mmu9d$jC z@hDav&!DNU9_96LQQ;^50#}v<*ZuKktAC?Pz1jEaEaS=A*!b_(R)gh4ekcZ&3fs0( zM{RAasm1qqIIcH}Nb{{8HR0iivIPVE=E@IKQ&X|+&O77F4~K5+>CrJUH`S!EZav^d z3xoFd_RjA97%J7fiShB7-9qIY>e1uXuA-l{6Tmcl0fgO-mP~cAE<$PL^Hlz99kpE% zPBuCGm0)9IlhLj+>JfZAiOleOO!7@AQY}!CsnBn`CbjwX=g)bZroOVTaam7Ic2?0a zk@xY7e(1U+UpE;NtaUc(T~sVTw$29A&s7;qzJ-ovf@VOl^sN@ob6bed9zs5sDL(_Cc3O-8N zR^{h^QTawBs6YdULTHk-gRbSQ+zJrBm0Z%}#ESfuo4BuM_3%)k7whfYp@M}DzvY=3 zcQDoG_rpp{Sao%Ee%D<@k3hl4G|rgh)*?2UA8(!>j{NS6F`Im{WmIPk_1iq-%ErqL zybd^iR~!S|QyR#TjJ%Gm7e3UzQD!f6)fl5Cz!86EU6-31*cm%2aQAg|EHhJ7?S4^Q zB+uKR*sc2fz14|1jAz94J#@3QkPzgC+qEJfQ`XpV;vVK=`RVb>ZP`b2n|pV6*Y|FI z+4r=c$~)}68yQzrs~|->zgK+x&cJz^+Y&q?VovPG>2l?;vN8sbnATQ4Wt!-!`}_OU z)VE=J)MqnQ#y9e{x4$Kc`1|L&6AXN%eQ68ct=n$%=%lLqpO$CBs;(W(H_Yw=e5Ygd z(Vjd4)4kbDX>ExmZBSIRq>V#wa(8lal35REf=yz|kCjbegm6p`NV(Xyt;NJZ99T^DrO^Hmf^8fr~)m+Pl$<{fgx{Du36u#q_~m1=}^L! z^du5^nUacjS~|LC_ayzA+H~c|Cv0r&rluxLh1uEJlXsR$R@=)RPx;M@;0)-Cnrz&Pw_e>lwPE<C)_@J?>v&VvpYTP6fKbt(0-{A4${Jl31>n~bO6Z|w>OwJyIM1LaWn3=W37 zHUQ_el??Y1)6xelN>PTBot%oQVLR*1)BTp9;Qg_vgVi3It9Zb^9^KsEcAvY=p(Ig6 zOn3OxFvBEZpg#)w4uH04`QIofBgO#=R-dD95S=@2X=tpdcd34# zD@Zr?v3rYXOjsd}heD=*Xvq7ZZrWY(Ct(UA9NpdBonS%dhcK|Es@dn1{os=Z80OO* z9Fg}A6(eI~njr6G5NnCwa$cSeGKCWkeI*iX7f0!_cX7#+ca~<9(PS-d>?ybSj#P9F zUR=(T#c`Yf;WCYwDR_HF$JLdy&agWK4LBD8<-DTtuGmYDaFIMx%>Bm=CO?nA5nQ>7 zrclt(vtDY&saVCe+P1;9t8R7<1~z*p65;=do{qRpe9m-r(n~ZyGjn}=YrE7s`}=p) zduDi8K>}#}kb96t=;-Rkuq6xl+}in`rF4K;CL$iiLu|A$5Epwr$kegF)aEss&hPE% z>nk9xR%^M?=-|E9a?~zM^X!FgFh8^MQQw8;Rdm??7G(iA85$n;yC^Y0 zBH)P)o5&i@5X=apYeKcHqIvhXsNBnejC8?WC@iGKT z0sEcaz~ed_Dm+bQml^opP~=%Jx2Gu@Ruqr9G!x@oWmELSsv=sw@en-42ej>;V(D?;&EUz+v?>+?gr>8UK#8eey{gA8|K0cREE8sZ}W2d zO~5JcGj61s7ff^DgoL0TU>(TCu>%mZ46Ib<;R&Qc=ZA}4M@wz@@BJ9k|Ffr>rUo|K z|Fx(7u0j$Lut9h+l=A3y5Xd%2-Nuj|TwU{JkB*P+PhcVbgiu*f2VwkPm-Ghh_jw8W zRwq7M5dU__^S4_jPU?v%DFp=u;SDb!=NPZBKq{IBD*^+72o-!%0{Q8f<-gNYb^Lxh zTG`<~#0VeMH%VfCmPsnzN#OB9q(B*UnE|@d_u($bJVv%nn^$dVU)yWq=e&G2U8_!= z-@EO14quL*>aWhF#i?IvKi}(qhy&7Yl=r&!+#VZm=y(^CHukM(<3$CDy+2D-N z-VH4sv>g8>krl+Sl^3fX^#*eR0JD;b9 z$0g2EC84Q!2A_ZH;bZK)?xMNsY{f5nL3C7xasrTZTL~4eI~|{v2_nCjVx1`3qo2Hx zHc~i3(K;7?f>Bf43N6WwIYyubV}oow@w^wt=fJzM#T)zb7q(4&q1k1NFXHmf&dl#2 z+fU2=$#cJw%77e&P=Up`TejV8cWr+M^IaFts1U1`3XLW7QUz`e(RK{3EcIt^)=;oO zb5`kP;4XZLs~#(7mB*}gepOJ8nkBoDowr~|OwUjx+&ii~({MKwpnF18+HbeFx3l_^ zGz%=8NtO#e(zMLU?1%+iExg`icD~k*`Hf&f5OSeH?nD{fq!IM+u=t_Qli|Y}!F|Ce zLyh)E8t`*6bk^G8HNSeCk=g2XanM78U=vonS_uA2Fok~>;^gd}!>=?O6SsSEoopMn zjI-*ZgY6sL9vdJk@sG?P+6RZ9y!=~lQf4e4nqr~6`agS-xo;&YIShia*$u};F4``d zktyrv@c&L%YY+LF%oI^qR(}ZgJ!?$y#q%j?u$lU4jSb^^LZiK=0R&*y$~xtLI~1gfmrNop&$jlN zcD}#dtGf)y{vlCBT~GB+xZ_5GvR-A_UBk?TXAg_5rL>jQLH7mZ#|n#B77w9w3Sz;W z097;gQ0MQ$n&D)A&2AfjzkpuUAgi(ch+kh)=VP*(#|=B!j=UBdBgdILhYu;eqdc68w z7HbBB#;ZkUGB#V*i$J)@oˑo~HHAT2A2ykzh_3)@y8?b@NJLkrM^KMQ?UGoFuu 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 O-s9?oLeLxoZv6`pJQ;fc literal 0 HcmV?d00001