Skip to content

Commit

Permalink
scope relationship configurations properly
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskn committed Jun 21, 2024
1 parent a93df91 commit 5f647a6
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RelationshipView, List<DependencyConfiguration>> = 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<DependencyConfiguration>,
writer: IndentingWriter,
) {
if (view is DynamicView && view.renderAsSequenceDiagram) {
writeRelationshipSequenceDiagram(view, relationshipView, writer)
return
Expand All @@ -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""""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -155,48 +151,37 @@ internal class RelationshipWriter(
relationshipView.relationship.description
}.ifEmpty { " " }

private fun determineMode(
view: ModelView,
configurations: List<DependencyConfiguration>,
): 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<DependencyConfiguration>,
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
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {

Expand Down Expand Up @@ -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,
Expand All @@ -144,12 +141,7 @@ class DynamicViewTest {

configureWithNestedParallelNumbering(dynamicView)

assertThrows<IllegalArgumentException> {
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
Expand Down
36 changes: 36 additions & 0 deletions src/test/resources/expected/view/dynamic/DynamicView.puml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 5f647a6

Please sign in to comment.