Skip to content

Commit e8002e8

Browse files
committed
GH-638 - Create aggregating Asciidoc document including all files generated
1 parent debe021 commit e8002e8

File tree

4 files changed

+231
-7
lines changed

4 files changed

+231
-7
lines changed

spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.regex.Matcher;
2323
import java.util.regex.Pattern;
2424
import java.util.stream.Collectors;
25+
import java.util.stream.IntStream;
2526
import java.util.stream.Stream;
2627

2728
import org.springframework.lang.Nullable;
@@ -367,4 +368,20 @@ public String renderBeanReferences(ApplicationModule module) {
367368

368369
return bullets.isBlank() ? "None" : bullets;
369370
}
371+
372+
public String renderHeadline(int i, String modules) {
373+
374+
return "=".repeat(i) + " " + modules + System.lineSeparator();
375+
}
376+
377+
public String renderPlantUmlInclude(String componentsFilename) {
378+
379+
return "plantuml::" + componentsFilename + "[]" + System.lineSeparator();
380+
}
381+
382+
public String renderGeneralInclude(String componentsFilename) {
383+
384+
return "include::" + componentsFilename + "[]" + System.lineSeparator();
385+
}
386+
370387
}

spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,89 @@ public Documenter writeDocumentation() {
184184
public Documenter writeDocumentation(DiagramOptions options, CanvasOptions canvasOptions) {
185185

186186
return writeModulesAsPlantUml(options)
187-
.writeIndividualModulesAsPlantUml(options) //
188-
.writeModuleCanvases(canvasOptions);
187+
.writeIndividualModulesAsPlantUml(options)
188+
.writeModuleCanvases(canvasOptions)
189+
.writeAggregatingDocument(options, canvasOptions);
190+
}
191+
192+
/**
193+
* Writes aggregating document called 'all-docs.adoc' that includes any existing component diagrams and canvases.
194+
* using {@link DiagramOptions#defaults()} and {@link CanvasOptions#defaults()}.
195+
*
196+
* @return the current instance, will never be {@literal null}.
197+
*/
198+
public Documenter writeAggregatingDocument(){
199+
200+
return writeAggregatingDocument(DiagramOptions.defaults(), CanvasOptions.defaults());
201+
}
202+
203+
/**
204+
* Writes aggregating document called 'all-docs.adoc' that includes any existing component diagrams and canvases.
205+
*
206+
* @param options must not be {@literal null}.
207+
* @param canvasOptions must not be {@literal null}.
208+
* @return the current instance, will never be {@literal null}.
209+
*/
210+
public Documenter writeAggregatingDocument(DiagramOptions options, CanvasOptions canvasOptions){
211+
212+
Assert.notNull(options, "DiagramOptions must not be null!");
213+
Assert.notNull(canvasOptions, "CanvasOptions must not be null!");
214+
215+
var asciidoctor = Asciidoctor.withJavadocBase(modules, canvasOptions.getApiBase());
216+
var outputFolder = new OutputFolder(this.outputFolder);
217+
218+
// Get file name for module overview diagram
219+
var componentsFilename = options.getTargetFileName().orElse(DEFAULT_COMPONENTS_FILE);
220+
var componentsDoc = new StringBuilder();
221+
222+
if (outputFolder.contains(componentsFilename)) {
223+
componentsDoc.append(asciidoctor.renderHeadline(2, modules.getSystemName().orElse("Modules")))
224+
.append(asciidoctor.renderPlantUmlInclude(componentsFilename))
225+
.append(System.lineSeparator());
226+
}
227+
228+
// Get file names for individual module diagrams and canvases
229+
var moduleDocs = modules.stream().map(it -> {
230+
231+
// Get diagram file name, e.g. module-inventory.puml
232+
var fileNamePattern = options.getTargetFileName().orElse(DEFAULT_MODULE_COMPONENTS_FILE);
233+
Assert.isTrue(fileNamePattern.contains("%s"), () -> String.format(INVALID_FILE_NAME_PATTERN, fileNamePattern));
234+
var filename = String.format(fileNamePattern, it.getName());
235+
236+
// Get canvas file name, e.g. module-inventory.adoc
237+
var canvasFilename = canvasOptions.getTargetFileName(it.getName());
238+
239+
// Generate output, e.g.:
240+
/*
241+
== Inventory
242+
plantuml::module-inventory.puml[]
243+
include::module-inventory.adoc[]
244+
*/
245+
var content = new StringBuilder();
246+
content.append((outputFolder.contains(filename) ? asciidoctor.renderPlantUmlInclude(filename) : ""))
247+
.append((outputFolder.contains(canvasFilename) ? asciidoctor.renderGeneralInclude(canvasFilename) : ""));
248+
if (!content.isEmpty()) {
249+
content.insert(0, asciidoctor.renderHeadline(2, it.getDisplayName()))
250+
.append(System.lineSeparator());
251+
}
252+
return content.toString();
253+
254+
}).collect(Collectors.joining());
255+
256+
var allDocs = componentsDoc.append(moduleDocs).toString();
257+
258+
// Write file to all-docs.adoc
259+
if (!allDocs.isBlank()) {
260+
Path file = recreateFile("all-docs.adoc");
261+
262+
try (Writer writer = new FileWriter(file.toFile())) {
263+
writer.write(allDocs);
264+
} catch (IOException o_O) {
265+
throw new RuntimeException(o_O);
266+
}
267+
}
268+
269+
return this;
189270
}
190271

191272
/**
@@ -1177,4 +1258,17 @@ protected void startContainerBoundary(ModelView view, Container container, Inden
11771258
@Override
11781259
protected void endContainerBoundary(ModelView view, IndentingWriter writer) {};
11791260
};
1261+
1262+
private static class OutputFolder {
1263+
1264+
private final String path;
1265+
1266+
OutputFolder(String path) {
1267+
this.path = path;
1268+
}
1269+
1270+
boolean contains(String filename) {
1271+
return Files.exists(Paths.get(path, filename));
1272+
}
1273+
}
11801274
}

spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.modulith.docs;
1717

18-
import static org.assertj.core.api.Assertions.*;
18+
import static org.assertj.core.api.Assertions.*;
1919

2020
import java.io.File;
2121
import java.io.IOException;
@@ -24,6 +24,7 @@
2424
import java.nio.file.Paths;
2525
import java.util.Comparator;
2626
import java.util.Optional;
27+
import java.util.stream.Stream;
2728

2829
import org.junit.jupiter.api.Test;
2930
import org.springframework.modulith.core.ApplicationModule;
@@ -86,10 +87,77 @@ void customizesOutputLocation() throws IOException {
8687

8788
} finally {
8889

89-
Files.walk(path)
90-
.sorted(Comparator.reverseOrder())
91-
.map(Path::toFile)
92-
.forEach(File::delete);
90+
deleteDirectory(path);
9391
}
9492
}
93+
94+
@Test // GH-638
95+
void writesAggregatingDocumentOnlyIfOtherDocsExist() throws IOException {
96+
97+
String customOutputFolder = "build/spring-modulith";
98+
Path path = Paths.get(customOutputFolder);
99+
100+
Documenter documenter = new Documenter(ApplicationModules.of(Application.class), customOutputFolder);
101+
102+
try {
103+
104+
// all-docs.adoc should be created
105+
documenter.writeDocumentation();
106+
107+
// Count files
108+
long actualFiles;
109+
try (Stream<Path> stream = Files.walk(path)) {
110+
actualFiles = stream.filter(Files::isRegularFile).count();
111+
}
112+
// Expect 2 files per module plus components diagram and all-docs.adoc
113+
long expectedFiles = (documenter.getModules().stream().count() * 2) + 2;
114+
assertThat(actualFiles).isEqualTo(expectedFiles);
115+
116+
Optional<Path> optionalPath = Files.walk(path)
117+
.filter(p -> p.getFileName().toString().equals("all-docs.adoc"))
118+
.findFirst();
119+
assertThat(optionalPath.isPresent());
120+
121+
// Count non-blank lines in all-docs.adoc
122+
long actualLines;
123+
try (Stream<String> lines = Files.lines(optionalPath.get())) {
124+
actualLines = lines.filter(line -> !line.trim().isEmpty())
125+
.count();
126+
}
127+
// Expect 3 lines per module and 2 lines for components
128+
long expectedLines = (documenter.getModules().stream().count() * 3) + 2;
129+
assertThat(actualLines).isEqualTo(expectedLines);
130+
131+
// all-docs.adoc should not be created
132+
deleteDirectoryContents(path);
133+
134+
documenter.writeAggregatingDocument();
135+
136+
optionalPath = Files.walk(path)
137+
.filter(p -> p.getFileName().toString().equals("all-docs.adoc"))
138+
.findFirst();
139+
assertThat(optionalPath.isEmpty());
140+
141+
} finally {
142+
143+
deleteDirectory(path);
144+
}
145+
146+
}
147+
148+
private static void deleteDirectoryContents(Path path) throws IOException {
149+
if (Files.exists(path) && Files.isDirectory(path)) {
150+
try (Stream<Path> walk = Files.walk(path)) {
151+
walk.sorted(Comparator.reverseOrder())
152+
.filter(p -> !p.equals(path)) // Ensure we don't delete the directory itself
153+
.map(Path::toFile)
154+
.forEach(File::delete);
155+
}
156+
}
157+
}
158+
159+
private static void deleteDirectory(Path path) throws IOException {
160+
deleteDirectoryContents(path);
161+
Files.deleteIfExists(path);
162+
}
95163
}

src/docs/antora/modules/ROOT/pages/documentation.adoc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Spring Modulith's `Documenter` abstraction can produce two different kinds of sn
77
* C4 and UML component diagrams describing the relationships between the individual application modules
88
* 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).
99

10+
Additionally, `Documenter` can produce an aggregating Asciidoc file that includes all existing component diagrams and canvases.
11+
1012
[[component-diagrams]]
1113
== Generating Application Module Component diagrams
1214

@@ -302,3 +304,46 @@ This will detect component stereotypes defined by https://github.com/xmolecules/
302304
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener`, jMolecules' `@DomainEventHandler` or beans implementing `ApplicationListener`.
303305
* __Configuration properties__ -- Spring Boot Configuration properties exposed by the application module.
304306
Requires the usage of the `spring-boot-configuration-processor` artifact to extract the metadata attached to the properties.
307+
308+
[[aggregating-document]]
309+
== Generating an Aggregating Document
310+
311+
The aggregating document can be generated by calling `Documenter.writeAggregatingDocument()`:
312+
313+
.Generating an aggregating document using `Documenter`
314+
[tabs]
315+
======
316+
Java::
317+
+
318+
[source, java, role="primary"]
319+
----
320+
class DocumentationTests {
321+
322+
ApplicationModules modules = ApplicationModules.of(Application.class);
323+
324+
@Test
325+
void writeDocumentationSnippets() {
326+
327+
new Documenter(modules)
328+
.writeAggregatingDocument();
329+
}
330+
}
331+
----
332+
Kotlin::
333+
+
334+
[source, kotlin, role="secondary"]
335+
----
336+
class DocumentationTests {
337+
338+
private val modules = ApplicationModules.of(Application::class)
339+
340+
@Test
341+
fun writeDocumentationSnippets() {
342+
Documenter(modules)
343+
.writeAggregatingDocument()
344+
}
345+
}
346+
----
347+
======
348+
349+
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 commit comments

Comments
 (0)