Skip to content

Commit

Permalink
Adds a couple of inspections related to embedded views in documentati…
Browse files Browse the repository at this point in the history
…on and decisions.
  • Loading branch information
simonbrowndotje committed Feb 18, 2024
1 parent 371d570 commit 757ef70
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> findEmbeddedViewKeys(Documentable documentable) {
Set<String> 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<String> findEmbeddedViewKeys(DocumentationContent content) {
Set<String> 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();
}

}
Original file line number Diff line number Diff line change
@@ -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<String> keys = findEmbeddedViewKeys(documentable);
Set<String> 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";
}

}
Original file line number Diff line number Diff line change
@@ -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<String> keys = findEmbeddedViewKeys(documentable);
Set<String> 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";
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading

0 comments on commit 757ef70

Please sign in to comment.