Skip to content

Commit

Permalink
Merge pull request #149 from galuszkak/feature/add-container-adrs
Browse files Browse the repository at this point in the history
Introduce Container ADRs in Decision Tab
  • Loading branch information
dirkgroot authored May 27, 2023
2 parents 8f10c58 + 5782055 commit ca14ca7
Show file tree
Hide file tree
Showing 21 changed files with 449 additions and 14 deletions.
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.any { it.hasDecisions() }

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 @@ -139,6 +139,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 @@ -159,6 +168,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,18 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.model.Container
import nl.avisi.structurizr.site.generatr.hasDecisions

fun PageViewModel.createContainerDecisionsTableViewModel(containers: Collection<Container>, hrefFactory: (Container) -> String) =
TableViewModel.create {
headerRow(headerCell("#"), headerCell("Container Decisions"))
containers
.sortedBy { it.name }
.filter { it.hasDecisions() }
.forEachIndexed { index, container ->
bodyRow(
cellWithIndex((index+1).toString()),
cellWithLink(this@createContainerDecisionsTableViewModel, container.name, hrefFactory(container))
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nl.avisi.structurizr.site.generatr.site.model

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

data class DecisionTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String) {
val link = LinkViewModel(pageViewModel, title, url, true)
}

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

import com.structurizr.documentation.Decision
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,22 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.model.Container
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,23 @@
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.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 containerDecisionsVisible = softwareSystem.hasContainerDecisions()
val softwareSystemDecisionsVisible = softwareSystem.hasDecisions()

val visible = softwareSystemDecisionsVisible or containerDecisionsVisible
val onlyContainersDecisionsVisible = !softwareSystemDecisionsVisible and containerDecisionsVisible

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,10 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.HTML
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,26 @@
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.SoftwareSystemContainerDecisionsPageViewModel

fun HTML.softwareSystemContainerDecisionsPage(viewModel: SoftwareSystemContainerDecisionsPageViewModel) {
if (viewModel.visible)
softwareSystemPage(viewModel) {
div(classes = "tabs") {
ul(classes = "m-0") {
viewModel.decisionTabs
.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,34 @@
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.onlyContainersDecisionsVisible) {
redirectRelative(
viewModel.decisionTabs.first().link.relativeHref
)
}
else if (viewModel.visible) {
softwareSystemPage(viewModel) {
table(viewModel.decisionsTable)
div(classes = "tabs") {
ul(classes = "m-0") {
viewModel.decisionTabs
.forEach {
li(classes = if (it.link.active) "is-active" else null) {
link(it.link)
}
}
}
}
if (viewModel.softwareSystemDecisionsVisible) {
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 ContainerDecisionsTableViewModelTest : ViewModelTest() {

@Test
fun `no container with decisions available`() {
assertThat(pageViewModel().createContainerDecisionsTableViewModel(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.createContainerDecisionsTableViewModel(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

0 comments on commit ca14ca7

Please sign in to comment.