1515import org .jspecify .annotations .Nullable ;
1616
1717import java .io .File ;
18+ import java .io .IOException ;
19+ import java .nio .file .Files ;
1820import java .nio .file .Path ;
21+ import java .nio .file .StandardCopyOption ;
22+ import java .util .ArrayList ;
1923import java .util .List ;
24+ import java .util .Locale ;
2025import java .util .Map ;
2126import 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 ();
0 commit comments