Skip to content

Commit

Permalink
GH-713 - Create aggregating Asciidoc document including all files gen…
Browse files Browse the repository at this point in the history
…erated.
  • Loading branch information
ciberkleid authored and odrotbohm committed Jul 8, 2024
1 parent bd2e496 commit b94d13b
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,19 @@ public String renderBeanReferences(ApplicationModule module) {

return bullets.isBlank() ? "None" : bullets;
}

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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,89 @@ public Documenter writeDocumentation() {
public Documenter writeDocumentation(DiagramOptions options, CanvasOptions canvasOptions) {

return writeModulesAsPlantUml(options)
.writeIndividualModulesAsPlantUml(options) //
.writeModuleCanvases(canvasOptions);
.writeIndividualModulesAsPlantUml(options)
.writeModuleCanvases(canvasOptions)
.writeAggregatingDocument(options, canvasOptions);
}

/**
* Writes aggregating document called '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}.
*/
public Documenter writeAggregatingDocument(){

return writeAggregatingDocument(DiagramOptions.defaults(), CanvasOptions.defaults());
}

/**
* Writes aggregating document called 'all-docs.adoc' that includes any existing component diagrams and canvases.
*
* @param options must not be {@literal null}.
* @param canvasOptions must not be {@literal null}.
* @return the current instance, will never be {@literal null}.
*/
public Documenter writeAggregatingDocument(DiagramOptions options, CanvasOptions canvasOptions){

Assert.notNull(options, "DiagramOptions must not be null!");
Assert.notNull(canvasOptions, "CanvasOptions must not be null!");

var asciidoctor = Asciidoctor.withJavadocBase(modules, canvasOptions.getApiBase());
var outputFolder = new OutputFolder(this.outputFolder);

// Get file name for module overview diagram
var componentsFilename = options.getTargetFileName().orElse(DEFAULT_COMPONENTS_FILE);
var componentsDoc = new StringBuilder();

if (outputFolder.contains(componentsFilename)) {
componentsDoc.append(asciidoctor.renderHeadline(2, modules.getSystemName().orElse("Modules")))
.append(asciidoctor.renderPlantUmlInclude(componentsFilename))
.append(System.lineSeparator());
}

// Get file names for individual module diagrams and canvases
var moduleDocs = modules.stream().map(it -> {

// 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 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) : ""));
if (!content.isEmpty()) {
content.insert(0, asciidoctor.renderHeadline(2, it.getDisplayName()))
.append(System.lineSeparator());
}
return content.toString();

}).collect(Collectors.joining());

var allDocs = componentsDoc.append(moduleDocs).toString();

// Write file to all-docs.adoc
if (!allDocs.isBlank()) {
Path file = recreateFile("all-docs.adoc");

try (Writer writer = new FileWriter(file.toFile())) {
writer.write(allDocs);
} catch (IOException o_O) {
throw new RuntimeException(o_O);
}
}

return this;
}

/**
Expand Down Expand Up @@ -1177,4 +1258,17 @@ protected void startContainerBoundary(ModelView view, Container container, Inden
@Override
protected void endContainerBoundary(ModelView view, IndentingWriter writer) {};
};

private static class OutputFolder {

private final String path;

OutputFolder(String path) {
this.path = path;
}

boolean contains(String filename) {
return Files.exists(Paths.get(path, filename));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModule;
Expand Down Expand Up @@ -86,10 +87,78 @@ void customizesOutputLocation() throws IOException {

} finally {

Files.walk(path)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
deleteDirectory(path);
}
}

@Test // GH-638
void writesAggregatingDocumentOnlyIfOtherDocsExist() throws IOException {

String customOutputFolder = "build/spring-modulith";
Path path = Paths.get(customOutputFolder);

Documenter documenter = new Documenter(ApplicationModules.of(Application.class), customOutputFolder);

try {

// all-docs.adoc should be created
documenter.writeDocumentation();

// Count files
long actualFiles;
try (Stream<Path> 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);

Optional<Path> optionalPath = Files.walk(path)
.filter(p -> p.getFileName().toString().equals("all-docs.adoc"))
.findFirst();
assertThat(optionalPath.isPresent());

// Count non-blank lines in all-docs.adoc
long actualLines;
try (Stream<String> 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);

// all-docs.adoc should not be created
deleteDirectoryContents(path);

documenter.writeAggregatingDocument();

optionalPath = Files.walk(path)
.filter(p -> p.getFileName().toString().equals("all-docs.adoc"))
.findFirst();
assertThat(optionalPath.isEmpty());

} finally {

deleteDirectory(path);
}
}

private static void deleteDirectoryContents(Path path) throws IOException {

if (Files.exists(path) && Files.isDirectory(path)) {
try (Stream<Path> walk = Files.walk(path)) {
walk.sorted(Comparator.reverseOrder())
.filter(p -> !p.equals(path)) // Ensure we don't delete the directory itself
.map(Path::toFile)
.forEach(File::delete);
}
}
}

private static void deleteDirectory(Path path) throws IOException {

deleteDirectoryContents(path);
Files.deleteIfExists(path);
}
}
45 changes: 45 additions & 0 deletions src/docs/antora/modules/ROOT/pages/documentation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Spring Modulith's `Documenter` abstraction can produce two different kinds of sn
* C4 and UML component diagrams describing the relationships between the individual application modules
* A so-called __Application Module Canvas__, a tabular overview about the module and the most relevant elements in those (Spring beans, aggregate roots, events published and listened to as well as configuration properties).

Additionally, `Documenter` can produce an aggregating Asciidoc file that includes all existing component diagrams and canvases.

[[component-diagrams]]
== Generating Application Module Component diagrams

Expand Down Expand Up @@ -302,3 +304,46 @@ This will detect component stereotypes defined by https://github.com/xmolecules/
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener`, jMolecules' `@DomainEventHandler` or beans implementing `ApplicationListener`.
* __Configuration properties__ -- Spring Boot Configuration properties exposed by the application module.
Requires the usage of the `spring-boot-configuration-processor` artifact to extract the metadata attached to the properties.

[[aggregating-document]]
== Generating an Aggregating Document

The aggregating document can be generated by calling `Documenter.writeAggregatingDocument()`:

.Generating an aggregating document using `Documenter`
[tabs]
======
Java::
+
[source, java, role="primary"]
----
class DocumentationTests {
ApplicationModules modules = ApplicationModules.of(Application.class);
@Test
void writeDocumentationSnippets() {
new Documenter(modules)
.writeAggregatingDocument();
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
class DocumentationTests {
private val modules = ApplicationModules.of(Application::class)
@Test
fun writeDocumentationSnippets() {
Documenter(modules)
.writeAggregatingDocument()
}
}
----
======

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.

0 comments on commit b94d13b

Please sign in to comment.