Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Container ADRs in Decision Tab #149

Merged
merged 12 commits into from
May 27, 2023
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 1. Record architecture decisions
# 1. Record Internet Banking System architecture decisions

Date: 2022-06-21

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 1. Record API Application architecture decision records

Date: 2022-06-21

## Status

Accepted

## Context

We need to record the architectural decisions made on this project.

## Decision

We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).

## Consequences

See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
1 change: 1 addition & 0 deletions docs/example/workspace.dsl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea
mobileApp = container "Mobile App" "Provides a limited subset of the Internet banking functionality to customers via their mobile device." "Xamarin" "Mobile App"
webApplication = container "Web Application" "Delivers the static content and the Internet banking single page application." "Java and Spring MVC"
apiApplication = container "API Application" "Provides Internet banking functionality via a JSON/HTTPS API." "Java and Spring MVC" {
!adrs internet-banking-system/api-application/adr
signinController = component "Sign In Controller" "Allows users to sign in to the Internet Banking System." "Spring MVC Rest Controller"
accountsSummaryController = component "Accounts Summary Controller" "Provides customers with a summary of their bank accounts." "Spring MVC Rest Controller"
resetPasswordController = component "Reset Password Controller" "Allows users to reset their passwords with a single use URL." "Spring MVC Rest Controller"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ val SoftwareSystem.includedProperties

fun SoftwareSystem.hasDecisions() = documentation.decisions.isNotEmpty()

fun SoftwareSystem.hasContainerDecisions() = containers.flatMap { it.documentation.decisions }.isNotEmpty()
galuszkak marked this conversation as resolved.
Show resolved Hide resolved

fun SoftwareSystem.hasDocumentationSections() = documentation.sections.size >= 2

fun Container.hasDecisions() = documentation.decisions.isNotEmpty()

fun ViewSet.hasSystemContextViews(softwareSystem: SoftwareSystem) =
systemContextViews.any { it.softwareSystem == softwareSystem }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ private fun generateHtmlFiles(context: GeneratorContext, branchDir: File) {
add { writeHtmlFile(branchDir, SoftwareSystemDecisionPageViewModel(context, it, decision)) }
}

it.containers
.filter { container -> container.documentation.decisions.isNotEmpty() }
.forEach { container ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerDecisionsPageViewModel(context, container)) }
container.documentation.decisions.forEach { decision ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerDecisionPageViewModel(context, container, decision)) }
}
}

it.documentation.sections.filter { section -> section.order != 1 }.forEach { section ->
add { writeHtmlFile(branchDir, SoftwareSystemSectionPageViewModel(context, it, section)) }
}
Expand All @@ -157,6 +166,8 @@ private fun writeHtmlFile(exportDir: File, viewModel: PageViewModel) {
is SoftwareSystemHomePageViewModel -> softwareSystemHomePage(viewModel)
is SoftwareSystemContextPageViewModel -> softwareSystemContextPage(viewModel)
is SoftwareSystemContainerPageViewModel -> softwareSystemContainerPage(viewModel)
is SoftwareSystemContainerDecisionPageViewModel -> softwareSystemContainerDecisionPage(viewModel)
is SoftwareSystemContainerDecisionsPageViewModel -> softwareSystemContainerDecisionsPage(viewModel)
is SoftwareSystemComponentPageViewModel -> softwareSystemComponentPage(viewModel)
is SoftwareSystemDeploymentPageViewModel -> softwareSystemDeploymentPage(viewModel)
is SoftwareSystemDependenciesPageViewModel -> softwareSystemDependenciesPage(viewModel)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.model.Container

fun PageViewModel.createContainerTableViewModel(containers: Collection<Container>, hrefFactory: (Container) -> String) =
galuszkak marked this conversation as resolved.
Show resolved Hide resolved
TableViewModel.create {
headerRow(headerCell("#"), headerCell("Container Decisions"))
containers
.sortedBy { it.name }
.filter { it.documentation.decisions.isNotEmpty() }
.forEachIndexed { index, container ->
bodyRow(
cellWithIndex((index+1).toString()),
cellWithLink(this@createContainerTableViewModel, container.name, hrefFactory(container))
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.*

data class DecisionTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val visible: Boolean, val url: String) {
galuszkak marked this conversation as resolved.
Show resolved Hide resolved
val link = LinkViewModel(pageViewModel, title, url, true)
}

fun SoftwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem: SoftwareSystem, tab: SoftwareSystemPageViewModel.Tab): List<DecisionTabViewModel> {
val softwareTab = DecisionTabViewModel(
this,
"System",
softwareSystem.hasDecisions(),
SoftwareSystemPageViewModel.url(softwareSystem, tab)
)
val containerTabs = softwareSystem
.containers
.map {
DecisionTabViewModel(
this,
it.name,
it.hasDecisions(),
SoftwareSystemContainerDecisionsPageViewModel.url(it)
)
}
return listOf(softwareTab).plus(containerTabs)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.documentation.Decision
import com.structurizr.model.SoftwareSystem
import com.structurizr.model.Container
import nl.avisi.structurizr.site.generatr.normalize
import nl.avisi.structurizr.site.generatr.site.GeneratorContext

class SoftwareSystemContainerDecisionPageViewModel(
generatorContext: GeneratorContext, container: Container, decision: Decision
) : SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.DECISIONS) {
override val url = url(container, decision)

val content = markdownToHtml(this, decision.content, generatorContext.svgFactory)

companion object {
fun url(container: Container, decision: Decision) =
"${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}/${decision.id}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.documentation.Decision
galuszkak marked this conversation as resolved.
Show resolved Hide resolved
import com.structurizr.model.Container
import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.hasContainerDecisions
galuszkak marked this conversation as resolved.
Show resolved Hide resolved
import nl.avisi.structurizr.site.generatr.hasDecisions
import nl.avisi.structurizr.site.generatr.normalize
import nl.avisi.structurizr.site.generatr.site.GeneratorContext

class SoftwareSystemContainerDecisionsPageViewModel(generatorContext: GeneratorContext, container: Container) :
SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.DECISIONS) {
override val url = url(container)
val decisionsTable = createDecisionsTableViewModel(container.documentation.decisions) {
"$url/${it.id}"
}

val visible = container.hasDecisions()
val decisionTabs = createDecisionsTabViewModel(container.softwareSystem, Tab.DECISIONS)

companion object {
fun url(container: Container) =
"${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}"
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.hasContainerDecisions
import nl.avisi.structurizr.site.generatr.hasDecisions
import nl.avisi.structurizr.site.generatr.normalize
galuszkak marked this conversation as resolved.
Show resolved Hide resolved
import nl.avisi.structurizr.site.generatr.site.GeneratorContext

class SoftwareSystemDecisionsPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :
SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) {

val decisionsTable = createDecisionsTableViewModel(softwareSystem.documentation.decisions) {
"$url/${it.id}"
}
val visible = softwareSystem.hasDecisions()

private val visibleContainerDecisions = softwareSystem.hasContainerDecisions()
val visibleSoftwareSystemDecisions = softwareSystem.hasDecisions()

val visible = visibleSoftwareSystemDecisions or visibleContainerDecisions
val visibleOnlyContainersDecisions = !visibleSoftwareSystemDecisions and visibleContainerDecisions
galuszkak marked this conversation as resolved.
Show resolved Hide resolved

val decisionTabs = createDecisionsTabViewModel(softwareSystem, Tab.DECISIONS)

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.hasComponentViews
import nl.avisi.structurizr.site.generatr.hasContainerViews
import nl.avisi.structurizr.site.generatr.hasDecisions
import nl.avisi.structurizr.site.generatr.hasDeploymentViews
import nl.avisi.structurizr.site.generatr.hasDocumentationSections
import nl.avisi.structurizr.site.generatr.hasSystemContextViews
import nl.avisi.structurizr.site.generatr.normalize
import nl.avisi.structurizr.site.generatr.*
import nl.avisi.structurizr.site.generatr.site.GeneratorContext

open class SoftwareSystemPageViewModel(
Expand Down Expand Up @@ -40,7 +34,7 @@ open class SoftwareSystemPageViewModel(
Tab.CONTAINER -> generatorContext.workspace.views.hasContainerViews(softwareSystem)
Tab.COMPONENT -> generatorContext.workspace.views.hasComponentViews(softwareSystem)
Tab.DEPLOYMENT -> generatorContext.workspace.views.hasDeploymentViews(softwareSystem)
Tab.DECISIONS -> softwareSystem.hasDecisions()
Tab.DECISIONS -> softwareSystem.hasDecisions() or softwareSystem.hasContainerDecisions()
Tab.SECTIONS -> softwareSystem.hasDocumentationSections()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import nl.avisi.structurizr.site.generatr.site.GeneratorContext

class SoftwareSystemSectionPageViewModel(
generatorContext: GeneratorContext, softwareSystem: SoftwareSystem, section: Section
) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) {
) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.SECTIONS) {
override val url = url(softwareSystem, section)

val content = markdownToHtml(this, section.content, generatorContext.svgFactory)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.HTML
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.meta
import kotlinx.html.title

fun HTML.redirectRelative(appendUrl: String) {
attributes["lang"] = "en"
head {
meta {
httpEquiv = "refresh"
content = "0; url=./$appendUrl"
}
title { +"Structurizr site generatr" }
}
body()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.HTML
import kotlinx.html.h3
galuszkak marked this conversation as resolved.
Show resolved Hide resolved
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionPageViewModel

fun HTML.softwareSystemContainerDecisionPage(viewModel: SoftwareSystemContainerDecisionPageViewModel) {
softwareSystemPage(viewModel) {
rawHtml(viewModel.content)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.*
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionsPageViewModel

fun HTML.softwareSystemContainerDecisionsPage(viewModel: SoftwareSystemContainerDecisionsPageViewModel) {
if (viewModel.visible)
softwareSystemPage(viewModel) {
div(classes = "tabs") {
ul(classes = "m-0") {
viewModel.decisionTabs
.filter { it.visible }
.forEach {
li(classes = if (it.link.active) "is-active" else null) {
link(it.link)
}
}
}
}
table(viewModel.decisionsTable)
}
else
redirectUpPage()
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.HTML
import kotlinx.html.div
import kotlinx.html.li
import kotlinx.html.ul
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionsPageViewModel

fun HTML.softwareSystemDecisionsPage(viewModel: SoftwareSystemDecisionsPageViewModel) {
if (viewModel.visible)
if (viewModel.visibleOnlyContainersDecisions) {
redirectRelative(
viewModel.decisionTabs.filter { it.visible }.first().link.relativeHref
galuszkak marked this conversation as resolved.
Show resolved Hide resolved
)
}
else if (viewModel.visible) {
softwareSystemPage(viewModel) {
table(viewModel.decisionsTable)
div(classes = "tabs") {
ul(classes = "m-0") {
viewModel.decisionTabs
.filter { it.visible }
.forEach {
li(classes = if (it.link.active) "is-active" else null) {
link(it.link)
}
}
}
}
if (viewModel.visibleSoftwareSystemDecisions) {
table(viewModel.decisionsTable)
}
}
}
else
redirectUpPage()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package nl.avisi.structurizr.site.generatr.site.model

import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test

class ContainersTableViewModelTest : ViewModelTest() {

@Test
fun `no container with decisions available`() {
assertThat(pageViewModel().createContainerTableViewModel(emptySet()) { "href" })
.isEqualTo(
TableViewModel.create {
containersTableHeaderRow()
}
)
}

@Test
fun `many containers shown if they have decisions`() {
val containers = generatorContext().workspace.model.addSoftwareSystem("Mock").also {
it.addContainer("Web Application")
it.addContainer("API Application")
.documentation.addDecision(createDecision("1","API Decision"))
it.addContainer("Mobile Application")
.documentation.addDecision(createDecision("1", "Mobile Decision"))
}.containers
val pageViewModel = pageViewModel()
assertThat(pageViewModel.createContainerTableViewModel(containers) { it.name }).isEqualTo(
TableViewModel.create {
containersTableHeaderRow()
bodyRow(
cellWithIndex("1"),
cellWithLink(pageViewModel, "API Application", "API Application"),
)
bodyRow(
cellWithIndex("2"),
cellWithLink(pageViewModel, "Mobile Application", "Mobile Application")
)
}
)
}

private fun TableViewModel.TableViewInitializerContext.containersTableHeaderRow() {
headerRow(headerCell("#"), headerCell("Container Decisions"))
}
}
Loading