GH-713 - Polishing.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user