ModulithMermaid.java
package li.selman.modulithdocsmarkdown;
import com.structurizr.Workspace;
import com.structurizr.export.mermaid.MermaidDiagramExporter;
import com.structurizr.model.Component;
import com.structurizr.model.Container;
import com.structurizr.model.Tags;
import com.structurizr.view.ComponentView;
import com.structurizr.view.Shape;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.ApplicationModules;
/**
* Renders Mermaid component diagrams for an {@link ApplicationModules} instance, built from the exact same
* Structurizr view model {@code Documenter} builds (software system -> container -> one {@link Component} per
* module, {@code uses} edges from direct module dependencies), then exported with Structurizr's own
* {@link MermaidDiagramExporter}. Diagram parity with Spring Modulith's PlantUML output holds by construction because
* both are derived from the identical model, never from a PlantUML-to-Mermaid text conversion.
*/
public final class ModulithMermaid {
private final ApplicationModules modules;
private final Workspace workspace;
private final Container container;
private final Map<ApplicationModule, Component> components;
public ModulithMermaid(ApplicationModules modules) {
this.modules = modules;
this.workspace = new Workspace("Modulith", "");
workspace
.getViews()
.getConfiguration()
.getStyles()
.addElementStyle(Tags.COMPONENT)
.shape(Shape.Component);
var systemName = modules.getSystemName().orElse("Modules");
var system = workspace.getModel().addSoftwareSystem(systemName, "");
this.container = system.addContainer(systemName, "", "");
this.components = modules.stream()
.collect(Collectors.toMap(
Function.identity(), module -> container.addComponent(module.getDisplayName(), "", "Module")));
modules.forEach(this::wireDependencies);
}
/**
* A diagram of all modules and every direct inter-module dependency.
*/
public String overviewDiagram() {
var componentView = workspace.getViews().createComponentView(container, "overview", "");
componentView.setTitle(modules.getSystemName().orElse("Modules"));
modules.forEach(module -> componentView.add(componentFor(module)));
return export(componentView);
}
/**
* A diagram of the given module, its direct dependencies and its bootstrap (upstream) dependencies.
*/
public String moduleDiagram(ApplicationModule module) {
var componentView =
workspace.getViews().createComponentView(container, module.getIdentifier() + "-diagram", "");
componentView.setTitle(module.getDisplayName());
Stream.concat(
Stream.of(module),
Stream.concat(
module.getDirectDependencies(modules).uniqueModules(),
module.getBootstrapDependencies(modules)))
.distinct()
.forEach(m -> componentView.add(componentFor(m)));
return export(componentView);
}
private void wireDependencies(ApplicationModule module) {
var source = componentFor(module);
module.getDirectDependencies(modules)
.uniqueModules()
.map(this::componentFor)
.forEach(target -> source.uses(target, "uses"));
}
@SuppressWarnings("NullAway") // components is populated for every module in the constructor
private Component componentFor(ApplicationModule module) {
return components.get(module);
}
private static String export(ComponentView view) {
return new MermaidDiagramExporter().export(view).getDefinition();
}
}