diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java index a4120317..23da80b8 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java @@ -369,17 +369,14 @@ class Asciidoctor { } public String renderHeadline(int i, String modules) { - return "=".repeat(i) + " " + modules + System.lineSeparator(); } public String renderPlantUmlInclude(String componentsFilename) { - return "plantuml::" + componentsFilename + "[]" + System.lineSeparator(); } public String renderGeneralInclude(String componentsFilename) { - return "include::" + componentsFilename + "[]" + System.lineSeparator(); } } diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java index c04e0b0e..b5385eac 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java @@ -70,6 +70,7 @@ import com.tngtech.archunit.core.domain.JavaClass; * API to create documentation for {@link ApplicationModules}. * * @author Oliver Drotbohm + * @author Cora Iberkleid */ public class Documenter { @@ -134,7 +135,7 @@ public class Documenter { .shape(Shape.Component); Model model = workspace.getModel(); - String systemName = modules.getSystemName().orElse("Modulith"); + String systemName = getDefaultedSystemName(); SoftwareSystem system = model.addSoftwareSystem(systemName, ""); @@ -190,13 +191,13 @@ public class Documenter { } /** - * Writes aggregating document called 'all-docs.adoc' that includes any existing component diagrams and canvases. - * using {@link DiagramOptions#defaults()} and {@link CanvasOptions#defaults()}. + * Writes aggregating document called {@code all-docs.adoc} that includes any existing component diagrams and + * canvases. using {@link DiagramOptions#defaults()} and {@link CanvasOptions#defaults()}. * * @return the current instance, will never be {@literal null}. + * @since 1.2.2 */ - public Documenter writeAggregatingDocument(){ - + public Documenter writeAggregatingDocument() { return writeAggregatingDocument(DiagramOptions.defaults(), CanvasOptions.defaults()); } @@ -206,8 +207,9 @@ public class Documenter { * @param options must not be {@literal null}. * @param canvasOptions must not be {@literal null}. * @return the current instance, will never be {@literal null}. + * @since 1.2.2 */ - public Documenter writeAggregatingDocument(DiagramOptions options, CanvasOptions canvasOptions){ + public Documenter writeAggregatingDocument(DiagramOptions options, CanvasOptions canvasOptions) { Assert.notNull(options, "DiagramOptions must not be null!"); Assert.notNull(canvasOptions, "CanvasOptions must not be null!"); @@ -220,7 +222,9 @@ public class Documenter { var componentsDoc = new StringBuilder(); if (outputFolder.contains(componentsFilename)) { - componentsDoc.append(asciidoctor.renderHeadline(2, modules.getSystemName().orElse("Modules"))) + + componentsDoc + .append(asciidoctor.renderHeadline(2, getDefaultedSystemName())) .append(asciidoctor.renderPlantUmlInclude(componentsFilename)) .append(System.lineSeparator()); } @@ -230,25 +234,20 @@ public class Documenter { // Get diagram file name, e.g. module-inventory.puml var fileNamePattern = options.getTargetFileName().orElse(DEFAULT_MODULE_COMPONENTS_FILE); - Assert.isTrue(fileNamePattern.contains("%s"), () -> String.format(INVALID_FILE_NAME_PATTERN, fileNamePattern)); - var filename = String.format(fileNamePattern, it.getName()); - - // Get canvas file name, e.g. module-inventory.adoc + var filename = fileNamePattern.formatted(it.getName()); var canvasFilename = canvasOptions.getTargetFileName(it.getName()); - - // Generate output, e.g.: - /* - == Inventory - plantuml::module-inventory.puml[] - include::module-inventory.adoc[] - */ var content = new StringBuilder(); - content.append((outputFolder.contains(filename) ? asciidoctor.renderPlantUmlInclude(filename) : "")) - .append((outputFolder.contains(canvasFilename) ? asciidoctor.renderGeneralInclude(canvasFilename) : "")); + + content.append(outputFolder.contains(filename) ? asciidoctor.renderPlantUmlInclude(filename) : "") + .append(outputFolder.contains(canvasFilename) ? asciidoctor.renderGeneralInclude(canvasFilename) : ""); + if (!content.isEmpty()) { - content.insert(0, asciidoctor.renderHeadline(2, it.getDisplayName())) + + content + .insert(0, asciidoctor.renderHeadline(2, it.getDisplayName())) .append(System.lineSeparator()); } + return content.toString(); }).collect(Collectors.joining()); @@ -257,7 +256,8 @@ public class Documenter { // Write file to all-docs.adoc if (!allDocs.isBlank()) { - Path file = recreateFile("all-docs.adoc"); + + var file = recreateFile("all-docs.adoc"); try (Writer writer = new FileWriter(file.toFile())) { writer.write(allDocs); @@ -351,9 +351,7 @@ public class Documenter { var fileNamePattern = options.getTargetFileName().orElse(DEFAULT_MODULE_COMPONENTS_FILE); - Assert.isTrue(fileNamePattern.contains("%s"), () -> String.format(INVALID_FILE_NAME_PATTERN, fileNamePattern)); - - return writeViewAsPlantUml(view, String.format(fileNamePattern, module.getName()), options); + return writeViewAsPlantUml(view, fileNamePattern.formatted(module.getName()), options); } /** @@ -584,7 +582,7 @@ public class Documenter { private String createPlantUml(DiagramOptions options) { ComponentView componentView = createComponentView(options); - componentView.setTitle(modules.getSystemName().orElse("Modules")); + componentView.setTitle(getDefaultedSystemName()); addComponentsToView(() -> modules.stream(), componentView, options, it -> {}); @@ -664,6 +662,10 @@ public class Documenter { return (new File("pom.xml").exists() ? "target" : "build").concat("/").concat(DEFAULT_LOCATION); } + private String getDefaultedSystemName() { + return modules.getSystemName().orElse("Modules"); + } + private static record Connection(Element source, Element target) { public static Connection of(Relationship relationship) { return new Connection(relationship.getSource(), relationship.getDestination()); @@ -771,6 +773,9 @@ public class Documenter { * include a {@code %s} placeholder for the module names. */ public DiagramOptions withTargetFileName(String targetFileName) { + + Assert.isTrue(targetFileName.contains("%s"), () -> INVALID_FILE_NAME_PATTERN.formatted(targetFileName)); + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); } @@ -1263,12 +1268,12 @@ public class Documenter { private final String path; - OutputFolder(String path) { - this.path = path; - } + OutputFolder(String path) { + this.path = path; + } boolean contains(String filename) { return Files.exists(Paths.get(path, filename)); } - } + } } diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java index 212c1282..ec6cc624 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java @@ -31,6 +31,7 @@ import org.springframework.modulith.core.ApplicationModule; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.DependencyType; import org.springframework.modulith.docs.Documenter.DiagramOptions; +import org.springframework.util.function.ThrowingConsumer; import com.acme.myproject.Application; @@ -38,6 +39,7 @@ import com.acme.myproject.Application; * Unit tests for {@link Documenter}. * * @author Oliver Drotbohm + * @author Cora Iberkleid */ class DocumenterTest { @@ -73,75 +75,68 @@ class DocumenterTest { } @Test - void customizesOutputLocation() throws IOException { + void customizesOutputLocation() throws Exception { String customOutputFolder = "build/spring-modulith"; Path path = Paths.get(customOutputFolder); - try { + doWith(path, it -> { new Documenter(ApplicationModules.of(Application.class), customOutputFolder).writeModuleCanvases(); assertThat(Files.list(path)).isNotEmpty(); assertThat(path).exists(); - - } finally { - - deleteDirectory(path); - } + }); } @Test // GH-638 - void writesAggregatingDocumentOnlyIfOtherDocsExist() throws IOException { + void createsAggregatingDocumentOnlyIfPartialsExist() throws Exception { - String customOutputFolder = "build/spring-modulith"; - Path path = Paths.get(customOutputFolder); + var customOutputFolder = "build/spring-modulith"; + var path = Paths.get(customOutputFolder); + var documenter = new Documenter(ApplicationModules.of(Application.class), customOutputFolder); - Documenter documenter = new Documenter(ApplicationModules.of(Application.class), customOutputFolder); - - try { + doWith(path, it -> { // all-docs.adoc should be created documenter.writeDocumentation(); - // Count files - long actualFiles; - try (Stream stream = Files.walk(path)) { - actualFiles = stream.filter(Files::isRegularFile).count(); - } - // Expect 2 files per module plus components diagram and all-docs.adoc - long expectedFiles = (documenter.getModules().stream().count() * 2) + 2; - assertThat(actualFiles).isEqualTo(expectedFiles); + // 2 per module (PlantUML + Canvas) + component overview + aggregating doc + var expectedFiles = documenter.getModules().stream().count() * 2 + 2; - Optional optionalPath = Files.walk(path) - .filter(p -> p.getFileName().toString().equals("all-docs.adoc")) - .findFirst(); - assertThat(optionalPath.isPresent()); + // 3 per module (headline + PlantUML + Canvas) + component headline + component PlantUML + var expectedLines = documenter.getModules().stream().count() * 3 + 2; - // Count non-blank lines in all-docs.adoc - long actualLines; - try (Stream lines = Files.lines(optionalPath.get())) { - actualLines = lines.filter(line -> !line.trim().isEmpty()) - .count(); - } - // Expect 3 lines per module and 2 lines for components - long expectedLines = (documenter.getModules().stream().count() * 3) + 2; - assertThat(actualLines).isEqualTo(expectedLines); + assertThat(Files.walk(it).filter(Files::isRegularFile).count()) + .isEqualTo(expectedFiles); - // all-docs.adoc should not be created - deleteDirectoryContents(path); + assertThat(path.resolve("all-docs.adoc")).exists().satisfies(doc -> { + assertThat(Files.lines(doc) + .filter(line -> !line.trim().isEmpty()) + .count()).isEqualTo(expectedLines); + }); + }); + } + + @Test // GH-638 + void doesNotCreateAggregatingDocumentIfNoPartialsExist() throws Exception { + + var customOutputFolder = "build/spring-modulith"; + var path = Paths.get(customOutputFolder); + + doWith(path, it -> { + + var documenter = new Documenter(ApplicationModules.of(Application.class), customOutputFolder) + .writeDocumentation(); + + deleteDirectoryContents(it); documenter.writeAggregatingDocument(); - optionalPath = Files.walk(path) - .filter(p -> p.getFileName().toString().equals("all-docs.adoc")) - .findFirst(); - assertThat(optionalPath.isEmpty()); + var aggregatingDoc = path.resolve("all-docs.adoc"); - } finally { - - deleteDirectory(path); - } + assertThat(aggregatingDoc).doesNotExist(); + }); } private static void deleteDirectoryContents(Path path) throws IOException { @@ -161,4 +156,13 @@ class DocumenterTest { deleteDirectoryContents(path); Files.deleteIfExists(path); } + + private static void doWith(Path path, ThrowingConsumer consumer) throws Exception { + + try { + consumer.accept(path); + } finally { + deleteDirectory(path); + } + } } diff --git a/src/docs/antora/modules/ROOT/pages/documentation.adoc b/src/docs/antora/modules/ROOT/pages/documentation.adoc index 05db0059..376b2232 100644 --- a/src/docs/antora/modules/ROOT/pages/documentation.adoc +++ b/src/docs/antora/modules/ROOT/pages/documentation.adoc @@ -308,7 +308,8 @@ Requires the usage of the `spring-boot-configuration-processor` artifact to extr [[aggregating-document]] == Generating an Aggregating Document -The aggregating document can be generated by calling `Documenter.writeAggregatingDocument()`: +When using `Documenter.writeDocumentation(…)` an `all-docs.adoc` file will be generated, linking all generated diagrams and Application Module Canvases. +We can manually generate the aggregating document by calling `Documenter.writeAggregatingDocument()`: .Generating an aggregating document using `Documenter` [tabs] @@ -325,7 +326,7 @@ class DocumentationTests { void writeDocumentationSnippets() { new Documenter(modules) - .writeAggregatingDocument(); + .writeAggregatingDocument(); } } ---- @@ -346,4 +347,5 @@ class DocumentationTests { ---- ====== -The aggregating document will include any existing application module component diagrams and application module canvases. If there are none, then this method will not produce an output file. \ No newline at end of file +The aggregating document will include any existing application module component diagrams and application module canvases. +If there are none, then this method will not produce an output file.