diff --git a/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/ExtendedC4PlantUMLExporter.kt b/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/ExtendedC4PlantUMLExporter.kt index 456e6e5..0f42531 100644 --- a/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/ExtendedC4PlantUMLExporter.kt +++ b/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/ExtendedC4PlantUMLExporter.kt @@ -1,5 +1,6 @@ package com.github.chriskn.structurizrextension.internal.export +import com.github.chriskn.structurizrextension.api.view.layout.LayoutRegistry import com.github.chriskn.structurizrextension.internal.export.view.ComponentViewExporter import com.github.chriskn.structurizrextension.internal.export.view.ContainerViewExporter import com.github.chriskn.structurizrextension.internal.export.view.DeploymentViewExporter @@ -56,7 +57,17 @@ internal class ExtendedC4PlantUMLExporter : AbstractDiagramExporter() { } override fun writeRelationship(view: ModelView, relationshipView: RelationshipView, writer: IndentingWriter) { - relationshipWriter.writeRelationship(view, relationshipView, writer) + val configurationsForRelationship = LayoutRegistry + .layoutForKey(view.key) + .dependencyConfigurations.filter { + it.filter(relationshipView.relationship) + } + relationshipWriter.writeRelationship( + view, + relationshipView, + configurationsForRelationship, + writer + ) } public override fun writeFooter(view: ModelView, writer: IndentingWriter) { @@ -96,7 +107,7 @@ internal class ExtendedC4PlantUMLExporter : AbstractDiagramExporter() { public override fun startSoftwareSystemBoundary( view: ModelView, softwareSystem: SoftwareSystem, - writer: IndentingWriter + writer: IndentingWriter, ) { boundaryWriter.startSoftwareSystemBoundary(view, softwareSystem, writer) } @@ -116,7 +127,7 @@ internal class ExtendedC4PlantUMLExporter : AbstractDiagramExporter() { public override fun startDeploymentNodeBoundary( view: DeploymentView, deploymentNode: DeploymentNode, - writer: IndentingWriter + writer: IndentingWriter, ) { boundaryWriter.startDeploymentNodeBoundary(view, deploymentNode, writer) } diff --git a/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/writer/RelationshipWriter.kt b/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/writer/RelationshipWriter.kt index f4c01ba..03b91b7 100644 --- a/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/writer/RelationshipWriter.kt +++ b/src/main/kotlin/com/github/chriskn/structurizrextension/internal/export/writer/RelationshipWriter.kt @@ -4,57 +4,59 @@ import com.github.chriskn.structurizrextension.api.model.icon import com.github.chriskn.structurizrextension.api.model.link import com.github.chriskn.structurizrextension.api.view.dynamic.renderAsSequenceDiagram import com.github.chriskn.structurizrextension.api.view.layout.DependencyConfiguration -import com.github.chriskn.structurizrextension.api.view.layout.Direction import com.github.chriskn.structurizrextension.api.view.layout.LayoutRegistry import com.github.chriskn.structurizrextension.api.view.layout.Mode import com.github.chriskn.structurizrextension.internal.export.idOf import com.github.chriskn.structurizrextension.internal.icons.IconRegistry import com.structurizr.export.IndentingWriter import com.structurizr.model.InteractionStyle -import com.structurizr.model.Relationship -import com.structurizr.model.removeProperty import com.structurizr.view.DynamicView import com.structurizr.view.ModelView import com.structurizr.view.RelationshipView -import com.structurizr.view.View private const val ASYNC_REL_TAG_NAME = "async relationship" -private const val C4_LAYOUT_DIRECTION = "c4:layout:direction" -private const val C4_LAYOUT_MODE = "c4:layout:mode" internal class RelationshipWriter( - private val propertyWriter: PropertyWriter + private val propertyWriter: PropertyWriter, ) { fun writeRelationships(view: ModelView, writer: IndentingWriter) { - // Relationship properties are bound to the ModelItem but c4 properties should only apply for Views. - // Thus, make sure only the configuration for this view is applied. - // In other words: Scope layout settings for Views instead for ModelItems - view.relationships.forEach { rel -> - rel.relationship.removeProperty(C4_LAYOUT_MODE) - rel.relationship.removeProperty(C4_LAYOUT_DIRECTION) - } - val dependencyConfigurations = LayoutRegistry.layoutForKey(view.key).dependencyConfigurations - dependencyConfigurations.forEach { conf -> - view.relationships - .filter { conf.filter(it.relationship) } - .map { relationshipView -> relationshipView.apply(conf, view) } - } + val configurationsByRelationship: Map> = view + .relationships + .associateWith { relView -> + dependencyConfigurations.filter { depConf -> + depConf.filter( + relView.relationship + ) + } + } + val sorted = if (view is DynamicView) { view.relationships.sortedBy { rv: RelationshipView -> rv.order } } else { - view.relationships - .sortedBy { rv: RelationshipView -> - rv.relationship.source.name + rv.relationship.destination.name - } + view.relationships.sortedBy { rv: RelationshipView -> + rv.relationship.source.name + rv.relationship.destination.name + } + } + sorted.forEach { rv: RelationshipView -> + writeRelationship( + view, + rv, + configurationsByRelationship.getOrDefault(rv, emptyList()), + writer + ) } - sorted.forEach { rv: RelationshipView -> writeRelationship(view, rv, writer) } } - internal fun writeRelationship(view: ModelView, relationshipView: RelationshipView, writer: IndentingWriter) { + internal fun writeRelationship( + view: ModelView, + relationshipView: RelationshipView, + configurations: List, + writer: IndentingWriter, + ) { if (view is DynamicView && view.renderAsSequenceDiagram) { writeRelationshipSequenceDiagram(view, relationshipView, writer) return @@ -66,11 +68,11 @@ internal class RelationshipWriter( source = relationship.destination destination = relationship.source } - val relationshipBuilder = StringBuilder() - val mode = determineMode(relationship, view) - val relationshipType = determineType(mode, relationshipView) val description = determineDescription(view, relationshipView) + val mode = determineMode(view, configurations) + val relationshipType = determineType(mode, configurations, relationshipView) + val relationshipBuilder = StringBuilder() if (view is DynamicView) { relationshipBuilder.append( """$relationshipType(${relationshipView.order},${idOf(source)}, ${idOf(destination)}, "$description"""" @@ -99,7 +101,11 @@ internal class RelationshipWriter( writer.writeLine(relationshipBuilder.toString()) } - private fun writeRelationshipSequenceDiagram(view: ModelView, relationshipView: RelationshipView, writer: IndentingWriter) { + private fun writeRelationshipSequenceDiagram( + view: ModelView, + relationshipView: RelationshipView, + writer: IndentingWriter, + ) { // Rel($from, $to, $label, $techn="", $descr="", $sprite="", $tags="", $link="", $index="", $rel="" val relationship = relationshipView.relationship var source = relationship.source @@ -136,16 +142,6 @@ internal class RelationshipWriter( writer.writeLine(relationshipBuilder.toString()) } - private fun determineMode( - relationship: Relationship, - view: ModelView - ): Mode = when { - relationship.properties.containsKey(C4_LAYOUT_MODE) -> Mode.valueOf(relationship.properties[C4_LAYOUT_MODE]!!) - // sequence diagrams need no order - view is DynamicView && !view.renderAsSequenceDiagram -> Mode.RelIndex - else -> Mode.Rel - } - private fun determineDescription( view: ModelView, relationshipView: RelationshipView, @@ -155,48 +151,37 @@ internal class RelationshipWriter( relationshipView.relationship.description }.ifEmpty { " " } + private fun determineMode( + view: ModelView, + configurations: List, + ): Mode = if (view is DynamicView && !view.renderAsSequenceDiagram) { + // Dynamic views use only indexed relationships + Mode.RelIndex + } else { + configurations.map { it.mode }.lastOrNull() ?: Mode.Rel + } + private fun determineType( mode: Mode, + configurations: List, relationshipView: RelationshipView, - ): String = when (mode) { - Mode.Rel, Mode.RelIndex -> { - if (relationshipView.relationship.properties.containsKey(C4_LAYOUT_DIRECTION)) { - val direction = determineDirection(relationshipView) - "${mode.macro}_${direction.macro()}" - } else { - mode.macro + ): String { + val configuredDirection = configurations.map { it.direction }.lastOrNull() + return when (mode) { + Mode.Rel, Mode.RelIndex -> { + if (configuredDirection != null) { + val direction = if (relationshipView.isResponse == true) { + configuredDirection.inverse() + } else { + configuredDirection + } + "${mode.macro}_${direction.macro()}" + } else { + mode.macro + } } - } - else -> "Rel_${mode.macro}" - } - private fun determineDirection( - relationshipView: RelationshipView, - ): Direction { - val direction = Direction.valueOf(relationshipView.relationship.properties[C4_LAYOUT_DIRECTION]!!) - if (relationshipView.isResponse == true) { - return direction.inverse() - } - return direction - } - - private fun RelationshipView.apply( - conf: DependencyConfiguration, - view: View - ): Relationship { - val rel = this.relationship - val direction = conf.direction - val mode = conf.mode - - if (direction != null) { - rel.addProperty(C4_LAYOUT_DIRECTION, direction.name) - } - if (mode != null) { - require(view !is DynamicView) { - "Setting the dependency mode is not supported fot dynamic views" - } - rel.addProperty(C4_LAYOUT_MODE, mode.name) + else -> "Rel_${mode.macro}" } - return rel } } diff --git a/src/main/kotlin/com/structurizr/model/RelationshipExtension.kt b/src/main/kotlin/com/structurizr/model/RelationshipExtension.kt deleted file mode 100644 index eeb6f2d..0000000 --- a/src/main/kotlin/com/structurizr/model/RelationshipExtension.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.structurizr.model - -internal fun Relationship.removeProperty(key: String) { - this.properties = this.properties.filterKeys { it != key } -} diff --git a/src/test/kotlin/com/github/chriskn/structurizrextension/view/DynamicViewTest.kt b/src/test/kotlin/com/github/chriskn/structurizrextension/view/DynamicViewTest.kt index a5d0056..5265956 100644 --- a/src/test/kotlin/com/github/chriskn/structurizrextension/view/DynamicViewTest.kt +++ b/src/test/kotlin/com/github/chriskn/structurizrextension/view/DynamicViewTest.kt @@ -17,7 +17,6 @@ import com.github.chriskn.structurizrextension.api.view.layout.Direction.Left import com.github.chriskn.structurizrextension.api.view.layout.Direction.Right import com.github.chriskn.structurizrextension.api.view.layout.Mode.Neighbor import com.github.chriskn.structurizrextension.api.view.showExternalBoundaries -import com.github.chriskn.structurizrextension.api.writeDiagrams import com.github.chriskn.structurizrextension.assertExpectedDiagramWasWrittenForView import com.structurizr.Workspace import com.structurizr.model.InteractionStyle.Asynchronous @@ -26,10 +25,8 @@ import com.structurizr.view.DynamicView import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import java.io.File class DynamicViewTest { @@ -126,8 +123,8 @@ class DynamicViewTest { } @Test - fun `setting the mode for dependencies in dynamic views throws exception`() { - val diagramKey = "view_with_mode" + fun `setting the mode for dependencies in dynamic views is ignored`() { + val diagramKey = "DynamicView" val dynamicView: DynamicView = workspace.views.dynamicView( customerInformationSystem, diagramKey, @@ -144,12 +141,7 @@ class DynamicViewTest { configureWithNestedParallelNumbering(dynamicView) - assertThrows { - workspace.writeDiagrams(File("")) - } - - // clean up: remove view to avoid exceptions while diagram is written in other test cases of this class - workspace.views.clear() + assertExpectedDiagramWasWrittenForView(workspace, pathToExpectedDiagrams, diagramKey) } @Test diff --git a/src/test/resources/expected/view/dynamic/DynamicView.puml b/src/test/resources/expected/view/dynamic/DynamicView.puml new file mode 100644 index 0000000..b71c5e6 --- /dev/null +++ b/src/test/resources/expected/view/dynamic/DynamicView.puml @@ -0,0 +1,36 @@ +@startuml(id=DynamicView) +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/gilbarbara-plantuml-sprites/master/sprites/mysql.puml +!includeurl https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Dynamic.puml +title Customer Information System - Dynamic +caption desc + +SHOW_PERSON_OUTLINE() +LAYOUT_TOP_DOWN() + +AddRelTag("async relationship", $textColor="$ARROW_COLOR", $lineColor="$ARROW_COLOR", $lineStyle = DashedLine()) + +Person(Customer, "Customer", "A costumer", "") +Container(CustomerInformationSystem.CustomerFrontendApplication, "Customer Frontend Application", "Angular", "Allows customers to manage their profile", "") +Container(CustomerInformationSystem.CustomerService, "Customer Service", "Java and Spring Boot", "The point of access for customer information.", "") +ContainerDb(CustomerInformationSystem.CustomerDatabase, "Customer Database", "Oracle", "Stores customer information", "") +ContainerQueue(CustomerInformationSystem.MessageBus, "Message Bus", "RabbitMQ", "Transport for business events.", "") +Container(CustomerInformationSystem.ReportingService, "Reporting Service", "Ruby", "Creates normalised data for reporting purposes.", "") +ContainerDb(CustomerInformationSystem.ReportingDatabase, "Reporting Database", "MySql", "Stores a normalised version of all business data for ad hoc reporting purposes", "") +Container(CustomerInformationSystem.AuditingService, "Auditing Service", "C#, Net", "Provides organisation-wide auditing facilities.", "") +ContainerDb(CustomerInformationSystem.AuditStore, "Audit Store", "Event Store", "Stores information about events that have happened", "") +RelIndex(1,Customer, CustomerInformationSystem.CustomerFrontendApplication, "Uses") +RelIndex(2,CustomerInformationSystem.CustomerFrontendApplication, CustomerInformationSystem.CustomerService, "Updates customer information using", "JSON/HTTPS") +RelIndex(3,CustomerInformationSystem.CustomerService, CustomerInformationSystem.CustomerDatabase, "Stores data in", "JDBC", $link="www.google.com") +SetPropertyHeader("field", "value") +AddProperty("Event Type", "create") +RelIndex(4,CustomerInformationSystem.CustomerService, CustomerInformationSystem.MessageBus, "Sends customer update events to", $tags="async relationship") +RelIndex(5.1,CustomerInformationSystem.MessageBus, CustomerInformationSystem.ReportingService, "Sends customer update events to", $tags="async relationship") +RelIndex(5.1.1,CustomerInformationSystem.ReportingService, CustomerInformationSystem.ReportingDatabase, "Stores data in", $sprite=mysql) +RelIndex(5.2,CustomerInformationSystem.MessageBus, CustomerInformationSystem.AuditingService, "Sends customer update events to", $tags="async relationship") +RelIndex(5.2.1,CustomerInformationSystem.AuditingService, CustomerInformationSystem.AuditStore, "Stores events in") +RelIndex(5.3,CustomerInformationSystem.CustomerService, CustomerInformationSystem.CustomerFrontendApplication, "Confirms update to", "WebSocket", $link="www.bing.com", $tags="async relationship") +RelIndex(6,CustomerInformationSystem.CustomerFrontendApplication, Customer, "Sends feedback to") + +SHOW_LEGEND(true) + +@enduml \ No newline at end of file