From 87ce66281bb91e8f7f3ff2f88f9fa52a04f8bde3 Mon Sep 17 00:00:00 2001 From: Christoph Knauf Date: Fri, 26 Apr 2024 13:46:22 +0200 Subject: [PATCH 1/2] show container boundaries in component diagrams only when componentView.showExternalContainerBoundaries = true --- .../export/ExtendedC4PlantUMLExporter.kt | 15 ++- .../export/exporter/ComponentViewExporter.kt | 74 ++++++++++++ .../view/ExternalBoundariesExtensions.kt | 9 ++ .../structurizrextension/ComponentViewTest.kt | 108 ++++++++++++++---- .../expected/ComponentWithBoundary.puml | 30 +++++ .../expected/ComponentWithContainers.puml | 33 ++++++ ...ComponentWithContainersAndBoundaries.puml} | 9 +- .../expected/ComponentWithoutBoundary.puml | 29 +++++ 8 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt create mode 100644 src/test/resources/expected/ComponentWithBoundary.puml create mode 100644 src/test/resources/expected/ComponentWithContainers.puml rename src/test/resources/expected/{Component.puml => ComponentWithContainersAndBoundaries.puml} (87%) create mode 100644 src/test/resources/expected/ComponentWithoutBoundary.puml diff --git a/src/main/kotlin/com/github/chriskn/structurizrextension/export/ExtendedC4PlantUMLExporter.kt b/src/main/kotlin/com/github/chriskn/structurizrextension/export/ExtendedC4PlantUMLExporter.kt index 0c515ef..69e3cfe 100644 --- a/src/main/kotlin/com/github/chriskn/structurizrextension/export/ExtendedC4PlantUMLExporter.kt +++ b/src/main/kotlin/com/github/chriskn/structurizrextension/export/ExtendedC4PlantUMLExporter.kt @@ -1,5 +1,6 @@ package com.github.chriskn.structurizrextension.export +import com.github.chriskn.structurizrextension.export.exporter.ComponentViewExporter import com.github.chriskn.structurizrextension.export.exporter.ContainerViewExporter import com.github.chriskn.structurizrextension.export.exporter.DeploymentViewExporter import com.github.chriskn.structurizrextension.export.exporter.DynamicViewExporter @@ -16,6 +17,7 @@ import com.structurizr.model.Container import com.structurizr.model.DeploymentNode import com.structurizr.model.Element import com.structurizr.model.SoftwareSystem +import com.structurizr.view.ComponentView import com.structurizr.view.ContainerView import com.structurizr.view.DeploymentView import com.structurizr.view.DynamicView @@ -46,7 +48,14 @@ class ExtendedC4PlantUMLExporter : AbstractDiagramExporter() { elementWriter, relationshipWriter ) - private val componentViewExporter = ContainerViewExporter( + private val containerViewExporter = ContainerViewExporter( + boundaryWriter, + footerWriter, + headerWriter, + elementWriter, + relationshipWriter + ) + private val componentViewExporter = ComponentViewExporter( boundaryWriter, footerWriter, headerWriter, @@ -78,7 +87,9 @@ class ExtendedC4PlantUMLExporter : AbstractDiagramExporter() { override fun export(view: DeploymentView): Diagram = deploymentViewExporter.exportDeploymentView(view) - override fun export(view: ContainerView): Diagram = componentViewExporter.exportContainerView(view) + override fun export(view: ContainerView): Diagram = containerViewExporter.exportContainerView(view) + + override fun export(view: ComponentView): Diagram = componentViewExporter.exportComponentView(view) override fun createDiagram(view: ModelView, definition: String): Diagram = createC4Diagram(view, definition) diff --git a/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt b/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt new file mode 100644 index 0000000..0155a30 --- /dev/null +++ b/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt @@ -0,0 +1,74 @@ +package com.github.chriskn.structurizrextension.export.exporter + +import com.github.chriskn.structurizrextension.export.createC4Diagram +import com.github.chriskn.structurizrextension.export.writer.BoundaryWriter +import com.github.chriskn.structurizrextension.export.writer.ElementWriter +import com.github.chriskn.structurizrextension.export.writer.FooterWriter +import com.github.chriskn.structurizrextension.export.writer.HeaderWriter +import com.github.chriskn.structurizrextension.export.writer.RelationshipWriter +import com.structurizr.export.Diagram +import com.structurizr.export.IndentingWriter +import com.structurizr.model.Component +import com.structurizr.model.Container +import com.structurizr.view.ComponentView +import com.structurizr.view.ElementView +import com.structurizr.view.ModelView + +class ComponentViewExporter( + private val boundaryWriter: BoundaryWriter, + private val footerWriter: FooterWriter, + private val headerWriter: HeaderWriter, + private val elementWriter: ElementWriter, + private val relationshipWriter: RelationshipWriter +) { + + internal fun exportComponentView(view: ComponentView): Diagram { + val writer = IndentingWriter() + headerWriter.writeHeader(view, writer) + + var elementsWritten = false + for (elementView in view.elements) { + if (elementView.element !is Component) { + elementWriter.writeElement(view, elementView.element, writer) + elementsWritten = true + } + } + if (elementsWritten) { + writer.writeLine() + } + + val containers: List = getBoundaryContainer(view) + for (container in containers) { + val showContainerBoundary = + container == view.container && view.externalContainerBoundariesVisible + if (showContainerBoundary) { + boundaryWriter.startContainerBoundary(container, writer, view) + } + for (elementView in view.elements) { + if (elementView.element.parent === container) { + elementWriter.writeElement(view, elementView.element, writer) + } + } + if (showContainerBoundary) { + boundaryWriter.endContainerBoundary(writer, view) + } else { + writer.writeLine() + } + } + + relationshipWriter.writeRelationships(view, writer) + + footerWriter.writeFooter(view, writer) + + return createC4Diagram(view, writer.toString()) + } + + private fun getBoundaryContainer(view: ModelView): List = + view.elements + .asSequence() + .map { obj: ElementView -> obj.element } + .filterIsInstance() + .map { it.container } + .toSet() + .toList() +} diff --git a/src/main/kotlin/com/github/chriskn/structurizrextension/view/ExternalBoundariesExtensions.kt b/src/main/kotlin/com/github/chriskn/structurizrextension/view/ExternalBoundariesExtensions.kt index 94ca9b9..d531112 100644 --- a/src/main/kotlin/com/github/chriskn/structurizrextension/view/ExternalBoundariesExtensions.kt +++ b/src/main/kotlin/com/github/chriskn/structurizrextension/view/ExternalBoundariesExtensions.kt @@ -1,5 +1,6 @@ package com.github.chriskn.structurizrextension.view +import com.structurizr.view.ComponentView import com.structurizr.view.ContainerView import com.structurizr.view.DynamicView @@ -16,3 +17,11 @@ var ContainerView.showExternalSoftwareSystemBoundaries: Boolean @Suppress("DEPRECATION") this.externalSoftwareSystemBoundariesVisible = value } + +var ComponentView.showExternalContainerBoundaries: Boolean + // naming bug in structurizr + get() = this.externalContainerBoundariesVisible + set(value) { + @Suppress("DEPRECATION") + this.setExternalSoftwareSystemBoundariesVisible(value) + } diff --git a/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt b/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt index dece850..289a3f6 100644 --- a/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt +++ b/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt @@ -7,12 +7,12 @@ import com.github.chriskn.structurizrextension.model.component import com.github.chriskn.structurizrextension.model.container import com.github.chriskn.structurizrextension.model.person import com.github.chriskn.structurizrextension.model.softwareSystem -import com.github.chriskn.structurizrextension.model.usedBy import com.github.chriskn.structurizrextension.model.uses import com.github.chriskn.structurizrextension.plantuml.C4PlantUmlLayout import com.github.chriskn.structurizrextension.plantuml.Layout import com.github.chriskn.structurizrextension.plantuml.LineType import com.github.chriskn.structurizrextension.view.componentView +import com.github.chriskn.structurizrextension.view.showExternalContainerBoundaries import com.structurizr.Workspace import com.structurizr.model.InteractionStyle import com.structurizr.model.Location @@ -20,21 +20,19 @@ import org.junit.jupiter.api.Test class ComponentViewTest { - private val diagramKey = "Component" + private val workspace = Workspace("My Workspace", "") + private val model = workspace.model + private val softwareSystem = model.softwareSystem("My Software System", "system description") + private val backendApplication = softwareSystem.container("Backend App", "some backend app", technology = "Kotlin") - @Test - fun `component diagram is written as expected`() { - val workspace = Workspace("My Workspace", "") - val model = workspace.model - val softwareSystem = model.softwareSystem("My Software System", "system description") - val backendApplication = softwareSystem.container("Backend App", "some backend app", technology = "Kotlin") + init { val user = model.person("User", "A user", Location.External) val restController = backendApplication.component( "MyRestController", "Provides data via rest", - technology = "REST" + technology = "REST", + usedBy = listOf(Dependency(user, "Website", "REST")) ) - restController.usedBy(user, "Website", "REST") val repository = backendApplication.component( "MyRepo", "Provides CRUD operations for data", @@ -49,21 +47,23 @@ class ComponentViewTest { technology = "", icon = "kotlin", usedBy = listOf(Dependency(restController, "calls")), + uses = listOf( + Dependency( + repository, + "gets notified", + interactionStyle = InteractionStyle.Asynchronous, + ) + ) ) - service.uses( - repository, - "gets notified", - interactionStyle = InteractionStyle.Asynchronous, - ) - val backendApp = backendApplication.component( + backendApplication.component( "Cache", "In Memory DB", link = "https://google.de", technology = "RocksDB", icon = "rocksdb", c4Type = C4Type.DATABASE, + usedBy = listOf(Dependency(service, "uses", link = "")) ) - backendApp.usedBy(service, "uses", link = "") softwareSystem.container( "Database", "some database", @@ -72,12 +72,76 @@ class ComponentViewTest { icon = "postgresql", usedBy = listOf(Dependency(backendApplication.components.first { it.hasTag("repo") }, "gets data from")) ) - val maintainer = model.person( + model.person( "Maintainer", "some employee", - location = Location.Internal + location = Location.Internal, + uses = listOf(Dependency(restController, "Admin UI", "REST", icon = "empty")) + ) + } + + @Test + fun `component diagram is written without boundary if it contains only components`() { + val diagramKey = "ComponentWithoutBoundary" + val componentView = workspace.views.componentView( + backendApplication, + diagramKey, + "Test component view", + C4PlantUmlLayout( + nodeSep = 100, + rankSep = 150, + lineType = LineType.Ortho, + layout = Layout.LeftToRight + ) + ) + componentView.addAllComponents() + + assertExpectedDiagramWasWrittenForView(workspace, diagramKey) + } + + @Test + fun `component diagram is written with boundary if container boundaries are visible`() { + val diagramKey = "ComponentWithBoundary" + val componentView = workspace.views.componentView( + backendApplication, + diagramKey, + "Test component view", + C4PlantUmlLayout( + nodeSep = 100, + rankSep = 150, + lineType = LineType.Ortho, + layout = Layout.LeftToRight + ) ) - maintainer.uses(restController, "Admin UI", "REST", icon = "empty") + componentView.addAllComponents() + componentView.showExternalContainerBoundaries = true + + assertExpectedDiagramWasWrittenForView(workspace, diagramKey) + } + + @Test + fun `component diagram is written without boundary if containers are added`() { + val diagramKey = "ComponentWithContainers" + val componentView = workspace.views.componentView( + backendApplication, + diagramKey, + "Test component view", + C4PlantUmlLayout( + nodeSep = 100, + rankSep = 150, + lineType = LineType.Ortho, + layout = Layout.LeftToRight + ) + ) + componentView.addAllComponents() + componentView.addAllContainers() + + assertExpectedDiagramWasWrittenForView(workspace, diagramKey) + } + + @Test + fun `component diagram is written with boundary if containers are added and container boundaries are visible`() { + val diagramKey = "ComponentWithContainersAndBoundaries" val componentView = workspace.views.componentView( backendApplication, diagramKey, @@ -89,7 +153,9 @@ class ComponentViewTest { layout = Layout.LeftToRight ) ) - componentView.addAllElements() + componentView.addAllComponents() + componentView.addAllContainers() + componentView.showExternalContainerBoundaries = true assertExpectedDiagramWasWrittenForView(workspace, diagramKey) } diff --git a/src/test/resources/expected/ComponentWithBoundary.puml b/src/test/resources/expected/ComponentWithBoundary.puml new file mode 100644 index 0000000..7305ee3 --- /dev/null +++ b/src/test/resources/expected/ComponentWithBoundary.puml @@ -0,0 +1,30 @@ +@startuml(id=ComponentWithBoundary) +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/kotlin.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/rocksdb.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml +title My Software System - Backend App - Components +caption Test component view + +SHOW_PERSON_OUTLINE() +LAYOUT_LEFT_RIGHT() + +AddRelTag("async relationship", $textColor="$ARROW_COLOR", $lineColor="$ARROW_COLOR", $lineStyle = DashedLine()) + +Container_Boundary("MySoftwareSystem.BackendApp_boundary", "Backend App") { + Component(MySoftwareSystem.BackendApp.MyRestController, "MyRestController", "REST", "Provides data via rest", "") + WithoutPropertyHeader() + AddProperty("jdbcUrl", "someurl") + Component(MySoftwareSystem.BackendApp.MyRepo, "MyRepo", "Kotlin, JDBC", "Provides CRUD operations for data", "") + Component(MySoftwareSystem.BackendApp.MyService, "MyService", "", "Does implement some logic", "kotlin", $link="https://google.de") + ComponentDb(MySoftwareSystem.BackendApp.Cache, "Cache", "RocksDB", "In Memory DB", "rocksdb", $link="https://google.de") +} +Rel(MySoftwareSystem.BackendApp.MyRestController, MySoftwareSystem.BackendApp.MyService, "calls") +Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.Cache, "uses") +Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.MyRepo, "gets notified", $tags="async relationship") + +skinparam linetype ortho +skinparam nodesep 100 +skinparam ranksep 150 +SHOW_LEGEND(true) + +@enduml \ No newline at end of file diff --git a/src/test/resources/expected/ComponentWithContainers.puml b/src/test/resources/expected/ComponentWithContainers.puml new file mode 100644 index 0000000..461968a --- /dev/null +++ b/src/test/resources/expected/ComponentWithContainers.puml @@ -0,0 +1,33 @@ +@startuml(id=ComponentWithContainers) +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/kotlin.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/postgresql.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/rocksdb.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml +title My Software System - Backend App - Components +caption Test component view + +SHOW_PERSON_OUTLINE() +LAYOUT_LEFT_RIGHT() + +AddRelTag("async relationship", $textColor="$ARROW_COLOR", $lineColor="$ARROW_COLOR", $lineStyle = DashedLine()) + +ContainerDb(MySoftwareSystem.Database, "Database", "postgres", "some database", "postgresql") + +Component(MySoftwareSystem.BackendApp.MyRestController, "MyRestController", "REST", "Provides data via rest", "") +WithoutPropertyHeader() +AddProperty("jdbcUrl", "someurl") +Component(MySoftwareSystem.BackendApp.MyRepo, "MyRepo", "Kotlin, JDBC", "Provides CRUD operations for data", "") +Component(MySoftwareSystem.BackendApp.MyService, "MyService", "", "Does implement some logic", "kotlin", $link="https://google.de") +ComponentDb(MySoftwareSystem.BackendApp.Cache, "Cache", "RocksDB", "In Memory DB", "rocksdb", $link="https://google.de") + +Rel(MySoftwareSystem.BackendApp.MyRepo, MySoftwareSystem.Database, "gets data from") +Rel(MySoftwareSystem.BackendApp.MyRestController, MySoftwareSystem.BackendApp.MyService, "calls") +Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.Cache, "uses") +Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.MyRepo, "gets notified", $tags="async relationship") + +skinparam linetype ortho +skinparam nodesep 100 +skinparam ranksep 150 +SHOW_LEGEND(true) + +@enduml \ No newline at end of file diff --git a/src/test/resources/expected/Component.puml b/src/test/resources/expected/ComponentWithContainersAndBoundaries.puml similarity index 87% rename from src/test/resources/expected/Component.puml rename to src/test/resources/expected/ComponentWithContainersAndBoundaries.puml index a095676..97ecddc 100644 --- a/src/test/resources/expected/Component.puml +++ b/src/test/resources/expected/ComponentWithContainersAndBoundaries.puml @@ -1,4 +1,4 @@ -@startuml(id=Component) +@startuml(id=ComponentWithContainersAndBoundaries) !includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/kotlin.puml !includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/postgresql.puml !includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/rocksdb.puml @@ -12,24 +12,19 @@ LAYOUT_LEFT_RIGHT() AddRelTag("async relationship", $textColor="$ARROW_COLOR", $lineColor="$ARROW_COLOR", $lineStyle = DashedLine()) ContainerDb(MySoftwareSystem.Database, "Database", "postgres", "some database", "postgresql") -Person_Ext(User, "User", "A user", "") -Person(Maintainer, "Maintainer", "some employee", "") Container_Boundary("MySoftwareSystem.BackendApp_boundary", "Backend App") { - ComponentDb(MySoftwareSystem.BackendApp.Cache, "Cache", "RocksDB", "In Memory DB", "rocksdb", $link="https://google.de") Component(MySoftwareSystem.BackendApp.MyRestController, "MyRestController", "REST", "Provides data via rest", "") WithoutPropertyHeader() AddProperty("jdbcUrl", "someurl") Component(MySoftwareSystem.BackendApp.MyRepo, "MyRepo", "Kotlin, JDBC", "Provides CRUD operations for data", "") Component(MySoftwareSystem.BackendApp.MyService, "MyService", "", "Does implement some logic", "kotlin", $link="https://google.de") + ComponentDb(MySoftwareSystem.BackendApp.Cache, "Cache", "RocksDB", "In Memory DB", "rocksdb", $link="https://google.de") } - -Rel(Maintainer, MySoftwareSystem.BackendApp.MyRestController, "Admin UI", "REST") Rel(MySoftwareSystem.BackendApp.MyRepo, MySoftwareSystem.Database, "gets data from") Rel(MySoftwareSystem.BackendApp.MyRestController, MySoftwareSystem.BackendApp.MyService, "calls") Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.Cache, "uses") Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.MyRepo, "gets notified", $tags="async relationship") -Rel(User, MySoftwareSystem.BackendApp.MyRestController, "Website", "REST") skinparam linetype ortho skinparam nodesep 100 diff --git a/src/test/resources/expected/ComponentWithoutBoundary.puml b/src/test/resources/expected/ComponentWithoutBoundary.puml new file mode 100644 index 0000000..a62b90d --- /dev/null +++ b/src/test/resources/expected/ComponentWithoutBoundary.puml @@ -0,0 +1,29 @@ +@startuml(id=ComponentWithoutBoundary) +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/kotlin.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/rocksdb.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml +title My Software System - Backend App - Components +caption Test component view + +SHOW_PERSON_OUTLINE() +LAYOUT_LEFT_RIGHT() + +AddRelTag("async relationship", $textColor="$ARROW_COLOR", $lineColor="$ARROW_COLOR", $lineStyle = DashedLine()) + +Component(MySoftwareSystem.BackendApp.MyRestController, "MyRestController", "REST", "Provides data via rest", "") +WithoutPropertyHeader() +AddProperty("jdbcUrl", "someurl") +Component(MySoftwareSystem.BackendApp.MyRepo, "MyRepo", "Kotlin, JDBC", "Provides CRUD operations for data", "") +Component(MySoftwareSystem.BackendApp.MyService, "MyService", "", "Does implement some logic", "kotlin", $link="https://google.de") +ComponentDb(MySoftwareSystem.BackendApp.Cache, "Cache", "RocksDB", "In Memory DB", "rocksdb", $link="https://google.de") + +Rel(MySoftwareSystem.BackendApp.MyRestController, MySoftwareSystem.BackendApp.MyService, "calls") +Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.Cache, "uses") +Rel(MySoftwareSystem.BackendApp.MyService, MySoftwareSystem.BackendApp.MyRepo, "gets notified", $tags="async relationship") + +skinparam linetype ortho +skinparam nodesep 100 +skinparam ranksep 150 +SHOW_LEGEND(true) + +@enduml \ No newline at end of file From 5bb75a4f9b144fb4231c178e0d6978943ebe4e10 Mon Sep 17 00:00:00 2001 From: Christoph Knauf Date: Fri, 26 Apr 2024 16:29:08 +0200 Subject: [PATCH 2/2] show system boundaries in container diagrams only when containerView.showExternalSoftwareSystemBoundaries = true --- .../export/exporter/ComponentViewExporter.kt | 3 +- .../export/exporter/ContainerViewExporter.kt | 19 +++-- .../structurizrextension/ComponentViewTest.kt | 2 +- .../structurizrextension/ContainerViewTest.kt | 78 +++++++++++++++++-- .../expected/ContainerWithBoundary.puml | 5 +- .../expected/ContainerWithSystems.puml | 40 ++++++++++ .../ContainerWithSystemsAndBoundaries.puml | 50 ++++++++++++ .../expected/ContainerWithoutBoundary.puml | 13 ++-- 8 files changed, 180 insertions(+), 30 deletions(-) create mode 100644 src/test/resources/expected/ContainerWithSystems.puml create mode 100644 src/test/resources/expected/ContainerWithSystemsAndBoundaries.puml diff --git a/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt b/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt index 0155a30..2e03509 100644 --- a/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt +++ b/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ComponentViewExporter.kt @@ -39,8 +39,7 @@ class ComponentViewExporter( val containers: List = getBoundaryContainer(view) for (container in containers) { - val showContainerBoundary = - container == view.container && view.externalContainerBoundariesVisible + val showContainerBoundary = view.externalContainerBoundariesVisible if (showContainerBoundary) { boundaryWriter.startContainerBoundary(container, writer, view) } diff --git a/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ContainerViewExporter.kt b/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ContainerViewExporter.kt index 8235029..09e7ebc 100644 --- a/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ContainerViewExporter.kt +++ b/src/main/kotlin/com/github/chriskn/structurizrextension/export/exporter/ContainerViewExporter.kt @@ -39,8 +39,7 @@ class ContainerViewExporter( val softwareSystems: List = getBoundarySoftwareSystems(view) for (softwareSystem in softwareSystems) { - val showSoftwareSystemBoundary = - softwareSystem == view.softwareSystem || view.externalSoftwareSystemBoundariesVisible + val showSoftwareSystemBoundary = view.externalSoftwareSystemBoundariesVisible if (showSoftwareSystemBoundary) { boundaryWriter.startSoftwareSystemBoundary(softwareSystem, writer, view) } @@ -63,12 +62,12 @@ class ContainerViewExporter( return createC4Diagram(view, writer.toString()) } - private fun getBoundarySoftwareSystems(view: ModelView): List { - val softwareSystems = view.elements - .map { obj: ElementView -> obj.element } - .filterIsInstance() - .map { it.softwareSystem } - .toSet() - return softwareSystems.sortedBy { it.id } - } + private fun getBoundarySoftwareSystems(view: ModelView): List = view.elements + .asSequence() + .map { obj: ElementView -> obj.element } + .filterIsInstance() + .map { it.softwareSystem } + .toSet() + .sortedBy { it.id } + .toList() } diff --git a/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt b/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt index 289a3f6..30f29a7 100644 --- a/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt +++ b/src/test/kotlin/com/github/chriskn/structurizrextension/ComponentViewTest.kt @@ -120,7 +120,7 @@ class ComponentViewTest { } @Test - fun `component diagram is written without boundary if containers are added`() { + fun `component diagram is written without boundary if containers are added and boundaries not visible`() { val diagramKey = "ComponentWithContainers" val componentView = workspace.views.componentView( backendApplication, diff --git a/src/test/kotlin/com/github/chriskn/structurizrextension/ContainerViewTest.kt b/src/test/kotlin/com/github/chriskn/structurizrextension/ContainerViewTest.kt index 935b829..689a853 100644 --- a/src/test/kotlin/com/github/chriskn/structurizrextension/ContainerViewTest.kt +++ b/src/test/kotlin/com/github/chriskn/structurizrextension/ContainerViewTest.kt @@ -105,9 +105,42 @@ class ContainerViewTest { icon = "android", uses = listOf(Dependency(app, "uses app")) ) + val thirdPartySystem = model.softwareSystem( + name = "Thrid Party System", + description = "External System", + location = Location.External, + usedBy = listOf(Dependency(backendApplication, "uses")) + ) @Test - fun `container diagram is written as expected with external boundary`() { + fun `container diagram is written without boundary if it contains only containers`() { + val diagramKey = "ContainerWithoutBoundary" + val containerView = workspace.views.containerView( + softwareSystem, + diagramKey, + "Test container view", + C4PlantUmlLayout( + legend = Legend.None, + layout = Layout.TopDown, + showPersonOutline = false, + dependencyConfigurations = listOf( + DependencyConfiguration(filter = { it.destination == database }, direction = Direction.Right), + DependencyConfiguration(filter = { it.source == externalSchema }, direction = Direction.Up) + ) + ) + ) + + containerView.addAllContainers() + containerView.add(topic) + containerView.add(internalSchema) + containerView.add(externalSchema) + containerView.addAllPeople() + + assertExpectedDiagramWasWrittenForView(workspace, diagramKey) + } + + @Test + fun `container diagram is written with boundary if system boundaries are visible`() { val diagramKey = "ContainerWithBoundary" val containerView = workspace.views.containerView( softwareSystem, @@ -125,21 +158,22 @@ class ContainerViewTest { ) ) ) + containerView.addAllContainers() - containerView.showExternalSoftwareSystemBoundaries = true containerView.add(topic) containerView.add(internalSchema) containerView.add(externalSchema) - - containerView.addDependentSoftwareSystems() + containerView.addAllSoftwareSystems() containerView.addAllPeople() + containerView.showExternalSoftwareSystemBoundaries = true + assertExpectedDiagramWasWrittenForView(workspace, diagramKey) } @Test - fun `container diagram is written as expected without external boundary`() { - val diagramKey = "ContainerWithoutBoundary" + fun `container diagram is written without boundary if systems are added and boundaries not visible`() { + val diagramKey = "ContainerWithSystems" val containerView = workspace.views.containerView( softwareSystem, diagramKey, @@ -158,10 +192,40 @@ class ContainerViewTest { containerView.add(topic) containerView.add(internalSchema) containerView.add(externalSchema) + containerView.addAllSoftwareSystems() + containerView.addAllPeople() - containerView.addDependentSoftwareSystems() + assertExpectedDiagramWasWrittenForView(workspace, diagramKey) + } + + @Test + fun `container diagram is written with boundary if systems are added and system boundaries are visible`() { + val diagramKey = "ContainerWithSystemsAndBoundaries" + val containerView = workspace.views.containerView( + softwareSystem, + diagramKey, + "Example container view", + C4PlantUmlLayout( + legend = Legend.ShowLegend, + layout = Layout.TopDown, + lineType = LineType.Ortho, + nodeSep = 100, + rankSep = 130, + dependencyConfigurations = listOf( + DependencyConfiguration(filter = { it.destination == database }, direction = Direction.Right), + DependencyConfiguration(filter = { it.destination == topic }, direction = Direction.Up) + ) + ) + ) + containerView.addAllContainers() + containerView.add(topic) + containerView.add(internalSchema) + containerView.add(externalSchema) + containerView.addAllSoftwareSystems() containerView.addAllPeople() + containerView.showExternalSoftwareSystemBoundaries = true + assertExpectedDiagramWasWrittenForView(workspace, diagramKey) } } diff --git a/src/test/resources/expected/ContainerWithBoundary.puml b/src/test/resources/expected/ContainerWithBoundary.puml index d795cc4..c41ab38 100644 --- a/src/test/resources/expected/ContainerWithBoundary.puml +++ b/src/test/resources/expected/ContainerWithBoundary.puml @@ -17,6 +17,7 @@ WithoutPropertyHeader() AddProperty("prop 1", "value 1") Person(Maintainer, "Maintainer", "some employee", "", $link="https://www.google.de") Person_Ext(AndroidUser, "Android User", "some Android user", "android-icon") +System_Ext(ThridPartySystem, "Thrid Party System", "External System", "") System_Boundary(MySoftwareSystem, My Software System) { WithoutPropertyHeader() @@ -25,20 +26,18 @@ System_Boundary(MySoftwareSystem, My Software System) { Container(MySoftwareSystem.App, "App", "Android", "android app", "android-icon") ContainerDb(MySoftwareSystem.Database, "Database", "postgres", "some database", "postgresql") } - System_Boundary(GraphQL, GraphQL) { Container(GraphQL.InternalSchema, "Internal Schema", "", "Schema provided by our app", "") Container_Ext(GraphQL.ExternalSchema, "External Schema", "", "Schema provided by another team", "") } - System_Boundary(Broker, Broker) { ContainerQueue_Ext(Broker.mytopic, "my.topic", "", "external topic", "kafka") } - Rel(AndroidUser, MySoftwareSystem.App, "uses app") Rel(MySoftwareSystem.App, GraphQL.InternalSchema, "reuqest data using", "GraphQL", $sprite=graphql, $link="https://graphql.org/") Rel_R(MySoftwareSystem.BackendApp, MySoftwareSystem.Database, "CRUD", "JPA") Rel(MySoftwareSystem.BackendApp, GraphQL.InternalSchema, "provides subgraph to") +Rel(MySoftwareSystem.BackendApp, ThridPartySystem, "uses") Rel_U(MySoftwareSystem.BackendApp, Broker.mytopic, "reads topic", "Avro", $tags="async relationship") Rel(GraphQL.ExternalSchema, GraphQL.InternalSchema, "extends schema") Rel(Maintainer, MySoftwareSystem.BackendApp, "Admin UI", "REST") diff --git a/src/test/resources/expected/ContainerWithSystems.puml b/src/test/resources/expected/ContainerWithSystems.puml new file mode 100644 index 0000000..8658780 --- /dev/null +++ b/src/test/resources/expected/ContainerWithSystems.puml @@ -0,0 +1,40 @@ +@startuml(id=ContainerWithSystems) +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/android-icon.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/docker-icon.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/graphql.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/kafka.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/postgresql.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +title My Software System - Containers +caption Test container view + +LAYOUT_TOP_DOWN() + +AddRelTag("async relationship", $textColor="$ARROW_COLOR", $lineColor="$ARROW_COLOR", $lineStyle = DashedLine()) + +WithoutPropertyHeader() +AddProperty("prop 1", "value 1") +Person(Maintainer, "Maintainer", "some employee", "", $link="https://www.google.de") +Person_Ext(AndroidUser, "Android User", "some Android user", "android-icon") +System_Ext(ThridPartySystem, "Thrid Party System", "External System", "") + +WithoutPropertyHeader() +AddProperty("prop 1", "value 1") +Container(MySoftwareSystem.BackendApp, "Backend App", "Kotlin", "some backend app", "docker-icon", $link="https://www.google.de") +Container(MySoftwareSystem.App, "App", "Android", "android app", "android-icon") +ContainerDb(MySoftwareSystem.Database, "Database", "postgres", "some database", "postgresql") + +Container(GraphQL.InternalSchema, "Internal Schema", "", "Schema provided by our app", "") +Container_Ext(GraphQL.ExternalSchema, "External Schema", "", "Schema provided by another team", "") + +ContainerQueue_Ext(Broker.mytopic, "my.topic", "", "external topic", "kafka") + +Rel(AndroidUser, MySoftwareSystem.App, "uses app") +Rel(MySoftwareSystem.App, GraphQL.InternalSchema, "reuqest data using", "GraphQL", $sprite=graphql, $link="https://graphql.org/") +Rel_R(MySoftwareSystem.BackendApp, MySoftwareSystem.Database, "CRUD", "JPA") +Rel(MySoftwareSystem.BackendApp, GraphQL.InternalSchema, "provides subgraph to") +Rel(MySoftwareSystem.BackendApp, ThridPartySystem, "uses") +Rel(MySoftwareSystem.BackendApp, Broker.mytopic, "reads topic", "Avro", $tags="async relationship") +Rel_U(GraphQL.ExternalSchema, GraphQL.InternalSchema, "extends schema") +Rel(Maintainer, MySoftwareSystem.BackendApp, "Admin UI", "REST") +@enduml \ No newline at end of file diff --git a/src/test/resources/expected/ContainerWithSystemsAndBoundaries.puml b/src/test/resources/expected/ContainerWithSystemsAndBoundaries.puml new file mode 100644 index 0000000..d3ae881 --- /dev/null +++ b/src/test/resources/expected/ContainerWithSystemsAndBoundaries.puml @@ -0,0 +1,50 @@ +@startuml(id=ContainerWithSystemsAndBoundaries) +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/android-icon.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/docker-icon.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/graphql.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/kafka.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/postgresql.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +title My Software System - Containers +caption Example container view + +SHOW_PERSON_OUTLINE() +LAYOUT_TOP_DOWN() + +AddRelTag("async relationship", $textColor="$ARROW_COLOR", $lineColor="$ARROW_COLOR", $lineStyle = DashedLine()) + +WithoutPropertyHeader() +AddProperty("prop 1", "value 1") +Person(Maintainer, "Maintainer", "some employee", "", $link="https://www.google.de") +Person_Ext(AndroidUser, "Android User", "some Android user", "android-icon") +System_Ext(ThridPartySystem, "Thrid Party System", "External System", "") + +System_Boundary(MySoftwareSystem, My Software System) { + WithoutPropertyHeader() + AddProperty("prop 1", "value 1") + Container(MySoftwareSystem.BackendApp, "Backend App", "Kotlin", "some backend app", "docker-icon", $link="https://www.google.de") + Container(MySoftwareSystem.App, "App", "Android", "android app", "android-icon") + ContainerDb(MySoftwareSystem.Database, "Database", "postgres", "some database", "postgresql") +} +System_Boundary(GraphQL, GraphQL) { + Container(GraphQL.InternalSchema, "Internal Schema", "", "Schema provided by our app", "") + Container_Ext(GraphQL.ExternalSchema, "External Schema", "", "Schema provided by another team", "") +} +System_Boundary(Broker, Broker) { + ContainerQueue_Ext(Broker.mytopic, "my.topic", "", "external topic", "kafka") +} +Rel(AndroidUser, MySoftwareSystem.App, "uses app") +Rel(MySoftwareSystem.App, GraphQL.InternalSchema, "reuqest data using", "GraphQL", $sprite=graphql, $link="https://graphql.org/") +Rel_R(MySoftwareSystem.BackendApp, MySoftwareSystem.Database, "CRUD", "JPA") +Rel(MySoftwareSystem.BackendApp, GraphQL.InternalSchema, "provides subgraph to") +Rel(MySoftwareSystem.BackendApp, ThridPartySystem, "uses") +Rel_U(MySoftwareSystem.BackendApp, Broker.mytopic, "reads topic", "Avro", $tags="async relationship") +Rel(GraphQL.ExternalSchema, GraphQL.InternalSchema, "extends schema") +Rel(Maintainer, MySoftwareSystem.BackendApp, "Admin UI", "REST") + +skinparam linetype ortho +skinparam nodesep 100 +skinparam ranksep 130 +SHOW_LEGEND(true) + +@enduml \ No newline at end of file diff --git a/src/test/resources/expected/ContainerWithoutBoundary.puml b/src/test/resources/expected/ContainerWithoutBoundary.puml index fa44dcb..7477285 100644 --- a/src/test/resources/expected/ContainerWithoutBoundary.puml +++ b/src/test/resources/expected/ContainerWithoutBoundary.puml @@ -17,13 +17,12 @@ AddProperty("prop 1", "value 1") Person(Maintainer, "Maintainer", "some employee", "", $link="https://www.google.de") Person_Ext(AndroidUser, "Android User", "some Android user", "android-icon") -System_Boundary(MySoftwareSystem, My Software System) { - WithoutPropertyHeader() - AddProperty("prop 1", "value 1") - Container(MySoftwareSystem.BackendApp, "Backend App", "Kotlin", "some backend app", "docker-icon", $link="https://www.google.de") - Container(MySoftwareSystem.App, "App", "Android", "android app", "android-icon") - ContainerDb(MySoftwareSystem.Database, "Database", "postgres", "some database", "postgresql") -} +WithoutPropertyHeader() +AddProperty("prop 1", "value 1") +Container(MySoftwareSystem.BackendApp, "Backend App", "Kotlin", "some backend app", "docker-icon", $link="https://www.google.de") +Container(MySoftwareSystem.App, "App", "Android", "android app", "android-icon") +ContainerDb(MySoftwareSystem.Database, "Database", "postgres", "some database", "postgresql") + Container(GraphQL.InternalSchema, "Internal Schema", "", "Schema provided by our app", "") Container_Ext(GraphQL.ExternalSchema, "External Schema", "", "Schema provided by another team", "")