Skip to content

Commit

Permalink
Explicit boundaries for static views (#210)
Browse files Browse the repository at this point in the history
Explicit boundaries for static views
show container boundaries in component diagrams only when componentView.showExternalContainerBoundaries = true
show system boundaries in container diagrams only when containerView.showExternalSoftwareSystemBoundaries = true
  • Loading branch information
chriskn authored Apr 26, 2024
1 parent be5271a commit 33861fc
Show file tree
Hide file tree
Showing 14 changed files with 454 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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<Container> = getBoundaryContainer(view)
for (container in containers) {
val showContainerBoundary = 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<Container> =
view.elements
.asSequence()
.map { obj: ElementView -> obj.element }
.filterIsInstance<Component>()
.map { it.container }
.toSet()
.toList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ class ContainerViewExporter(

val softwareSystems: List<SoftwareSystem> = getBoundarySoftwareSystems(view)
for (softwareSystem in softwareSystems) {
val showSoftwareSystemBoundary =
softwareSystem == view.softwareSystem || view.externalSoftwareSystemBoundariesVisible
val showSoftwareSystemBoundary = view.externalSoftwareSystemBoundariesVisible
if (showSoftwareSystemBoundary) {
boundaryWriter.startSoftwareSystemBoundary(softwareSystem, writer, view)
}
Expand All @@ -63,12 +62,12 @@ class ContainerViewExporter(
return createC4Diagram(view, writer.toString())
}

private fun getBoundarySoftwareSystems(view: ModelView): List<SoftwareSystem> {
val softwareSystems = view.elements
.map { obj: ElementView -> obj.element }
.filterIsInstance<Container>()
.map { it.softwareSystem }
.toSet()
return softwareSystems.sortedBy { it.id }
}
private fun getBoundarySoftwareSystems(view: ModelView): List<SoftwareSystem> = view.elements
.asSequence()
.map { obj: ElementView -> obj.element }
.filterIsInstance<Container>()
.map { it.softwareSystem }
.toSet()
.sortedBy { it.id }
.toList()
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,32 @@ 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
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",
Expand All @@ -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",
Expand All @@ -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 and boundaries not visible`() {
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,
Expand All @@ -89,7 +153,9 @@ class ComponentViewTest {
layout = Layout.LeftToRight
)
)
componentView.addAllElements()
componentView.addAllComponents()
componentView.addAllContainers()
componentView.showExternalContainerBoundaries = true

assertExpectedDiagramWasWrittenForView(workspace, diagramKey)
}
Expand Down
Loading

0 comments on commit 33861fc

Please sign in to comment.