Skip to content

Commit

Permalink
generate messaging diagram
Browse files Browse the repository at this point in the history
  • Loading branch information
slu-it committed Aug 30, 2024
1 parent aa3e70c commit d56e620
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 15 deletions.
6 changes: 4 additions & 2 deletions .build/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import documentation.generateComponentDiagramsFromJson
import documentation.generateDiagramsFromPlantUml
import documentation.generateEndpointOverviewDocumentFromJson
import documentation.generateEventsOverviewDocumentFromJson
import documentation.generateMessagingDiagramsFromJson
import documentation.generateOverviewDiagramsFromJson
import documentation.tasks.generateComponentDescription

tasks.register("generateFiles") {
val srcFolder = File(project.rootDir, "src")
val rootFolder = project.rootDir.parentFile
doLast {
generateDiagramsFromPlantUml(srcFolder, rootFolder)
generateComponentDiagramsFromJson(srcFolder, rootFolder)
generateOverviewDiagramsFromJson(srcFolder, rootFolder)
generateDiagramsFromPlantUml(srcFolder, rootFolder)
generateEndpointOverviewDocumentFromJson(srcFolder, rootFolder)
generateEventsOverviewDocumentFromJson(srcFolder, rootFolder)
generateMessagingDiagramsFromJson(srcFolder, rootFolder)
generateOverviewDiagramsFromJson(srcFolder, rootFolder)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ abstract class AbstractDiagramGenerator(

protected fun StringBuilder.appendComponentLine(component: DiagramComponent) =
with(component) {
appendLine("""$type "$name" as $id $style""")
appendLine("""$type "$name" as $id ${style ?: ""}""")
}

protected fun StringBuilder.appendRelationshipLine(relationship: DiagramRelationship) =
Expand Down Expand Up @@ -75,18 +75,23 @@ abstract class AbstractDiagramGenerator(

// CONVERTER

protected fun diagramComponent(component: Component) =
protected fun diagramComponent(component: Component, qualifier: String? = null) =
DiagramComponent(
id = diagramComponentId(component),
id = diagramComponentId(component, qualifier),
type = type(component),
name = componentName(component.id),
style = style(component)
)

protected fun diagramRelationship(source: Component, target: Component): DiagramRelationship =
protected fun diagramRelationship(
source: Component,
target: Component,
sourceQualifier: String? = null,
targetQualifier: String? = null
): DiagramRelationship =
DiagramRelationship(
source = diagramComponentId(source),
target = diagramComponentId(target),
source = diagramComponentId(source, sourceQualifier),
target = diagramComponentId(target, targetQualifier),
link = link(target),
label = linkLabel(source, target),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package documentation.generators.plantuml

import documentation.model.Application
import documentation.model.ComponentType
import documentation.model.ComponentType.BACKEND
import documentation.model.Messaging.Binding
import documentation.model.Messaging.ConsumedQueue
import documentation.model.Messaging.PublishedMessage
import documentation.model.componentName

class MessagingDiagramGenerator(
private val applications: List<Application>,
private val options: Options = Options(),
) : AbstractDiagramGenerator(options) {

data class Options(
override val lineType: LineType = LineType.DEFAULT,
override val includedComponentTypes: Set<ComponentType> = setOf(BACKEND),
override val includeCredentials: Boolean = false,
) : DiagramGeneratorOptions

private val publishingComponents: List<DiagramComponent>
private val publishingNotes: List<DiagramNote>
private val exchanges: List<DiagramComponent>
private val queues: List<DiagramComponent>
private val consumingComponents: List<DiagramComponent>
private val relationships: List<DiagramRelationship>

init {
publishingComponents = publishingApplications()
.map { diagramComponent(it, "producer") }
publishingNotes = publishingApplications()
.map(::publishingNote)
exchanges = getAllExchangeNames()
.map(::diagramExchangeComponent)
queues = getAllQueueNames()
.map(::diagramQueueComponent)
consumingComponents = consumingApplications()
.map { diagramComponent(it, "consumer") }

relationships = buildList {
publishingApplications()
.forEach { application ->
application.messaging.publishedMessages
.map { (exchange) ->
publisherToExchangeRelationship(application, exchange)
}
.forEach(::add)
}
consumingApplications()
.forEach { application ->
application.messaging.consumedQueues
.forEach { (name, bindings) ->
bindings.forEach { (exchange, routingKeyPattern) ->
add(exchangeToQueueRelationship(exchange, name, routingKeyPattern))
}
add(queueToConsumerRelationship(name, application))
}
}
}
}

override fun plantUmlSource(): String =
buildString {
appendLine("@startuml")
appendLine("'https://plantuml.com/deployment-diagram")
appendLine()
appendLine("left to right direction")
appendLine()
appendLine(options.lineType)
appendLine()
publishingComponents.forEach { appendComponentLine(it) }
appendLine("""frame "Message Broker" {""")
exchanges.forEach { appendComponentLine(it) }
queues.forEach { appendComponentLine(it) }
appendLine("}")
consumingComponents.forEach { appendComponentLine(it) }
appendLine()
publishingNotes.forEach { appendNote(it) }
appendLine()
relationships.forEach { appendRelationshipLine(it) }
appendLine()
appendLine("@enduml")
}

private fun diagramExchangeComponent(it: String) =
DiagramComponent(id = diagramExchangeId(it), type = "boundary", name = it, style = null)

private fun diagramQueueComponent(it: String) =
DiagramComponent(id = diagramQueueId(it), type = "queue", name = it, style = null)

private fun publisherToExchangeRelationship(publisher: Application, exchange: String) =
DiagramRelationship(
source = diagramComponentId(publisher, "producer"),
target = diagramExchangeId(exchange),
link = "-->",
)

private fun exchangeToQueueRelationship(exchange: String, queue: String, routingKeyPattern: String) =
DiagramRelationship(
source = diagramExchangeId(exchange),
target = diagramQueueId(queue),
link = "-->",
label = routingKeyPattern
)

private fun queueToConsumerRelationship(queue: String, consumer: Application) =
DiagramRelationship(
source = diagramQueueId(queue),
target = diagramComponentId(consumer, "consumer"),
link = "-->",
)

private fun publishingNote(application: Application) = DiagramNote(
target = diagramComponentId(application, "producer"),
text = publishingNoteText(application).trim(),
position = "left"
)

private fun publishingNoteText(application: Application) =
buildString {
application.messaging.publishedMessages
.forEach { (exchange, routingKeys) ->
appendLine("**$exchange**:")
routingKeys.forEach { routingKey ->
appendLine(" - $routingKey")
}
appendLine()
}
}

private fun getAllExchangeNames(): List<String> =
(getPublishingExchangeNames() + getConsumingExchangeNames()).distinct()

private fun getPublishingExchangeNames(): List<String> =
applications
.flatMap { application -> application.messaging.publishedMessages.map(PublishedMessage::exchange) }
.distinct()

private fun getConsumingExchangeNames(): List<String> =
applications
.flatMap { application -> application.messaging.consumedQueues.flatMap { it.bindings.map(Binding::exchange) } }
.distinct()

private fun getAllQueueNames(): List<String> =
applications
.flatMap { application -> application.messaging.consumedQueues.map(ConsumedQueue::name) }
.distinct()

private fun publishingApplications() = applications
.filter { it.messaging.publishedMessages.isNotEmpty() }
.sortedBy { componentName(it.id) }

private fun consumingApplications() = applications
.filter { it.messaging.consumedQueues.isNotEmpty() }
.sortedBy { componentName(it.id) }

private fun diagramExchangeId(it: String) = "exchange__${normalizeIdPart(it)}"
private fun diagramQueueId(it: String) = "queue__${normalizeIdPart(it)}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ data class DiagramComponent(
val id: String,
val type: String,
val name: String,
val style: String?,
val style: String? = null,
)

data class DiagramRelationship(
val source: String,
val target: String,
val link: String,
val label: String?,
val label: String? = null,
)

data class DiagramNote(
Expand All @@ -23,10 +23,10 @@ data class DiagramNote(
val position: String = "right",
) {
companion object {
fun httpEndpoints(dependency: Dependency): DiagramNote {
fun httpEndpoints(dependency: Dependency, qualifier: String? = null): DiagramNote {
val methodLength = dependency.httpEndpoints.maxOfOrNull { it.method.length } ?: 0
return DiagramNote(
target = diagramComponentId(dependency),
target = diagramComponentId(dependency, qualifier),
text = dependency.httpEndpoints
.joinToString(prefix = "**HTTP Endpoints:**\n", separator = "\n") { endpoint ->
"\"\"${endpoint.method.padEnd(methodLength, ' ')} ${endpoint.path}\"\""
Expand Down Expand Up @@ -57,11 +57,11 @@ enum class LineType(private val value: String) {

private val invalidIdCharacters = Regex("[^0-9a-z_]")

fun diagramComponentId(component: Component): String =
listOfNotNull(component.systemId, component.groupId, component.id)
fun diagramComponentId(component: Component, qualifier: String?): String =
listOfNotNull(component.systemId, component.groupId, component.id, qualifier)
.joinToString(separator = "__", transform = ::normalizeIdPart)

private fun normalizeIdPart(value: String): String =
fun normalizeIdPart(value: String): String =
value.lowercase()
.replace('-', '_')
.replace(invalidIdCharacters, "")
16 changes: 16 additions & 0 deletions .build/buildSrc/src/main/kotlin/documentation/tasks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import documentation.generators.plantuml.LineType
import documentation.generators.plantuml.LineType.DEFAULT
import documentation.generators.plantuml.LineType.ORTHOGONAL
import documentation.generators.plantuml.LineType.POLY
import documentation.generators.plantuml.MessagingDiagramGenerator
import documentation.generators.plantuml.MultipleApplicationsDiagramGenerator
import documentation.generators.plantuml.PlantUmlDiagramGenerator.generateDiagramAndSaveAsImage
import documentation.model.Application
Expand Down Expand Up @@ -181,6 +182,21 @@ private fun generateApplicationsOverviewDiagram(targetFolder: File, generator: D
generateDiagramAndSaveAsImage(diagramSource, targetFolder, "overview", FileFormat.SVG)
}

// MESSAGING DIAGRAMS

fun generateMessagingDiagramsFromJson(srcFolder: File, rootFolder: File) {
val sourcesFolder = File(srcFolder, "json/components")
val targetFolder = File(rootFolder, "diagrams/messaging")

val applications = loadApplications(sourcesFolder)

val generator = MessagingDiagramGenerator(applications)
val diagramSource = generator.plantUmlSource()

generateDiagramAndSaveAsImage(diagramSource, targetFolder, "messaging", FileFormat.PNG)
generateDiagramAndSaveAsImage(diagramSource, targetFolder, "messaging", FileFormat.SVG)
}

// USED ENDPOINTS

fun generateEndpointOverviewDocumentFromJson(srcFolder: File, rootFolder: File) {
Expand Down

0 comments on commit d56e620

Please sign in to comment.