Skip to content

Commit

Permalink
extend image generation
Browse files Browse the repository at this point in the history
  • Loading branch information
slu-it committed Feb 24, 2024
1 parent 1e749d8 commit 3feb268
Show file tree
Hide file tree
Showing 34 changed files with 543 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: image-update
name: detailed-image-update

on:
push:
Expand Down Expand Up @@ -38,7 +38,7 @@ jobs:
run: chmod +x gradlew
- name: "Generate Images"
shell: bash
run: './gradlew --no-daemon generate-images'
run: './gradlew --no-daemon generate-detailed-images'
- name: "Commit Changes"
shell: bash
run: |
Expand Down
65 changes: 51 additions & 14 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import documentation.DetailedDiagramGenerator
import documentation.ImageGenerator.generateDiagramImage
import documentation.OverviewDiagramGenerator
import documentation.model.RootComponent
import documentation.model.loadComponents
import net.sourceforge.plantuml.FileFormat
import net.sourceforge.plantuml.FileFormatOption
import net.sourceforge.plantuml.SourceStringReader
import java.io.FileOutputStream

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("com.fasterxml.jackson.core:jackson-annotations:2.16.1")
classpath("com.fasterxml.jackson.core:jackson-core:2.16.1")
classpath("com.fasterxml.jackson.core:jackson-databind:2.16.1")
classpath("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1")
classpath("net.sourceforge.plantuml:plantuml:1.2024.3")
}
}

tasks.register("generate-images", GenerateImagesTask::class)
tasks.register("generate-overview-image", GenerateOverviewImageTask::class)
tasks.register("generate-detailed-images", GenerateDetailedImagesTask::class)
tasks.register("generate-detailed2-images", GenerateDetailed2ImagesTask::class)

open class GenerateImagesTask : DefaultTask() {
open class GenerateDetailedImagesTask : DefaultTask() {

@TaskAction
fun execute() {
Expand All @@ -27,19 +35,48 @@ open class GenerateImagesTask : DefaultTask() {
private fun generateImages(sourceFolder: File, targetFolder: File) {
check(sourceFolder.isDirectory)
sourceFolder.listFiles()!!
.filter { file -> file.name.endsWith(".puml") }
.filter { file -> file.extension == "puml" }
.forEach { inputFile ->
generateImage(inputFile, targetFolder, FileFormat.PNG)
generateImage(inputFile, targetFolder, FileFormat.SVG)
generateDiagramImage(inputFile, targetFolder, FileFormat.PNG)
generateDiagramImage(inputFile, targetFolder, FileFormat.SVG)
}
}
}

open class GenerateOverviewImageTask : DefaultTask() {

@TaskAction
fun execute() {
val sourceFolder = File(project.rootDir, "src/descriptions")
val targetFolder = File(project.rootDir, "documentation/overview")

loadComponents(sourceFolder)
.also { generateOverviewDiagram(it, targetFolder) }
}

private fun generateOverviewDiagram(components: List<RootComponent>, targetFolder: File) {
val diagramSource = OverviewDiagramGenerator(components).generate()

generateDiagramImage(diagramSource, targetFolder, "overview", FileFormat.PNG)
generateDiagramImage(diagramSource, targetFolder, "overview", FileFormat.SVG)
}
}

open class GenerateDetailed2ImagesTask : DefaultTask() {

@TaskAction
fun execute() {
val sourceFolder = File(project.rootDir, "src/descriptions")
val targetFolder = File(project.rootDir, "documentation/detailed2")

loadComponents(sourceFolder)
.forEach { generateDetailedDiagram(it, targetFolder) }
}

private fun generateImage(inputFile: File, outputFolder: File, format: FileFormat) {
val outputFile = File(outputFolder, inputFile.nameWithoutExtension + format.fileSuffix)
private fun generateDetailedDiagram(component: RootComponent, targetFolder: File) {
val diagramSource = DetailedDiagramGenerator(component).generate()

logger.info("Generating diagram form: $inputFile")
val fileFormatOption = FileFormatOption(format, false)
SourceStringReader(inputFile.readText()).outputImage(FileOutputStream(outputFile, false), fileFormatOption)
logger.info("Diagram successfully created: $outputFile")
generateDiagramImage(diagramSource, targetFolder, component.id, FileFormat.PNG)
generateDiagramImage(diagramSource, targetFolder, component.id, FileFormat.SVG)
}
}
15 changes: 15 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
kotlin("jvm") version "1.9.22"
}

repositories {
mavenCentral()
}

dependencies {
implementation("com.fasterxml.jackson.core:jackson-annotations:2.16.1")
implementation("com.fasterxml.jackson.core:jackson-core:2.16.1")
implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1")
implementation("net.sourceforge.plantuml:plantuml:1.2024.3")
}
124 changes: 124 additions & 0 deletions buildSrc/src/main/kotlin/documentation/DetailedDiagramGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package documentation

import documentation.model.Component
import documentation.model.Component.Relationship.CLOSE
import documentation.model.Component.Relationship.EXTERNAL
import documentation.model.Component.Relationship.OWNED
import documentation.model.Component.Type.BACKEND
import documentation.model.Component.Type.DATABASE
import documentation.model.Component.Type.FRONTEND
import documentation.model.Dependent
import documentation.model.DiagramComponent
import documentation.model.DiagramRelationship
import documentation.model.RootComponent

class DetailedDiagramGenerator(private val rootComponent: RootComponent) {

private val components: List<Component>
private val incomingRelationships: List<DiagramRelationship>
private val outgoingRelationships: List<DiagramRelationship>

private val systemAndContextMap: Map<SystemAndContextId, List<String>>

init {
components = rootComponent.dependents + rootComponent + rootComponent.dependencies

incomingRelationships = rootComponent.dependents
.map { source -> diagramRelationship(source, rootComponent) }
outgoingRelationships = rootComponent.dependencies
.map { target -> diagramRelationship(rootComponent, target) }

systemAndContextMap = components
.groupBy(::systemAndContextId, ::diagramComponentId)
}

fun generate(): String =
buildString {
appendLine("@startuml")
appendLine("'https://plantuml.com/deployment-diagram")
appendLine()
systemAndContextMap.keys.forEach { (systemId, contextId) ->
if (systemId != null) appendLine("""folder "${systemName(systemId)}" {""")
if (contextId != null) appendLine("""frame "${contextName(contextId)}" {""")

components
.filter { it.systemId == systemId && it.contextId == contextId }
.map(::diagramComponent)
.forEach { appendLine(it) }

if (contextId != null) appendLine("}")
if (systemId != null) appendLine("}")
}
appendLine()
incomingRelationships.forEach { appendLine(it) }
outgoingRelationships.forEach { appendLine(it) }
appendLine()
appendLine("@enduml")
}

// common functions

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

private fun StringBuilder.appendLine(relationship: DiagramRelationship) =
with(relationship) {
appendLine("""$source $link $target""")
}

private fun diagramComponent(description: Component) =
DiagramComponent(
id = diagramComponentId(description),
type = type(description),
name = name(description),
style = style(description)
)

private fun systemAndContextId(it: Component): SystemAndContextId =
SystemAndContextId(it.systemId, it.contextId)

private fun diagramComponentId(component: Component): String =
component.id.replace('-', '_').lowercase()

private fun type(component: Component): String {
return when (component.type) {
BACKEND, FRONTEND -> "rectangle"
DATABASE -> "database"
else -> "circle"
}
}

private fun name(component: Component): String =
componentName(component.id)

private fun style(component: Component): String {
if (component is RootComponent) return "#skyblue;line.bold"
if (component is Dependent) return "#lightgrey"
if (component.type == DATABASE) return ""

return when (component.relationship) {
OWNED -> "#lightblue"
CLOSE -> "#moccasin"
EXTERNAL -> "#lightcoral"
else -> ""
}
}

private fun diagramRelationship(source: Component, target: Component): DiagramRelationship =
DiagramRelationship(
source = diagramComponentId(source),
target = diagramComponentId(target),
link = when (target.type) {
DATABASE -> "-d->"
else -> when (target.relationship) {
OWNED -> "-r->"
CLOSE -> "-d->"
else -> "-->"
}
}
)

data class SystemAndContextId(val systemId: String?, val contextId: String?)
}
27 changes: 27 additions & 0 deletions buildSrc/src/main/kotlin/documentation/ImageGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package documentation

import net.sourceforge.plantuml.FileFormat
import net.sourceforge.plantuml.FileFormatOption
import net.sourceforge.plantuml.SourceStringReader
import java.io.File
import java.io.FileOutputStream

object ImageGenerator {

fun generateDiagramImage(inputFile: File, outputFolder: File, format: FileFormat) {
val diagramSource = inputFile.readText()
val fileName = inputFile.nameWithoutExtension
generateDiagramImage(diagramSource, outputFolder, fileName, format)
}

fun generateDiagramImage(diagramSource: String, outputFolder: File, fileName: String, format: FileFormat) {
val outputFile = File(outputFolder, fileName + format.fileSuffix)
val fileFormatOption = FileFormatOption(format, false)
val reader = SourceStringReader(diagramSource)

outputFolder.mkdirs()

FileOutputStream(outputFile, false)
.use { reader.outputImage(it, fileFormatOption) }
}
}
106 changes: 106 additions & 0 deletions buildSrc/src/main/kotlin/documentation/OverviewDiagramGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package documentation

import documentation.model.Component
import documentation.model.Component.Relationship.CLOSE
import documentation.model.Component.Relationship.EXTERNAL
import documentation.model.Component.Relationship.OWNED
import documentation.model.Component.Type.BACKEND
import documentation.model.Component.Type.DATABASE
import documentation.model.Component.Type.FRONTEND
import documentation.model.DiagramComponent
import documentation.model.DiagramRelationship
import documentation.model.RootComponent

class OverviewDiagramGenerator(rootComponents: List<RootComponent>) {

private val relevantTypes = setOf(BACKEND, FRONTEND)

private val rootComponentIds = rootComponents.map { it.id }.toSet()
private val diagramRootComponents = rootComponents
.map { diagramComponent(it) }
private val additionalDiagramComponents = rootComponents
.asSequence()
.flatMap { it.dependencies }
.filter { it.id !in rootComponentIds }
.filter { it.type in relevantTypes }
.distinctBy { it.id }
.map { diagramComponent(it) }
.toList()
private val diagramRelationships = rootComponents
.flatMap { source ->
source.dependencies
.filter { it.type in relevantTypes }
.map { target -> source to target }
}
.map { (source, target) -> diagramRelationship(source, target) }

fun generate(): String =
buildString {
appendLine("@startuml")
appendLine("'https://plantuml.com/deployment-diagram")
appendLine()
appendLine("left to right direction")
appendLine()
diagramRootComponents.forEach { appendLine(it) }
additionalDiagramComponents.forEach { appendLine(it) }
appendLine()
diagramRelationships.forEach { appendLine(it) }
appendLine()
appendLine("@enduml")
}

// common functions

private fun StringBuilder.appendLine(component: DiagramComponent) =
with(component) {
appendLine("""$type "$name" as $id ${if (style != null) "#$style" else ""}""")
}

private fun StringBuilder.appendLine(relationship: DiagramRelationship) =
with(relationship) {
appendLine("""$source $link $target""")
}

private fun diagramComponent(description: Component) =
DiagramComponent(
id = id(description),
type = type(description),
name = name(description),
style = color(description)
)

private fun id(description: Component): String =
description.id.replace('-', '_').lowercase()

private fun type(description: Component): String {
return when (description.type) {
BACKEND, FRONTEND -> "rectangle"
DATABASE -> "database"
else -> "circle"
}
}

private fun name(description: Component): String =
description.id

private fun color(description: Component): String? =
when (description.type) {
DATABASE -> null
else -> when (description.relationship) {
OWNED -> "lightblue"
CLOSE -> "moccasin"
EXTERNAL -> "lightcoral"
else -> null
}
}

private fun diagramRelationship(source: Component, target: Component): DiagramRelationship {
val sourceComponent = diagramComponent(source)
val targetComponent = diagramComponent(target)
return DiagramRelationship(
source = sourceComponent.id,
target = targetComponent.id,
link = "-->"
)
}
}
Loading

0 comments on commit 3feb268

Please sign in to comment.