Skip to content

GH-638 - Create aggregating Asciidoc document including all files generated #712

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading