From 745ca82471923fd96053c7222e4e0983692e4bdb Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Sun, 12 Jul 2026 13:25:24 +0300 Subject: [PATCH] persistent openapi specification --- docs/asciidoc/modules/openapi.adoc | 9 ++- .../main/java/io/jooby/maven/OpenAPIMojo.java | 76 ++++++++++++++++++- .../java/io/jooby/maven/OpenAPIMojoTest.java | 57 ++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 modules/jooby-maven-plugin/src/test/java/io/jooby/maven/OpenAPIMojoTest.java diff --git a/docs/asciidoc/modules/openapi.adoc b/docs/asciidoc/modules/openapi.adoc index 4650e3c61f..7d986bdd76 100644 --- a/docs/asciidoc/modules/openapi.adoc +++ b/docs/asciidoc/modules/openapi.adoc @@ -128,7 +128,14 @@ To avoid this behaviour you can specify maven build phase which suits your needs |`openapi.yaml` |Set openAPI template file path. -|=== +|`copyOpenApiSpecTo` +| +|Copy the generated OpenAPI spec to the given file in the project repository. The format is +determined by the file extension: `.yaml`, `.yml` or `.json`. Example: + + ${project.basedir}/docs/openapi.yaml + +|`=== === Usage diff --git a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java index 9c4bf4caef..2c4be14630 100644 --- a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java +++ b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java @@ -10,9 +10,14 @@ import static org.apache.maven.plugins.annotations.ResolutionScope.COMPILE_PLUS_RUNTIME; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -53,6 +58,9 @@ public class OpenAPIMojo extends BaseMojo { @Parameter private List adoc; + @Parameter(property = "openAPI.copyOpenApiSpecTo") + private File copyOpenApiSpecTo; + @Override protected void doExecute(List projects, String mainClass) throws Exception { ClassLoader classLoader = createClassLoader(projects); @@ -84,10 +92,48 @@ protected void doExecute(List projects, String mainClass) throws E var result = tool.generate(mainClass); var adocPath = ofNullable(adoc).orElse(List.of()).stream().map(File::toPath).toList(); + var written = new ArrayList(); for (var format : OpenAPIGenerator.Format.values()) { - tool.export(result, format, Map.of("adoc", adocPath)) - .forEach(output -> getLog().info(" writing: " + output)); + written.addAll(tool.export(result, format, Map.of("adoc", adocPath))); + } + written.forEach(output -> getLog().info(" writing: " + output)); + + if (copyOpenApiSpecTo != null) { + var destination = copyOpenApiSpecTo.toPath(); + copySpec(written, destination); + getLog().info(" copying: " + destination); + } + } + + public static void copySpec(List written, Path destination) throws IOException { + var format = specFormat(destination); + var source = + written.stream() + .filter(path -> path.getFileName().toString().endsWith("." + format.extension())) + .findFirst() + .orElseThrow( + () -> + new IOException( + String.format( + "OpenAPI %s output not found for copyOpenApiSpecTo: %s", + format.name(), destination))); + var parent = destination.getParent(); + if (parent != null) { + Files.createDirectories(parent); } + Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); + } + + public static OpenAPIGenerator.Format specFormat(Path destination) { + var name = destination.getFileName().toString().toLowerCase(Locale.ROOT); + if (name.endsWith(".json")) { + return OpenAPIGenerator.Format.JSON; + } + if (name.endsWith(".yaml") || name.endsWith(".yml")) { + return OpenAPIGenerator.Format.YAML; + } + throw new IllegalArgumentException( + "copyOpenApiSpecTo must end with .yaml, .yml or .json: " + destination); } private Optional trim(String value) { @@ -186,4 +232,30 @@ public void setJavadoc(String javadoc) { public String getJavadoc() { return javadoc; } + + /** + * Copy the generated OpenAPI spec to the given file. The format is determined by the file + * extension: .yaml, .yml or .json. + * + * @return Destination file. + */ + public @Nullable File getCopyOpenApiSpecTo() { + return copyOpenApiSpecTo; + } + + /** + * Copy the generated OpenAPI spec to the given file. The format is determined by the file + * extension: .yaml, .yml or .json. + * + *

Example: + * + *

{@code
+   * ${project.basedir}/docs/openapi.yaml
+   * }
+ * + * @param copyOpenApiSpecTo Destination file. + */ + public void setCopyOpenApiSpecTo(@Nullable File copyOpenApiSpecTo) { + this.copyOpenApiSpecTo = copyOpenApiSpecTo; + } } diff --git a/modules/jooby-maven-plugin/src/test/java/io/jooby/maven/OpenAPIMojoTest.java b/modules/jooby-maven-plugin/src/test/java/io/jooby/maven/OpenAPIMojoTest.java new file mode 100644 index 0000000000..e5c138a431 --- /dev/null +++ b/modules/jooby-maven-plugin/src/test/java/io/jooby/maven/OpenAPIMojoTest.java @@ -0,0 +1,57 @@ +package io.jooby.maven; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.jooby.openapi.OpenAPIGenerator; + +public class OpenAPIMojoTest { + + @Test + public void specFormat() { + assertEquals( + OpenAPIGenerator.Format.YAML, OpenAPIMojo.specFormat(Path.of("docs/openapi.yaml"))); + assertEquals( + OpenAPIGenerator.Format.YAML, OpenAPIMojo.specFormat(Path.of("docs/openapi.yml"))); + assertEquals( + OpenAPIGenerator.Format.JSON, OpenAPIMojo.specFormat(Path.of("docs/openapi.json"))); + assertThrows( + IllegalArgumentException.class, () -> OpenAPIMojo.specFormat(Path.of("docs/openapi.txt"))); + } + + @Test + public void copyYamlSpec(@TempDir Path tempDir) throws Exception { + var outputDir = tempDir.resolve("classes/myapp"); + Files.createDirectories(outputDir); + var source = outputDir.resolve("App.yaml"); + Files.writeString(source, "openapi: 3.0.1"); + + var destination = tempDir.resolve("docs/openapi.yml"); + OpenAPIMojo.copySpec(List.of(source), destination); + + assertTrue(Files.isRegularFile(destination)); + assertEquals(Files.readString(source), Files.readString(destination)); + } + + @Test + public void copyJsonSpec(@TempDir Path tempDir) throws Exception { + var outputDir = tempDir.resolve("classes/myapp"); + Files.createDirectories(outputDir); + var source = outputDir.resolve("App.json"); + Files.writeString(source, "{\"openapi\":\"3.0.1\"}"); + + var destination = tempDir.resolve("docs/openapi.json"); + OpenAPIMojo.copySpec(List.of(source), destination); + + assertTrue(Files.isRegularFile(destination)); + assertEquals(Files.readString(source), Files.readString(destination)); + } +}