GH-713 - Polishing.

This commit is contained in:
Oliver Drotbohm
2024-07-08 17:10:15 +02:00
parent b94d13b717
commit 570f347cd8
4 changed files with 88 additions and 80 deletions

View File

@@ -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();
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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<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);
// 2 per module (PlantUML + Canvas) + component overview + aggregating doc
var expectedFiles = documenter.getModules().stream().count() * 2 + 2;
Optional<Path> 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<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);
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<Path> consumer) throws Exception {
try {
consumer.accept(path);
} finally {
deleteDirectory(path);
}
}
}

View File

@@ -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.
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.