Skip to content

Commit 8a8d411

Browse files
authored
Use MP OpenAPI 1.2 and adapt to SmallRye OpenAPI 2.0.26 which supports it (helidon-io#3294)
* Use MP OpenAPI 1.2 and adapt to SmallRye OpenAPI 2.0.26 which supports it Signed-off-by: tim.quinn@oracle.com <tim.quinn@oracle.com> * Allow but warn of unquoted HTTP status values in incoming OpenAPI documents (they should be quoted) * Add logging (and refactor a bit for it), correct a logging typo, and set the 'openapi' element when merging documents (because SmallRye skips that during merge) * Prevent false prefix matches in regex used for selecting classes for annotation processing, add some logging, and use method reference instead of a lambda * Rewrite a test to use Helidon test support and Hamcrest, enhance error reporting in TestUtil, and update pom.xml for testing changes * Updated pom.xml
1 parent 8624a06 commit 8a8d411

16 files changed

Lines changed: 326 additions & 98 deletions

File tree

dependencies/pom.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
<version.lib.microprofile-health>2.2</version.lib.microprofile-health>
106106
<version.lib.microprofile-jwt>1.1.1</version.lib.microprofile-jwt>
107107
<version.lib.microprofile-metrics-api>2.3.2</version.lib.microprofile-metrics-api>
108-
<version.lib.microprofile-openapi-api>1.1.2</version.lib.microprofile-openapi-api>
108+
<version.lib.microprofile-openapi-api>1.2</version.lib.microprofile-openapi-api>
109109
<version.lib.microprofile-reactive-messaging-api>1.0</version.lib.microprofile-reactive-messaging-api>
110110
<version.lib.microprofile-reactive-streams-operators-api>1.0.1</version.lib.microprofile-reactive-streams-operators-api>
111111
<version.lib.microprofile-reactive-streams-operators-core>1.0.1</version.lib.microprofile-reactive-streams-operators-core>
@@ -130,7 +130,7 @@
130130
<version.lib.postgresql>42.2.18</version.lib.postgresql>
131131
<version.lib.prometheus>0.9.0</version.lib.prometheus>
132132
<version.lib.slf4j>1.7.30</version.lib.slf4j>
133-
<version.lib.smallrye-openapi>1.2.3</version.lib.smallrye-openapi>
133+
<version.lib.smallrye-openapi>2.0.26</version.lib.smallrye-openapi>
134134
<version.lib.snakeyaml>1.27</version.lib.snakeyaml>
135135
<version.lib.transaction-api>1.3.3</version.lib.transaction-api>
136136
<version.lib.typesafe-config>1.4.1</version.lib.typesafe-config>
@@ -740,6 +740,7 @@
740740
<groupId>io.smallrye</groupId>
741741
<artifactId>smallrye-open-api</artifactId>
742742
<version>${version.lib.smallrye-openapi}</version>
743+
<type>pom</type>
743744
</dependency>
744745

745746
<!-- Integrations related -->

microprofile/openapi/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@
122122
<artifactId>snakeyaml</artifactId>
123123
<scope>test</scope>
124124
</dependency>
125+
<dependency>
126+
<groupId>io.helidon.microprofile.tests</groupId>
127+
<artifactId>helidon-microprofile-tests-junit5</artifactId>
128+
<scope>test</scope>
129+
</dependency>
130+
<dependency>
131+
<groupId>org.hamcrest</groupId>
132+
<artifactId>hamcrest-all</artifactId>
133+
<scope>test</scope>
134+
</dependency>
125135
</dependencies>
126136

127137
</project>

microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.function.Supplier;
2828
import java.util.logging.Level;
2929
import java.util.logging.Logger;
30+
import java.util.regex.Pattern;
3031
import java.util.stream.Collectors;
3132

3233
import javax.enterprise.inject.spi.CDI;
@@ -173,19 +174,41 @@ private FilteredIndexView appRelatedClassesToFilteredIndexView(Set<Class<?>> app
173174
* Create an OpenAPIConfig instance to limit scanning to this app's classes by overriding any inclusions of classes or
174175
* packages specified in the config with our own inclusions based on this app's classes.
175176
*/
176-
OpenApiConfigImpl openAPIFilteringConfig = new OpenApiConfigImpl(mpConfig);
177-
Set<String> scanClasses = openAPIFilteringConfig.scanClasses();
178-
scanClasses.clear();
179-
openAPIFilteringConfig.scanPackages().clear();
180-
181-
appRelatedClassesToScan.stream()
182-
.map(Class::getName)
183-
.forEach(scanClasses::add);
177+
Pattern appRelatedClassesPattern = Pattern.compile(
178+
appRelatedClassesToScan.stream()
179+
.map(Class::getName)
180+
.map(Pattern::quote)
181+
.map(name -> "^" + name + "$") // avoid false prefix matches
182+
.collect(Collectors.joining("|", "(", ")")));
183+
184+
FilteredIndexView result = new FilteredIndexView(singleIndexViewSupplier.get(),
185+
new FilteringOpenApiConfigImpl(mpConfig, appRelatedClassesPattern));
186+
if (LOGGER.isLoggable(Level.FINE)) {
187+
LOGGER.log(Level.FINE, String.format("FilteredIndexView for %n"
188+
+ " application classes %s%n"
189+
+ " will use pattern: %s%n"
190+
+ " with known classes %s",
191+
appRelatedClassesToScan, appRelatedClassesPattern, result.getKnownClasses()));
192+
}
184193

185-
FilteredIndexView result = new FilteredIndexView(singleIndexViewSupplier.get(), openAPIFilteringConfig);
186194
return result;
187195
}
188196

197+
private static class FilteringOpenApiConfigImpl extends OpenApiConfigImpl {
198+
199+
private final Pattern appRelatedClassNamesToScan;
200+
201+
FilteringOpenApiConfigImpl(Config config, Pattern appRelatedClassNamesToScan) {
202+
super(config);
203+
this.appRelatedClassNamesToScan = appRelatedClassNamesToScan;
204+
}
205+
206+
@Override
207+
public Pattern scanClasses() {
208+
return appRelatedClassNamesToScan;
209+
}
210+
}
211+
189212
/**
190213
* Sets the OpenApiConfig instance to use in governing the behavior of the
191214
* smallrye OpenApi implementation.
@@ -211,7 +234,7 @@ MPOpenAPIBuilder singleIndexViewSupplier(Supplier<? extends IndexView> singleInd
211234

212235
@Override
213236
protected Supplier<List<? extends IndexView>> indexViewsSupplier() {
214-
return () -> buildPerAppFilteredIndexViews();
237+
return this::buildPerAppFilteredIndexViews;
215238
}
216239

217240
@Override

microprofile/openapi/src/main/java/module-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
module io.helidon.microprofile.openapi {
2323
requires java.logging;
2424

25-
requires smallrye.open.api;
25+
requires smallrye.open.api.core;
2626

2727
requires microprofile.config.api;
2828
requires io.helidon.microprofile.server;

microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,61 +16,63 @@
1616
*/
1717
package io.helidon.microprofile.openapi;
1818

19-
import java.net.HttpURLConnection;
2019
import java.util.Map;
2120

22-
import io.helidon.common.http.MediaType;
23-
import io.helidon.config.Config;
24-
import io.helidon.microprofile.openapi.other.TestApp2;
25-
import io.helidon.microprofile.server.Server;
21+
import javax.inject.Inject;
22+
import javax.ws.rs.client.WebTarget;
23+
import javax.ws.rs.core.Response;
24+
25+
import io.helidon.common.http.Http;
26+
import io.helidon.microprofile.tests.junit5.AddBean;
27+
import io.helidon.microprofile.tests.junit5.HelidonTest;
28+
import io.helidon.openapi.OpenAPISupport;
2629

27-
import org.junit.jupiter.api.AfterAll;
28-
import org.junit.jupiter.api.BeforeAll;
2930
import org.junit.jupiter.api.Test;
31+
import org.yaml.snakeyaml.Yaml;
3032

33+
import static org.hamcrest.MatcherAssert.assertThat;
34+
import static org.hamcrest.Matchers.equalTo;
35+
import static org.hamcrest.Matchers.is;
3136
import static org.junit.jupiter.api.Assertions.assertEquals;
3237

3338
/**
3439
* Test that MP OpenAPI support works when retrieving the OpenAPI document
3540
* from the server's /openapi endpoint.
3641
*/
42+
@HelidonTest
43+
@AddBean(TestApp.class)
44+
@AddBean(TestApp3.class)
3745
public class BasicServerTest {
3846

39-
private static final String OPENAPI_PATH = "/openapi";
40-
41-
private static Server server;
42-
43-
private static HttpURLConnection cnx;
44-
4547
private static Map<String, Object> yaml;
4648

47-
public BasicServerTest() {
49+
@Inject
50+
WebTarget webTarget;
51+
52+
private static Map<String, Object> retrieveYaml(WebTarget webTarget) {
53+
try (Response response = webTarget
54+
.path(OpenAPISupport.DEFAULT_WEB_CONTEXT)
55+
.request(OpenAPISupport.DEFAULT_RESPONSE_MEDIA_TYPE.toString())
56+
.get()) {
57+
assertThat("Fetch of OpenAPI document from server status", response.getStatus(),
58+
is(equalTo(Http.Status.OK_200.code())));
59+
String yamlText = response.readEntity(String.class);
60+
return new Yaml().load(yamlText);
61+
}
4862
}
4963

50-
/**
51-
* Start the server to run the test app and read the response from the
52-
* /openapi endpoint into a map that all tests can use.
53-
*
54-
* @throws Exception in case of error reading the response as yaml
55-
*/
56-
@BeforeAll
57-
public static void startServer() throws Exception {
58-
server = TestUtil.startServer(Config.create(), TestApp.class, TestApp3.class);
59-
cnx = TestUtil.getURLConnection(
60-
server.port(),
61-
"GET",
62-
OPENAPI_PATH,
63-
MediaType.APPLICATION_OPENAPI_YAML);
64+
private static Map<String, Object> yaml(WebTarget webTarget) {
65+
if (yaml == null) {
66+
yaml = retrieveYaml(webTarget);
67+
}
68+
return yaml;
69+
}
6470

65-
yaml = TestUtil.yamlFromResponse(cnx);
71+
private Map<String, Object> yaml() {
72+
return yaml(webTarget);
6673
}
6774

68-
/**
69-
* Stop the server.
70-
*/
71-
@AfterAll
72-
public static void stopServer() {
73-
TestUtil.cleanup(server, cnx);
75+
public BasicServerTest() {
7476
}
7577

7678
/**
@@ -79,16 +81,18 @@ public static void stopServer() {
7981
*
8082
* @throws Exception in case of errors reading the HTTP response
8183
*/
82-
@SuppressWarnings("unchecked")
8384
@Test
8485
public void simpleTest() throws Exception {
85-
String goSummary = TestUtil.fromYaml(yaml, "paths./testapp/go.get.summary", String.class);
86-
assertEquals(TestApp.GO_SUMMARY, goSummary);
86+
checkPathValue("paths./testapp/go.get.summary", TestApp.GO_SUMMARY);
8787
}
8888

8989
@Test
9090
public void testMultipleApps() {
91-
String goSummary3 = TestUtil.fromYaml(yaml, "paths./testapp3/go3.get.summary", String.class);
92-
assertEquals(TestApp3.GO_SUMMARY, goSummary3);
91+
checkPathValue("paths./testapp3/go3.get.summary", TestApp3.GO_SUMMARY);
92+
}
93+
94+
private void checkPathValue(String pathExpression, String expected) {
95+
String result = TestUtil.fromYaml(yaml(), pathExpression, String.class);
96+
assertThat(pathExpression, result, is(equalTo(expected)));
9397
}
9498
}

microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.yaml.snakeyaml.Yaml;
3535

3636
import static org.junit.jupiter.api.Assertions.assertTrue;
37+
import static org.junit.jupiter.api.Assertions.fail;
3738

3839
/**
3940
* Useful utility methods during testing.
@@ -198,9 +199,14 @@ public static Map<String, Object> yamlFromResponse(HttpURLConnection cnx) throws
198199
*/
199200
@SuppressWarnings(value = "unchecked")
200201
public static <T> T fromYaml(Map<String, Object> map, String dottedPath, Class<T> cl) {
202+
Map<String, Object> originalMap = map;
201203
String[] segments = dottedPath.split("\\.");
202204
for (int i = 0; i < segments.length - 1; i++) {
203205
map = (Map<String, Object>) map.get(segments[i]);
206+
if (map == null) {
207+
fail("Traversing dotted path " + dottedPath + " segment " + segments[i] + " not found in parsed map "
208+
+ originalMap);
209+
}
204210
}
205211
return cl.cast(map.get(segments[segments.length - 1]));
206212
}

openapi/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
<failOnMissingClassifierArtifact>true</failOnMissingClassifierArtifact>
7474
<outputDirectory>${openapi-impls-dir}</outputDirectory>
7575
<includeGroupIds>io.smallrye</includeGroupIds>
76-
<includeArtifactIds>smallrye-open-api</includeArtifactIds>
76+
<includeArtifactIds>smallrye-open-api-core</includeArtifactIds>
7777
<includes>io/smallrye/openapi/api/models/**/*.java</includes>
7878
</configuration>
7979
</execution>
@@ -123,6 +123,7 @@
123123
<dependency>
124124
<groupId>io.smallrye</groupId>
125125
<artifactId>smallrye-open-api</artifactId>
126+
<type>pom</type>
126127
<exclusions>
127128
<exclusion>
128129
<groupId>com.fasterxml.jackson.core</groupId>

openapi/src/main/java/io/helidon/openapi/CustomConstructor.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616
*/
1717
package io.helidon.openapi;
1818

19+
import java.util.ArrayList;
1920
import java.util.HashMap;
21+
import java.util.List;
2022
import java.util.Map;
23+
import java.util.logging.Level;
24+
import java.util.logging.Logger;
2125

2226
import org.eclipse.microprofile.openapi.models.PathItem;
2327
import org.eclipse.microprofile.openapi.models.Paths;
@@ -32,10 +36,12 @@
3236
import org.eclipse.microprofile.openapi.models.servers.ServerVariables;
3337
import org.yaml.snakeyaml.TypeDescription;
3438
import org.yaml.snakeyaml.constructor.Constructor;
39+
import org.yaml.snakeyaml.error.Mark;
3540
import org.yaml.snakeyaml.nodes.MappingNode;
3641
import org.yaml.snakeyaml.nodes.Node;
3742
import org.yaml.snakeyaml.nodes.NodeId;
3843
import org.yaml.snakeyaml.nodes.SequenceNode;
44+
import org.yaml.snakeyaml.nodes.Tag;
3945

4046
/**
4147
* Specialized SnakeYAML constructor for modifying {@code Node} objects for OpenAPI types that extend {@code Map} to adjust the
@@ -64,6 +70,8 @@ final class CustomConstructor extends Constructor {
6470
// maps OpenAPI interfaces which extend Map<?, List<type>> to the type that appears in the list
6571
private static final Map<Class<?>, Class<?>> CHILD_MAP_OF_LIST_TYPES = new HashMap<>();
6672

73+
private static final Logger LOGGER = Logger.getLogger(CustomConstructor.class.getName());
74+
6775
static {
6876
CHILD_MAP_TYPES.put(Paths.class, PathItem.class);
6977
CHILD_MAP_TYPES.put(Callback.class, PathItem.class);
@@ -99,6 +107,28 @@ protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> map
99107
}
100108
});
101109
}
110+
111+
// Older releases silently accepted numbers for APIResponse status values; they should be strings.
112+
if (parentType.equals(APIResponses.class)) {
113+
convertIntHttpStatuses(node);
114+
}
102115
super.constructMapping2ndStep(node, mapping);
103116
}
117+
118+
private void convertIntHttpStatuses(MappingNode node) {
119+
List<Mark> numericHttpStatusMarks = new ArrayList<>();
120+
node.getValue().forEach(t -> {
121+
Node keyNode = t.getKeyNode();
122+
if (keyNode.getTag().equals(Tag.INT)) {
123+
numericHttpStatusMarks.add(keyNode.getStartMark());
124+
keyNode.setTag(Tag.STR);
125+
}
126+
});
127+
if (!numericHttpStatusMarks.isEmpty()) {
128+
LOGGER.log(Level.WARNING,
129+
"Numeric HTTP status value(s) should be quoted. "
130+
+ "Please change the following; unquoted numeric values might be rejected in a future release: {0}",
131+
numericHttpStatusMarks);
132+
}
133+
}
104134
}

0 commit comments

Comments
 (0)