Skip to content

Commit

Permalink
Get ContextDiagrams working
Browse files Browse the repository at this point in the history
Still needs a more thorough test case
  • Loading branch information
reidspencer committed Jan 25, 2024
1 parent 0b6c500 commit f348ca3
Show file tree
Hide file tree
Showing 12 changed files with 477 additions and 116 deletions.
213 changes: 176 additions & 37 deletions diagrams/src/main/scala/com/ossuminc/riddl/diagrams/DiagramsPass.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
package com.ossuminc.riddl.diagrams

import com.ossuminc.riddl.language.AST.*
import com.ossuminc.riddl.language.{AST, Messages}
import com.ossuminc.riddl.language.{AST, At, Messages}
import com.ossuminc.riddl.passes.*
import com.ossuminc.riddl.passes.resolve.ResolutionPass
import com.ossuminc.riddl.passes.symbols.Symbols.Parent
import com.ossuminc.riddl.passes.symbols.SymbolsPass
import com.ossuminc.riddl.passes.validate.ValidationPass

Expand All @@ -29,6 +30,8 @@ case class UseCaseDiagramData(
interactions: Seq[Interaction]
)

type ContextRelationship = (Context, String)

/** The information needed to generate a Context Diagram showing the relationships between bounded contexts
*
* @param domain
Expand All @@ -41,11 +44,11 @@ case class UseCaseDiagramData(
case class ContextDiagramData(
domain: Domain,
aggregates: Seq[Entity] = Seq.empty,
relationships: Seq[(Context, String)]
relationships: Seq[ContextRelationship]
)

/** The information needed to generate a Context Diagram at the Domain level to show the relationships between
* its constituent bounded contexts
/** The information needed to generate a Context Diagram at the Domain level to show the relationships between its
* constituent bounded contexts
*/
type DomainDiagramData = Seq[(Context, ContextDiagramData)]

Expand Down Expand Up @@ -89,7 +92,7 @@ class DiagramsPass(input: PassInput, outputs: PassesOutput) extends Pass(input,
definition match
case c: Context =>
val aggregates = c.entities.filter(_.hasOption[EntityIsAggregate])
val relationships = findRelationships(c)
val relationships = makeRelationships(c)
val domain = parents.head.asInstanceOf[Domain]
contextDiagrams.put(c, ContextDiagramData(domain, aggregates, relationships))
case epic: Epic =>
Expand All @@ -101,20 +104,109 @@ class DiagramsPass(input: PassInput, outputs: PassesOutput) extends Pass(input,
case _ => ()
}

private def findRelationships(context: AST.Context): Seq[(Context, String)] = {
val statements: Seq[Statement] = pullStatements(context) ++
context.entities.flatMap(pullStatements) ++
context.adaptors.flatMap(pullStatements) ++
context.repositories.flatMap(pullStatements) ++
context.streamlets.flatMap(pullStatements)
private def makeRelationships(context: Context): Seq[ContextRelationship] = {
val allProcessors = findProcessors(context)
for {
processor <- allProcessors
relationship <- makeProcessorRelationships(context, processor)
} yield {
relationship
}
}

private def findProcessors(processor: Processor[?, ?]): Seq[Processor[?, ?]] = {
val containedProcessors = processor match {
case a: Adaptor => a.contents.processors
case a: Application => a.contents.processors
case c: Context => c.contents.processors
case e: Entity => e.contents.processors
case p: Projector => p.contents.processors
case r: Repository => r.contents.processors
case s: Streamlet => s.contents.processors
}
val includedProcessors = processor.includes.processors
val nestedProcessors = (containedProcessors ++ includedProcessors).flatMap(findProcessors)

includedProcessors ++ nestedProcessors :+ processor
}

private def makeProcessorRelationships(
context: Context,
processor: Processor[?, ?]
): Seq[ContextRelationship] = {
val rel1 = makeTypeRelationships(context, processor.types, processor)
val rel2 = makeFunctionRelationships(context, processor.functions)
val rel3 = makeHandlerRelationships(context, processor.handlers)
val rel4 = makeInletRelationships(context, processor.inlets)
val rel5 = makeOutletRelationships(context, processor.outlets)
val rel6 = processor match {
case a: Adaptor => inferRelationship(context, a)
case a: Application => Seq.empty[ContextRelationship]
case c: Context => Seq.empty[ContextRelationship]
case e: Entity => makeHandlerRelationships(context, e.states.flatMap(_.handlers))
case p: Projector => Seq.empty[ContextRelationship]
case r: Repository => Seq.empty[ContextRelationship]
case s: Streamlet => Seq.empty[ContextRelationship]
}
rel1 ++ rel2 ++ rel3 ++ rel4 ++ rel5 ++ rel6
}

private def makeFunctionRelationships(context: Context, functions: Seq[Function]): Seq[ContextRelationship] = {
for {
f <- functions
inputFields = f.input.map(_.fields).getOrElse(Seq.empty)
outputFields = f.output.map(_.fields).getOrElse(Seq.empty)
relationship <- makeFieldRelationships(context, inputFields ++ outputFields, f)
} yield {
relationship
}
}

private def makeOutletRelationships(context: Context, outlets: Seq[Outlet]): Seq[ContextRelationship] = {
for {
o <- outlets
r = o.type_
t <- this.refMap.definitionOf[Type](r, o)
relationship <- inferRelationship(context, t)
} yield {
relationship
}
}

private def makeInletRelationships(context: Context, inlets: Seq[Inlet]): Seq[ContextRelationship] = {
for {
s <- statements
ref <- getStatementReferences(s)
definition <- refMap.definitionOf(ref, context)
relationship <- inferRelationship(definition)
i <- inlets
r = i.type_
t <- this.refMap.definitionOf[Type](r, i)
relationship <- inferRelationship(context, t)
} yield {
context -> relationship
relationship
}
}

private def makeHandlerRelationships(context: Context, handlers: Seq[Handler]): Seq[ContextRelationship] = {
for {
h <- handlers
oc: OnClause <- h.clauses
omc: OnMessageClause = oc.asInstanceOf[OnMessageClause] if oc.isInstanceOf[OnMessageClause]
relationship <- makeStatementRelationships(context, omc, omc.statements)
} yield {
relationship
}
}

private def makeStatementRelationships(
context: Context,
parent: OnMessageClause,
statements: Seq[Statement]
): Seq[ContextRelationship] = {
for {
statement <- statements
ref <- getStatementReferences(statement)
definition <- this.refMap.definitionOf[Definition](ref, parent)
relationship <- inferRelationship(context, definition)
} yield {
relationship
}
}

Expand All @@ -124,30 +216,77 @@ class DiagramsPass(input: PassInput, outputs: PassesOutput) extends Pass(input,
case TellStatement(_, msg, processor) => Seq(msg, processor)
case SetStatement(_, field, _) => Seq(field)
case ReplyStatement(_, message) => Seq(message)
case CallStatement(_, function) => Seq(function)
case _ => Seq.empty
}

private def inferRelationship(definition: Definition): Option[String] = {
definition match
case m: Type if m.typ.isContainer && m.typ.hasDefinitions =>
symTab.contextOf(m).map(c => s"Uses ${m.identify} in ${c.identify}")
case f: Field =>
symTab.contextOf(f).map(c => s"Sets ${f.identify} in ${c.identify}")
case p: Portlet =>
symTab.contextOf(p).map(c => s"Sends to ${p.identify} in ${c.identify}")
case p: Processor[?, ?] =>
symTab.contextOf(p).map(c => s"Tells to ${p.identify} in ${c.identify}")
case _ => None
}

private def pullStatements(processor: Processor[?, ?]): Seq[Statement] = {
val s1 = processor.functions.flatMap(_.statements)
val s2 = for {
h <- processor.handlers
omc <- h.clauses if omc.isInstanceOf[OnMessageClause]
s <- omc.statements
} yield { s }
s1 ++ s2
private def getTypeReferences(typEx: TypeExpression): Seq[Reference[Definition]] = {
typEx match {
case EntityReferenceTypeExpression(loc, pid) => Seq(EntityRef(loc, pid))
case AliasedTypeExpression(loc, keyword, pathId) => Seq(TypeRef(loc, keyword, pathId))
case aucte: AggregateUseCaseTypeExpression =>
aucte.fields.foldLeft(Seq.empty) { case (s, f) => s ++ getTypeReferences(f.typeEx) }
case ate: AggregateTypeExpression =>
ate.fields.foldLeft(Seq.empty) { case (s, f) => s ++ getTypeReferences(f.typeEx) }
case _: TypeExpression => Seq.empty
}
}

private def makeTypeRelationships(
context: Context,
types: Seq[Type],
parent: Definition
): Seq[ContextRelationship] = {
for {
typ <- types
typEx = typ.typ
ref: Reference[Definition] <- getTypeReferences(typEx)
definition <- refMap.definitionOf[Definition](ref, parent)
relationship <- inferRelationship(context, definition)
} yield {
relationship
}
}

private def makeFieldRelationships(
context: Context,
fields: Seq[Field],
parent: Definition
): Seq[ContextRelationship] = {
for {
f <- fields
ref: Reference[Definition] <- getTypeReferences(f.typeEx)
definition: Definition <- this.refMap.definitionOf[Definition](ref, parent)
relationship <- inferRelationship(context, definition)
} yield {
relationship
}
}

private def inferRelationship(context: Context, definition: Definition): Option[ContextRelationship] = {
this.symTab.contextOf(definition) match {
case Some(foreignContext) =>
if (foreignContext != context) then
definition match {
case a: Adaptor =>
refMap.definitionOf[Context](a.context, a) match {
case Some(foreignContext) =>
if foreignContext != context then Some(foreignContext -> s"adaptation ${a.direction.format}")
else None
case None => None
}
case m: Type => Some(foreignContext -> s"Uses ${m.identify} from")
case e: Entity => Some(foreignContext -> s"References ${e.identify} in")
case f: Field => Some(foreignContext -> s"Sets ${f.identify} in")
case i: Inlet => Some(foreignContext -> s"Sends to ${i.identify} in")
case o: Outlet => Some(foreignContext -> s"Takes from ${o.identify} in")
case p: Processor[?, ?] => Some(foreignContext -> s"Tells to ${p.identify} in")
case _ => None
}
else None
end if
case None => None
}
}

private def actorsFirst(a: (String, Definition), b: (String, Definition)): Boolean = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.ossuminc.riddl.diagrams.mermaid

import com.ossuminc.riddl.language.AST.Context
import com.ossuminc.riddl.language.AST.{Context, Processor}
import com.ossuminc.riddl.diagrams.ContextDiagramData
import com.ossuminc.riddl.diagrams.mermaid.FlowchartDiagramGenerator

/** Context Diagram generator using a DataFlow Diagram from Mermaid
*
Expand Down Expand Up @@ -58,47 +57,82 @@ case class ContextDiagram(context: Context, data: ContextDiagramData)

emitClassDefs()
emitDomainSubgraph()
makeClassAssignments()

private def emitClassDefs(): Unit = {
addLine("classDef default fill:#666,stroke:black,stroke-width:3px,color:white;")
for {
context <- relatedContexts
css = getCssFor(context) if css.nonEmpty
ctxt <- context +: relatedContexts
} do {
addLine(s"classDef ${context.id.value}_class $css; ")
val css: String = getCssFor(ctxt)
if css.nonEmpty then
addLine(s"classDef ${ctxt.id.value}_class $css; ")
else
addLine(s"classDef ${ctxt.id.value}_class color:white,stroke-width:3px;")
end if
}
}

private def makeClassAssignments(): Unit = {
for {
ctxt <- context +: relatedContexts
} do {
addLine(s"class ${ctxt.id.value} ${ctxt.id.value}_class")
}
}

private def emitDomainSubgraph(): Unit = {
addLine(s"subgraph ${data.domain.identify}")
incr
makeNodes()
makeRelationships()
decr
addLine("end")
makeClassAssignments()
}

private def makeRelationships(): Unit = {
private def makeNode(processor: Processor[?,?]): String = {
val iconName = getIconFor(processor)
val faicon = if iconName.nonEmpty then "fa:" + iconName + "<br/>" else ""
val contextName: String = processor.id.value
val numWords = contextName.count(_.isSpaceChar) + 1
val spacedName =
if numWords > 4 then
var i = 0
for {
word <- contextName.split(' ')
} yield {
i = i + 1
if i == numWords then word
else if i % 3 == 0 then word + "<br/"
else word + " "
}.mkString
else
if contextName.length > 8 then
contextName
else
val numSpaces = (8 - contextName.length) / 2
val fix = "&nbsp;".repeat(numSpaces)
fix + contextName + fix
end if
end if
s"$contextName(($faicon$spacedName))"
}

private def makeNodes(): Unit = {
for {
(context,relationship) <- data.relationships
aContext <- context +: relatedContexts
} {
val iconName = getIconFor(context)
val faicon = if iconName.nonEmpty then "fa:" + iconName + "<br/>" else ""
val contextName = context.id.value
val numSpaces = if contextName.length < 8 then (8-contextName.length)/2 else 0
val fix = "&nbsp;".repeat(numSpaces)
val name = fix + contextName + fix
val fullName = s"(($faicon$name))"

val name = makeNode(aContext)
addLine(name)
}
}

private def makeClassAssignments(): Unit = {
private def makeRelationships(): Unit = {
val mainNodeName = context.id.value
for {
context <- relatedContexts
css = getCssFor(context) if css.nonEmpty
} do {
addLine(s"class ${context.id.value} ${context.id.value}_class ")
(ctxt,relationship) <- data.relationships
} {
addIndent().append(mainNodeName).append("-->|").append(relationship).append("|").append(makeNode(ctxt)).nl
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.ossuminc.riddl.diagrams.mermaid

/** A mermaid diagram generator for making sequence diagrams
*
*/
trait SequenceDiagramGenerator extends MermaidDiagramGenerator {
def kind: String = "sequenceDiagram"
frontMatter()
addLine("sequenceDiagram").nl.incr.addIndent("autonumber")

addLine("sequenceDiagram").incr.addIndent("autonumber")
}
Loading

0 comments on commit f348ca3

Please sign in to comment.