Skip to content

Commit 2ba0124

Browse files
committed
open-api: merge operations from template file
- fix #3578
1 parent 44ea7e5 commit 2ba0124

14 files changed

Lines changed: 638 additions & 70 deletions

File tree

docs/asciidoc/modules/openapi.adoc

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,7 @@ The OpenAPI output generates some default values for `info` and `server` section
428428
the necessary to follow the specification and produces a valid output. These sections can be override
429429
with better information/metadata.
430430

431-
To do so just write an `openapi.yaml` file inside the `conf` directory the we are going to use it
432-
as template.
431+
To do so just write an `openapi.yaml` file inside the `conf` directory to use it as template.
433432

434433
.conf/openapi.yaml
435434
[source, yaml]
@@ -446,9 +445,26 @@ info:
446445
license:
447446
name: Apache 2.0
448447
url: http://www.apache.org/licenses/LICENSE-2.0.html
448+
paths:
449+
/api/pets:
450+
get:
451+
operationId: listPets
452+
description: List and sort pets.
453+
parameters:
454+
name: page
455+
descripton: Page number.
456+
449457
----
450458

451-
All sections from template file (except the paths section) are merged into the final output.
459+
All sections from template file are merged into the final output.
460+
461+
The extension property: `x-merge-policy` controls how merge must be done:
462+
463+
- ignore: Silently ignore a path or operation present in template but not found in generated output. This is the default value.
464+
- keep: Add a path or operation to final output. Must be valid path or operation.
465+
- fail: Throw an error when path or operation is present in template but not found in generated output.
466+
467+
The extension property can be added at root/global level, paths, pathItem, operation or parameter level.
452468

453469
[NOTE]
454470
====
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.openapi;
7+
8+
import static java.util.Optional.ofNullable;
9+
10+
import java.util.Map;
11+
import java.util.Objects;
12+
13+
public enum MergePolicy {
14+
FAIL {
15+
@Override
16+
public boolean handle(String message) {
17+
throw new IllegalArgumentException(message);
18+
}
19+
},
20+
KEEP {
21+
@Override
22+
public boolean handle(String message) {
23+
return true;
24+
}
25+
},
26+
IGNORE {
27+
@Override
28+
public boolean handle(String message) {
29+
return false;
30+
}
31+
};
32+
33+
public static MergePolicy parse(Map<String, Object> extensions, MergePolicy defaultPolicy) {
34+
if (extensions == null) {
35+
return defaultPolicy;
36+
}
37+
var value = extensions.remove("x-merge-policy");
38+
return ofNullable(value)
39+
.map(Objects::toString)
40+
.map(String::toUpperCase)
41+
.map(MergePolicy::valueOf)
42+
.orElse(defaultPolicy);
43+
}
44+
45+
public abstract boolean handle(String message);
46+
}

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java

Lines changed: 195 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,21 @@
55
*/
66
package io.jooby.internal.openapi;
77

8-
import java.io.IOException;
9-
import java.net.URL;
10-
import java.nio.file.Files;
11-
import java.nio.file.Path;
12-
import java.util.Collections;
13-
import java.util.List;
14-
import java.util.Optional;
8+
import java.util.*;
9+
import java.util.function.BiConsumer;
10+
import java.util.function.Consumer;
11+
import java.util.function.Function;
1512

1613
import com.fasterxml.jackson.annotation.JsonIgnore;
17-
import io.jooby.SneakyThrows;
18-
import io.swagger.v3.core.util.Yaml;
19-
import io.swagger.v3.oas.models.OpenAPI;
14+
import io.jooby.Router;
15+
import io.swagger.v3.oas.models.*;
16+
import io.swagger.v3.oas.models.parameters.Parameter;
2017

2118
public class OpenAPIExt extends OpenAPI {
2219
@JsonIgnore private List<OperationExt> operations = Collections.emptyList();
2320

2421
@JsonIgnore private String source;
2522

26-
public static Optional<OpenAPI> fromTemplate(
27-
Path basedir, ClassLoader classLoader, String templateName) {
28-
try {
29-
Path path = basedir.resolve("conf").resolve(templateName);
30-
if (Files.exists(path)) {
31-
return Optional.of(Yaml.mapper().readValue(path.toFile(), OpenAPIExt.class));
32-
}
33-
URL resource = classLoader.getResource(templateName);
34-
if (resource != null) {
35-
return Optional.of(Yaml.mapper().readValue(resource, OpenAPIExt.class));
36-
}
37-
return Optional.empty();
38-
} catch (IOException x) {
39-
throw SneakyThrows.propagate(x);
40-
}
41-
}
42-
4323
public List<OperationExt> getOperations() {
4424
return operations;
4525
}
@@ -55,4 +35,192 @@ public String getSource() {
5535
public void setSource(String classname) {
5636
this.source = classname;
5737
}
38+
39+
@Override
40+
public void setPaths(Paths paths) {
41+
var existingPaths = this.getPaths();
42+
if (existingPaths != null && !existingPaths.isEmpty()) {
43+
var mergePolicy =
44+
MergePolicy.parse(
45+
existingPaths.getExtensions(),
46+
MergePolicy.parse(getExtensions(), MergePolicy.IGNORE));
47+
super.setPaths(mergePaths(existingPaths, paths, mergePolicy));
48+
} else {
49+
super.setPaths(paths);
50+
}
51+
}
52+
53+
private Paths mergePaths(Paths docPaths, Paths paths, MergePolicy mergePolicy) {
54+
for (var e : docPaths.entrySet()) {
55+
var pattern = e.getKey();
56+
var path = paths.get(pattern);
57+
if (path != null) {
58+
// Copy into generated path
59+
var docPath = e.getValue();
60+
setProperty(docPath, PathItem::getSummary, path, PathItem::setSummary);
61+
setProperty(docPath, PathItem::getDescription, path, PathItem::setDescription);
62+
setProperty(docPath, PathItem::getServers, path, PathItem::setServers);
63+
setProperty(docPath, PathItem::getParameters, path, PathItem::setParameters);
64+
setProperty(docPath, PathItem::get$ref, path, PathItem::set$ref);
65+
setProperty(docPath, PathItem::getExtensions, path, PathItem::setExtensions);
66+
// Operation
67+
mergeOperation(
68+
Router.GET, pattern, docPath.getGet(), path.getGet(), mergePolicy, path::setGet);
69+
mergeOperation(
70+
Router.POST, pattern, docPath.getPost(), path.getPost(), mergePolicy, path::setPost);
71+
mergeOperation(
72+
Router.PUT, pattern, docPath.getPut(), path.getPut(), mergePolicy, path::setPut);
73+
mergeOperation(
74+
Router.PATCH,
75+
pattern,
76+
docPath.getPatch(),
77+
path.getPatch(),
78+
mergePolicy,
79+
path::setPatch);
80+
mergeOperation(
81+
Router.DELETE,
82+
pattern,
83+
docPath.getDelete(),
84+
path.getDelete(),
85+
mergePolicy,
86+
path::setDelete);
87+
mergeOperation(
88+
Router.HEAD, pattern, docPath.getHead(), path.getHead(), mergePolicy, path::setHead);
89+
mergeOperation(
90+
Router.OPTIONS,
91+
pattern,
92+
docPath.getOptions(),
93+
path.getOptions(),
94+
mergePolicy,
95+
path::setOptions);
96+
mergeOperation(
97+
Router.TRACE,
98+
pattern,
99+
docPath.getTrace(),
100+
path.getTrace(),
101+
mergePolicy,
102+
path::setTrace);
103+
} else if (mergePolicy.handle("Unknown path: \"" + pattern + "\"")) {
104+
var newOperation = e.getValue();
105+
clearMergePolicy(newOperation, PathItem::getExtensions, PathItem::setExtensions);
106+
clearMergePolicy(newOperation.getGet(), Operation::getExtensions, Operation::setExtensions);
107+
clearMergePolicy(
108+
newOperation.getPost(), Operation::getExtensions, Operation::setExtensions);
109+
clearMergePolicy(newOperation.getPut(), Operation::getExtensions, Operation::setExtensions);
110+
clearMergePolicy(
111+
newOperation.getPatch(), Operation::getExtensions, Operation::setExtensions);
112+
clearMergePolicy(
113+
newOperation.getDelete(), Operation::getExtensions, Operation::setExtensions);
114+
clearMergePolicy(
115+
newOperation.getHead(), Operation::getExtensions, Operation::setExtensions);
116+
clearMergePolicy(
117+
newOperation.getOptions(), Operation::getExtensions, Operation::setExtensions);
118+
clearMergePolicy(
119+
newOperation.getTrace(), Operation::getExtensions, Operation::setExtensions);
120+
paths.put(e.getKey(), newOperation);
121+
}
122+
}
123+
return paths;
124+
}
125+
126+
private <T> void clearMergePolicy(
127+
T src, Function<T, Map<String, Object>> getter, BiConsumer<T, Map<String, Object>> setter) {
128+
if (src != null) {
129+
Map<String, Object> extensions = getter.apply(src);
130+
if (extensions != null) {
131+
extensions.remove("x-merge-policy");
132+
if (extensions.isEmpty()) {
133+
extensions = null;
134+
}
135+
setter.accept(src, extensions);
136+
}
137+
}
138+
}
139+
140+
private void mergeOperation(
141+
String method,
142+
String pattern,
143+
Operation src,
144+
Operation target,
145+
MergePolicy defaultMergePolicy,
146+
Consumer<Operation> appender) {
147+
if (src != null) {
148+
MergePolicy mergePolicy = MergePolicy.parse(src.getExtensions(), defaultMergePolicy);
149+
if (target != null) {
150+
setProperty(src, Operation::getTags, target, Operation::setTags);
151+
setProperty(src, Operation::getSummary, target, Operation::setSummary);
152+
setProperty(src, Operation::getDescription, target, Operation::setDescription);
153+
setProperty(src, Operation::getExternalDocs, target, Operation::setExternalDocs);
154+
setProperty(src, Operation::getOperationId, target, Operation::setOperationId);
155+
setProperty(src, Operation::getRequestBody, target, Operation::setRequestBody);
156+
setProperty(src, Operation::getResponses, target, Operation::setResponses);
157+
setProperty(src, Operation::getCallbacks, target, Operation::setCallbacks);
158+
setProperty(src, Operation::getDeprecated, target, Operation::setDeprecated);
159+
setProperty(src, Operation::getSecurity, target, Operation::setSecurity);
160+
setProperty(src, Operation::getServers, target, Operation::setServers);
161+
setProperty(src, Operation::getExtensions, target, Operation::setExtensions);
162+
163+
// Parameter are sync in next line:
164+
// setProperty(src, Operation::getParameters, target, Operation::setParameters);
165+
var srcParameters =
166+
Optional.ofNullable(src.getParameters()).orElseGet(List::of).stream()
167+
.filter(Objects::nonNull)
168+
.toList();
169+
var targetParameters =
170+
Optional.ofNullable(target.getParameters()).orElseGet(List::of).stream()
171+
.filter(Objects::nonNull)
172+
.toList();
173+
for (var srcParameter : srcParameters) {
174+
targetParameters.stream()
175+
.filter(it -> it.getName().equals(srcParameter.getName()))
176+
.findFirst()
177+
.ifPresent(targetParameter -> mergeParameter(srcParameter, targetParameter));
178+
}
179+
} else if (mergePolicy.handle("Operation not found: " + method + " " + pattern)) {
180+
appender.accept(src);
181+
}
182+
}
183+
}
184+
185+
private void mergeParameter(Parameter src, Parameter target) {
186+
setProperty(src, Parameter::getIn, target, Parameter::setIn);
187+
setProperty(src, Parameter::getDescription, target, Parameter::setDescription);
188+
setProperty(src, Parameter::getRequired, target, Parameter::setRequired);
189+
setProperty(src, Parameter::getDeprecated, target, Parameter::setDeprecated);
190+
setProperty(src, Parameter::getAllowEmptyValue, target, Parameter::setAllowEmptyValue);
191+
setProperty(src, Parameter::get$ref, target, Parameter::set$ref);
192+
setProperty(src, Parameter::getStyle, target, Parameter::setStyle);
193+
setProperty(src, Parameter::getExplode, target, Parameter::setExplode);
194+
setProperty(src, Parameter::getAllowReserved, target, Parameter::setAllowReserved);
195+
setProperty(src, Parameter::getSchema, target, Parameter::setSchema);
196+
setProperty(src, Parameter::getExamples, target, Parameter::setExamples);
197+
setProperty(src, Parameter::getExample, target, Parameter::setExample);
198+
setProperty(src, Parameter::getContent, target, Parameter::setContent);
199+
setProperty(src, Parameter::getExtensions, target, Parameter::setExtensions);
200+
}
201+
202+
private <S, V> void setProperty(S src, Function<S, V> getter, S target, BiConsumer<S, V> setter) {
203+
var value = getter.apply(src);
204+
// Copy only non-null values
205+
if (value != null) {
206+
if (value instanceof Collection<?> collection) {
207+
// non-empty
208+
if (!collection.isEmpty()) {
209+
setter.accept(target, value);
210+
}
211+
} else if (value instanceof Map<?, ?> map) {
212+
// non-empty
213+
if (!map.isEmpty()) {
214+
setter.accept(target, value);
215+
}
216+
} else if (value instanceof CharSequence string) {
217+
// non-empty
218+
if (!string.isEmpty()) {
219+
setter.accept(target, value);
220+
}
221+
} else {
222+
setter.accept(target, value);
223+
}
224+
}
225+
}
58226
}

0 commit comments

Comments
 (0)