Skip to content

Commit 4aff826

Browse files
authored
Merge pull request #3977 from kliushnichenko/feat/persistent-openapi-spec
persistent openapi specification
2 parents c6a3e5e + 3cb392d commit 4aff826

4 files changed

Lines changed: 224 additions & 5 deletions

File tree

docs/asciidoc/modules/openapi.adoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,21 @@ To avoid this behaviour you can specify maven build phase which suits your needs
128128
|`openapi.yaml`
129129
|Set openAPI template file path.
130130

131+
|`copyOpenApiSpecTo`
132+
|
133+
|Copy the generated OpenAPI spec to the given file in the project repository. The format is
134+
determined by the file extension: `.yaml`, `.yml` or `.json`. Example:
135+
136+
.Maven:
137+
138+
<copyOpenApiSpecTo>${project.basedir}/docs/openapi.yaml</copyOpenApiSpecTo>
139+
140+
.Gradle:
141+
142+
{
143+
copyOpenApiSpecTo = file("$projectDir/docs/openapi.yaml")
144+
}
145+
131146
|===
132147

133148
=== Usage

modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
import org.jspecify.annotations.Nullable;
1616

1717
import java.io.File;
18+
import java.io.IOException;
19+
import java.nio.file.Files;
1820
import java.nio.file.Path;
21+
import java.nio.file.StandardCopyOption;
22+
import java.util.ArrayList;
1923
import java.util.List;
24+
import java.util.Locale;
2025
import java.util.Map;
2126
import java.util.Optional;
2227

@@ -44,6 +49,8 @@ public class OpenAPITask extends BaseTask {
4449

4550
private String javadoc;
4651

52+
private File copyOpenApiSpecTo;
53+
4754
/**
4855
* Creates an OpenAPI task.
4956
*/
@@ -58,7 +65,7 @@ public OpenAPITask() {}
5865
public void generate() throws Throwable {
5966
List<Project> projects = getProjects();
6067

61-
String mainClass = Optional.ofNullable(this.mainClass)
68+
String mainClass = ofNullable(this.mainClass)
6269
.orElseGet(() -> computeMainClassName(projects));
6370
var sources = projects.stream()
6471
.flatMap(project -> {
@@ -94,10 +101,48 @@ public void generate() throws Throwable {
94101
OpenAPI result = tool.generate(mainClass);
95102

96103
var adocPath = ofNullable(adoc).orElse(List.of()).stream().map(File::toPath).toList();
104+
var written = new ArrayList<Path>();
97105
for (var format : OpenAPIGenerator.Format.values()) {
98-
tool.export(result, format, Map.of("adoc", adocPath))
99-
.forEach(output -> getLogger().info(" writing: " + output));
106+
written.addAll(tool.export(result, format, Map.of("adoc", adocPath)));
107+
}
108+
written.forEach(output -> getLogger().info(" writing: " + output));
109+
110+
if (copyOpenApiSpecTo != null) {
111+
var destination = copyOpenApiSpecTo.toPath();
112+
copySpec(written, destination);
113+
getLogger().info(" copying: " + destination);
114+
}
115+
}
116+
117+
static void copySpec(List<Path> written, Path destination) throws IOException {
118+
var format = specFormat(destination);
119+
var source =
120+
written.stream()
121+
.filter(path -> path.getFileName().toString().endsWith("." + format.extension()))
122+
.findFirst()
123+
.orElseThrow(
124+
() ->
125+
new IOException(
126+
String.format(
127+
"OpenAPI %s output not found for copyOpenApiSpecTo: %s",
128+
format.name(), destination)));
129+
var parent = destination.getParent();
130+
if (parent != null) {
131+
Files.createDirectories(parent);
100132
}
133+
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
134+
}
135+
136+
static OpenAPIGenerator.Format specFormat(Path destination) {
137+
var name = destination.getFileName().toString().toLowerCase(Locale.ROOT);
138+
if (name.endsWith(".json")) {
139+
return OpenAPIGenerator.Format.JSON;
140+
}
141+
if (name.endsWith(".yaml") || name.endsWith(".yml")) {
142+
return OpenAPIGenerator.Format.YAML;
143+
}
144+
throw new IllegalArgumentException(
145+
"copyOpenApiSpecTo must end with .yaml, .yml or .json: " + destination);
101146
}
102147

103148
/**
@@ -243,6 +288,36 @@ public void setAdoc(List<File> adoc) {
243288
this.adoc = adoc;
244289
}
245290

291+
/**
292+
* Copy the generated OpenAPI spec to the given file. The format is determined by the file
293+
* extension: <code>.yaml</code>, <code>.yml</code> or <code>.json</code>.
294+
*
295+
* @return Destination file.
296+
*/
297+
@Input
298+
@org.gradle.api.tasks.Optional
299+
public @Nullable File getCopyOpenApiSpecTo() {
300+
return copyOpenApiSpecTo;
301+
}
302+
303+
/**
304+
* Copy the generated OpenAPI spec to the given file. The format is determined by the file
305+
* extension: <code>.yaml</code>, <code>.yml</code> or <code>.json</code>.
306+
*
307+
* <p>Example:
308+
*
309+
* <pre>{@code
310+
* openAPI {
311+
* copyOpenApiSpecTo = file("$projectDir/docs/openapi.yaml")
312+
* }
313+
* }</pre>
314+
*
315+
* @param copyOpenApiSpecTo Destination file.
316+
*/
317+
public void setCopyOpenApiSpecTo(@Nullable File copyOpenApiSpecTo) {
318+
this.copyOpenApiSpecTo = copyOpenApiSpecTo;
319+
}
320+
246321
private Optional<String> trim(String value) {
247322
if (value == null || value.trim().isEmpty()) {
248323
return Optional.empty();

modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
import static org.apache.maven.plugins.annotations.ResolutionScope.COMPILE_PLUS_RUNTIME;
1111

1212
import java.io.File;
13+
import java.io.IOException;
14+
import java.nio.file.Files;
1315
import java.nio.file.Path;
1416
import java.nio.file.Paths;
17+
import java.nio.file.StandardCopyOption;
18+
import java.util.ArrayList;
1519
import java.util.List;
20+
import java.util.Locale;
1621
import java.util.Map;
1722
import java.util.Optional;
1823

@@ -53,6 +58,9 @@ public class OpenAPIMojo extends BaseMojo {
5358

5459
@Parameter private List<File> adoc;
5560

61+
@Parameter(property = "openAPI.copyOpenApiSpecTo")
62+
private File copyOpenApiSpecTo;
63+
5664
@Override
5765
protected void doExecute(List<MavenProject> projects, String mainClass) throws Exception {
5866
ClassLoader classLoader = createClassLoader(projects);
@@ -84,10 +92,48 @@ protected void doExecute(List<MavenProject> projects, String mainClass) throws E
8492
var result = tool.generate(mainClass);
8593

8694
var adocPath = ofNullable(adoc).orElse(List.of()).stream().map(File::toPath).toList();
95+
var written = new ArrayList<Path>();
8796
for (var format : OpenAPIGenerator.Format.values()) {
88-
tool.export(result, format, Map.of("adoc", adocPath))
89-
.forEach(output -> getLog().info(" writing: " + output));
97+
written.addAll(tool.export(result, format, Map.of("adoc", adocPath)));
98+
}
99+
written.forEach(output -> getLog().info(" writing: " + output));
100+
101+
if (copyOpenApiSpecTo != null) {
102+
var destination = copyOpenApiSpecTo.toPath();
103+
copySpec(written, destination);
104+
getLog().info(" copying: " + destination);
105+
}
106+
}
107+
108+
public static void copySpec(List<Path> written, Path destination) throws IOException {
109+
var format = specFormat(destination);
110+
var source =
111+
written.stream()
112+
.filter(path -> path.getFileName().toString().endsWith("." + format.extension()))
113+
.findFirst()
114+
.orElseThrow(
115+
() ->
116+
new IOException(
117+
String.format(
118+
"OpenAPI %s output not found for copyOpenApiSpecTo: %s",
119+
format.name(), destination)));
120+
var parent = destination.getParent();
121+
if (parent != null) {
122+
Files.createDirectories(parent);
90123
}
124+
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
125+
}
126+
127+
public static OpenAPIGenerator.Format specFormat(Path destination) {
128+
var name = destination.getFileName().toString().toLowerCase(Locale.ROOT);
129+
if (name.endsWith(".json")) {
130+
return OpenAPIGenerator.Format.JSON;
131+
}
132+
if (name.endsWith(".yaml") || name.endsWith(".yml")) {
133+
return OpenAPIGenerator.Format.YAML;
134+
}
135+
throw new IllegalArgumentException(
136+
"copyOpenApiSpecTo must end with .yaml, .yml or .json: " + destination);
91137
}
92138

93139
private Optional<String> trim(String value) {
@@ -186,4 +232,30 @@ public void setJavadoc(String javadoc) {
186232
public String getJavadoc() {
187233
return javadoc;
188234
}
235+
236+
/**
237+
* Copy the generated OpenAPI spec to the given file. The format is determined by the file
238+
* extension: <code>.yaml</code>, <code>.yml</code> or <code>.json</code>.
239+
*
240+
* @return Destination file.
241+
*/
242+
public @Nullable File getCopyOpenApiSpecTo() {
243+
return copyOpenApiSpecTo;
244+
}
245+
246+
/**
247+
* Copy the generated OpenAPI spec to the given file. The format is determined by the file
248+
* extension: <code>.yaml</code>, <code>.yml</code> or <code>.json</code>.
249+
*
250+
* <p>Example:
251+
*
252+
* <pre>{@code
253+
* <copyOpenApiSpecTo>${project.basedir}/docs/openapi.yaml</copyOpenApiSpecTo>
254+
* }</pre>
255+
*
256+
* @param copyOpenApiSpecTo Destination file.
257+
*/
258+
public void setCopyOpenApiSpecTo(@Nullable File copyOpenApiSpecTo) {
259+
this.copyOpenApiSpecTo = copyOpenApiSpecTo;
260+
}
189261
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.jooby.maven;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.util.List;
10+
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.io.TempDir;
13+
14+
import io.jooby.openapi.OpenAPIGenerator;
15+
16+
public class OpenAPIMojoTest {
17+
18+
@Test
19+
public void specFormat() {
20+
assertEquals(
21+
OpenAPIGenerator.Format.YAML, OpenAPIMojo.specFormat(Path.of("docs/openapi.yaml")));
22+
assertEquals(
23+
OpenAPIGenerator.Format.YAML, OpenAPIMojo.specFormat(Path.of("docs/openapi.yml")));
24+
assertEquals(
25+
OpenAPIGenerator.Format.JSON, OpenAPIMojo.specFormat(Path.of("docs/openapi.json")));
26+
assertThrows(
27+
IllegalArgumentException.class, () -> OpenAPIMojo.specFormat(Path.of("docs/openapi.txt")));
28+
}
29+
30+
@Test
31+
public void copyYamlSpec(@TempDir Path tempDir) throws Exception {
32+
var outputDir = tempDir.resolve("classes/myapp");
33+
Files.createDirectories(outputDir);
34+
var source = outputDir.resolve("App.yaml");
35+
Files.writeString(source, "openapi: 3.0.1");
36+
37+
var destination = tempDir.resolve("docs/openapi.yml");
38+
OpenAPIMojo.copySpec(List.of(source), destination);
39+
40+
assertTrue(Files.isRegularFile(destination));
41+
assertEquals(Files.readString(source), Files.readString(destination));
42+
}
43+
44+
@Test
45+
public void copyJsonSpec(@TempDir Path tempDir) throws Exception {
46+
var outputDir = tempDir.resolve("classes/myapp");
47+
Files.createDirectories(outputDir);
48+
var source = outputDir.resolve("App.json");
49+
Files.writeString(source, "{\"openapi\":\"3.0.1\"}");
50+
51+
var destination = tempDir.resolve("docs/openapi.json");
52+
OpenAPIMojo.copySpec(List.of(source), destination);
53+
54+
assertTrue(Files.isRegularFile(destination));
55+
assertEquals(Files.readString(source), Files.readString(destination));
56+
}
57+
}

0 commit comments

Comments
 (0)