diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java index 5262a23f..039c8a01 100644 --- a/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java @@ -1,6 +1,8 @@ package com.structurizr.inspection; import com.structurizr.Workspace; +import com.structurizr.inspection.documentation.EmbeddedViewMissingInspection; +import com.structurizr.inspection.documentation.EmbeddedViewWithGeneratedKeyInspection; import com.structurizr.inspection.model.*; import com.structurizr.inspection.view.*; import com.structurizr.inspection.workspace.WorkspaceScopeInspection; @@ -21,6 +23,8 @@ public DefaultInspector(Workspace workspace) { private void runWorkspaceInspections() { add(new WorkspaceToolingInspection(this).run()); add(new WorkspaceScopeInspection(this).run()); + add(new EmbeddedViewMissingInspection(this).run(getWorkspace())); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run(getWorkspace())); } private void runModelInspections() { @@ -37,16 +41,22 @@ private void runModelInspections() { add(new SoftwareSystemDescriptionInspection(this).run(element)); add(new SoftwareSystemDocumentationInspection(this).run(element)); add(new SoftwareSystemDecisionsInspection(this).run(element)); + add(new EmbeddedViewMissingInspection(this).run((SoftwareSystem)element)); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run((SoftwareSystem)element)); } if (element instanceof Container) { add(new ContainerDescriptionInspection(this).run(element)); add(new ContainerTechnologyInspection(this).run(element)); + add(new EmbeddedViewMissingInspection(this).run((Container)element)); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run((Container)element)); } if (element instanceof Component) { add(new ComponentDescriptionInspection(this).run(element)); add(new ComponentTechnologyInspection(this).run(element)); + add(new EmbeddedViewMissingInspection(this).run((Component)element)); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run((Component)element)); } if (element instanceof DeploymentNode) { diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java new file mode 100644 index 00000000..23814a33 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java @@ -0,0 +1,83 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.*; +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class AbstractDocumentableInspection extends Inspection { + + private static final Pattern MARKDOWN_EMBED = Pattern.compile("!\\[.*?]\\(embed:(.+?)\\)"); + private static final Pattern ASCIIDOC_EMBED = Pattern.compile("image::embed:(.+?)\\[]"); + + public AbstractDocumentableInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run(Documentable documentable) { + Severity severity; + if (documentable instanceof Workspace) { + severity = getInspector().getSeverityStrategy().getSeverity(this, (Workspace)documentable); + } else { + Element element = (Element)documentable; + severity = getInspector().getSeverityStrategy().getSeverity(this, element); + } + Violation violation = inspect(documentable); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected abstract Violation inspect(Documentable documentable); + + protected Set findEmbeddedViewKeys(Documentable documentable) { + Set keys = new LinkedHashSet<>(); + + for (Section section : documentable.getDocumentation().getSections()) { + keys.addAll(findEmbeddedViewKeys(section)); + } + + for (Decision decision : documentable.getDocumentation().getDecisions()) { + keys.addAll(findEmbeddedViewKeys(decision)); + } + + return keys; + } + + private Set findEmbeddedViewKeys(DocumentationContent content) { + Set keys = new LinkedHashSet<>(); + + String[] lines = content.getContent().split("\n"); + for (String line : lines) { + if (content.getFormat() == Format.Markdown) { + // ![](embed:MyDiagramKey) + Matcher matcher = MARKDOWN_EMBED.matcher(line); + if (matcher.matches()) { + String key = matcher.group(1); + keys.add(key); + } + } else if (content.getFormat() == Format.AsciiDoc) { + // image::embed:MyDiagramKey[] + Matcher matcher = ASCIIDOC_EMBED.matcher(line); + if (matcher.matches()) { + String key = matcher.group(1); + keys.add(key); + } + } + } + + return keys; + } + + protected String terminologyFor(Element element) { + return getWorkspace().getViews().getConfiguration().getTerminology().findTerminology(element).toLowerCase(); + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspection.java new file mode 100644 index 00000000..76559eb8 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspection.java @@ -0,0 +1,52 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.view.View; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class EmbeddedViewMissingInspection extends AbstractDocumentableInspection { + + public EmbeddedViewMissingInspection(Inspector inspector) { + super(inspector); + } + + protected Violation inspect(Documentable documentable) { + Set keys = findEmbeddedViewKeys(documentable); + Set missingViews = new LinkedHashSet<>(); + + for (String key : keys) { + View view = getWorkspace().getViews().getViewWithKey(key); + if (view == null) { + missingViews.add(key); + } + } + + if (!missingViews.isEmpty()) { + if (documentable instanceof Workspace) { + return violation("The following views are embedded into documentation for the workspace but do not exist in the workspace: " + String.join(", ", missingViews)); + } else if (documentable instanceof Element) { + Element element = (Element)documentable; + return violation("The following views are embedded into documentation for the " + terminologyFor(element).toLowerCase() + " named \"" + element.getName() + "\" but do not exist in the workspace: " + String.join(", ", missingViews)); + } + } + + return noViolation(); + } + + @Override + protected String getType() { + return "documentation.embeddedView"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspection.java new file mode 100644 index 00000000..09199c22 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspection.java @@ -0,0 +1,49 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Documentable; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.view.View; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class EmbeddedViewWithGeneratedKeyInspection extends AbstractDocumentableInspection { + + public EmbeddedViewWithGeneratedKeyInspection(Inspector inspector) { + super(inspector); + } + + protected Violation inspect(Documentable documentable) { + Set keys = findEmbeddedViewKeys(documentable); + Set viewsWithGeneratedKeys = new LinkedHashSet<>(); + + for (String key : keys) { + View view = getWorkspace().getViews().getViewWithKey(key); + if (view != null && view.isGeneratedKey()) { + viewsWithGeneratedKeys.add(key); + } + } + + if (!viewsWithGeneratedKeys.isEmpty()) { + if (documentable instanceof Workspace) { + return violation("The following views are embedded into documentation for the workspace via an automatically generated view key: " + String.join(", ", viewsWithGeneratedKeys)); + } else if (documentable instanceof Element) { + Element element = (Element)documentable; + return violation("The following views are embedded into documentation for the " + terminologyFor(element).toLowerCase() + " named \"" + element.getName() + "\" via an automatically generated view key: " + String.join(", ", viewsWithGeneratedKeys)); + } + } + + return noViolation(); + } + + @Override + protected String getType() { + return "documentation.embeddedView"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspectionTests.java new file mode 100644 index 00000000..fac6f825 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspectionTests.java @@ -0,0 +1,91 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.inspection.model.SoftwareSystemDocumentationInspection; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class EmbeddedViewMissingInspectionTests { + + @Test + public void run_WithMissingView() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext) + """); + softwareSystem.getDocumentation().addSection(section); + + Decision decision = new Decision("1"); + decision.setTitle("Decision 1"); + decision.setStatus("Accepted"); + decision.setFormat(Format.AsciiDoc); + decision.setContent(""" + ## Containers + + image::embed:Containers[] + """); + softwareSystem.getDocumentation().addDecision(decision); + + Violation violation = new EmbeddedViewMissingInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("documentation.embeddedView", violation.getType()); + assertEquals("The following views are embedded into documentation for the software system named \"Software System\" but do not exist in the workspace: SystemContext, Containers", violation.getMessage()); + + workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + + violation = new EmbeddedViewMissingInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("documentation.embeddedView", violation.getType()); + assertEquals("The following views are embedded into documentation for the software system named \"Software System\" but do not exist in the workspace: Containers", violation.getMessage()); + } + + @Test + public void run_WithoutMissingView() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext) + """); + softwareSystem.getDocumentation().addSection(section); + + Decision decision = new Decision("1"); + decision.setTitle("Decision 1"); + decision.setStatus("Accepted"); + decision.setFormat(Format.AsciiDoc); + decision.setContent(""" + ## Containers + + image::embed:Containers[] + """); + softwareSystem.getDocumentation().addDecision(decision); + + workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + workspace.getViews().createContainerView(softwareSystem, "Containers", "Description"); + + Violation violation = new EmbeddedViewMissingInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspectionTests.java new file mode 100644 index 00000000..5081eb93 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspectionTests.java @@ -0,0 +1,80 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class EmbeddedViewWithGeneratedKeyInspectionTests { + + @Test + public void run_WithGeneratedKey() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext-001) + """); + softwareSystem.getDocumentation().addSection(section); + + section = new Section(); + section.setFormat(Format.AsciiDoc); + section.setContent(""" + ## Containers + + image::embed:Container-001[] + """); + softwareSystem.getDocumentation().addSection(section); + + workspace.getViews().createSystemContextView(softwareSystem, "", "Description"); + workspace.getViews().createContainerView(softwareSystem, "", "Description"); + + Violation violation = new EmbeddedViewWithGeneratedKeyInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("documentation.embeddedView", violation.getType()); + assertEquals("The following views are embedded into documentation for the software system named \"Software System\" via an automatically generated view key: SystemContext-001, Container-001", violation.getMessage()); + } + + @Test + public void run_WithoutGeneratedKey() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext) + """); + softwareSystem.getDocumentation().addSection(section); + + section = new Section(); + section.setFormat(Format.AsciiDoc); + section.setContent(""" + ## Containers + + image::embed:Containers[] + """); + softwareSystem.getDocumentation().addSection(section); + + workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + workspace.getViews().createContainerView(softwareSystem, "Containers", "Description"); + + Violation violation = new EmbeddedViewWithGeneratedKeyInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +}