Skip to content

Commit ec42adb

Browse files
TimvdLippegsmet
authored andcommitted
Allow adding Open API specification endpoints to OpenAPI using config
This mirrors the PR that allow for adding the health endpoint to the OpenAPI config [1] to also do that for the documented Open API endpoints themselves. Since the server dynamically generates this file, it can be requested from the server. Thus, it should also be possible to specify it in the document. To avoid a recursive problem of needing to specify the full structure of the document itself, inside of the document, we only specify that it is a JSON object. [1]: quarkusio#12044
1 parent 59c0402 commit ec42adb

File tree

7 files changed

+246
-0
lines changed

7 files changed

+246
-0
lines changed

extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ public interface SmallRyeOpenApiConfig {
118118
@WithDefault("true")
119119
boolean autoAddSecurity();
120120

121+
/**
122+
* This will automatically add the OpenAPI specification document endpoint to the schema.
123+
* It also adds "openapi" to the list of tags and specify an "operationId"
124+
*/
125+
@WithDefault("false")
126+
boolean autoAddOpenApiEndpoint();
127+
121128
/**
122129
* Required when using `apiKey` security. The location of the API key. Valid values are "query", "header" or "cookie".
123130
*/

extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import io.quarkus.security.PermissionsAllowed;
9595
import io.quarkus.smallrye.openapi.OpenApiFilter;
9696
import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig;
97+
import io.quarkus.smallrye.openapi.deployment.filter.AutoAddOpenApiEndpointFilter;
9798
import io.quarkus.smallrye.openapi.deployment.filter.AutoServerFilter;
9899
import io.quarkus.smallrye.openapi.deployment.filter.ClassAndMethod;
99100
import io.quarkus.smallrye.openapi.deployment.filter.DefaultInfoFilter;
@@ -403,6 +404,15 @@ public boolean accepts(DotName className) {
403404
return new OpenApiFilteredIndexViewBuildItem(indexView);
404405
}
405406

407+
@BuildStep
408+
void addAutoOpenApiEndpointFilter(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenAPIDefinitionProducer,
409+
SmallRyeOpenApiConfig config) {
410+
if (config.autoAddOpenApiEndpoint()) {
411+
addToOpenAPIDefinitionProducer
412+
.produce(new AddToOpenAPIDefinitionBuildItem(new AutoAddOpenApiEndpointFilter(config.path())));
413+
}
414+
}
415+
406416
@BuildStep
407417
void addAutoFilters(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenAPIDefinitionProducer,
408418
List<SecurityInformationBuildItem> securityInformationBuildItems,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.quarkus.smallrye.openapi.deployment.filter;
2+
3+
import org.eclipse.microprofile.openapi.OASFactory;
4+
import org.eclipse.microprofile.openapi.OASFilter;
5+
import org.eclipse.microprofile.openapi.models.OpenAPI;
6+
import org.eclipse.microprofile.openapi.models.Paths;
7+
import org.eclipse.microprofile.openapi.models.media.Content;
8+
9+
public class AutoAddOpenApiEndpointFilter implements OASFilter {
10+
private static final String OPENAPI_TAG = "openapi";
11+
private static final String ENDPOINT_DESCRIPTION = "OpenAPI specification";
12+
13+
private final String path;
14+
15+
private enum MediaTypeForEndpoint {
16+
JSON,
17+
YAML,
18+
BOTH,
19+
}
20+
21+
public AutoAddOpenApiEndpointFilter(String path) {
22+
this.path = path;
23+
}
24+
25+
@Override
26+
public void filterOpenAPI(OpenAPI openAPI) {
27+
Paths paths = openAPI.getPaths();
28+
if (paths == null) {
29+
paths = OASFactory.createPaths();
30+
openAPI.setPaths(paths);
31+
}
32+
openAPI.addTag(OASFactory.createTag().name(OPENAPI_TAG));
33+
createPathItem(paths, "", MediaTypeForEndpoint.BOTH);
34+
createPathItem(paths, "Json", MediaTypeForEndpoint.JSON);
35+
createPathItem(paths, "Yaml", MediaTypeForEndpoint.YAML);
36+
createPathItem(paths, "Yml", MediaTypeForEndpoint.YAML);
37+
}
38+
39+
private void createPathItem(Paths paths, String suffix, MediaTypeForEndpoint mediaTypeForEndpoints) {
40+
Content openApiContent = OASFactory.createContent();
41+
if (mediaTypeForEndpoints == MediaTypeForEndpoint.JSON || mediaTypeForEndpoints == MediaTypeForEndpoint.BOTH) {
42+
openApiContent.addMediaType("application/json", OASFactory.createMediaType());
43+
}
44+
if (mediaTypeForEndpoints == MediaTypeForEndpoint.YAML || mediaTypeForEndpoints == MediaTypeForEndpoint.BOTH) {
45+
openApiContent.addMediaType("application/yaml", OASFactory.createMediaType());
46+
}
47+
var openApiResponse = OASFactory.createAPIResponses()
48+
.addAPIResponse(
49+
"200",
50+
OASFactory.createAPIResponse()
51+
.description(ENDPOINT_DESCRIPTION)
52+
.content(openApiContent));
53+
var pathItemPath = this.path;
54+
// Strip off dot
55+
if (!suffix.isEmpty()) {
56+
pathItemPath += "." + suffix.toLowerCase();
57+
}
58+
var pathItem = OASFactory.createPathItem()
59+
.GET(
60+
OASFactory.createOperation()
61+
.description(ENDPOINT_DESCRIPTION)
62+
.addTag(OPENAPI_TAG)
63+
.operationId("getOpenAPISpecification" + suffix)
64+
.responses(openApiResponse));
65+
paths.addPathItem(pathItemPath, pathItem);
66+
}
67+
}

extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/deployment/filter/SecurityConfigFilterTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ public boolean autoAddSecurity() {
147147
return false;
148148
}
149149

150+
@Override
151+
public boolean autoAddOpenApiEndpoint() {
152+
return false;
153+
}
154+
150155
@Override
151156
public Optional<String> apiKeyParameterIn() {
152157
return Optional.ofNullable(apiKeyParameterIn);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import org.hamcrest.Matchers;
4+
import org.jboss.shrinkwrap.api.asset.StringAsset;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.quarkus.test.QuarkusUnitTest;
9+
import io.restassured.RestAssured;
10+
11+
public class AutoAddOpenApiEndpointFilterDisabledTest {
12+
private static final String OPEN_API_PATH = "/openapi";
13+
14+
@RegisterExtension
15+
static QuarkusUnitTest runner = new QuarkusUnitTest()
16+
.withApplicationRoot((jar) -> jar
17+
.addClasses(OpenApiResource.class, ResourceBean.class)
18+
.addAsResource(
19+
new StringAsset("""
20+
quarkus.smallrye-openapi.auto-add-open-api-endpoint=false
21+
quarkus.smallrye-openapi.path=%s
22+
""".formatted(OPEN_API_PATH)),
23+
24+
"application.properties"));
25+
26+
@Test
27+
public void testNoAddedFilterDoesNotProducePath() {
28+
RestAssured.given().header("Accept", "application/json")
29+
.when().get(OPEN_API_PATH)
30+
.then()
31+
.header("Content-Type", "application/json;charset=UTF-8")
32+
.body("paths", Matchers.not(Matchers.hasKey(OPEN_API_PATH)));
33+
}
34+
35+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import org.hamcrest.Matchers;
4+
import org.jboss.shrinkwrap.api.asset.StringAsset;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.quarkus.test.QuarkusUnitTest;
9+
import io.restassured.RestAssured;
10+
11+
public class AutoAddOpenApiEndpointFilterNoResourcesTest {
12+
private static final String OPEN_API_PATH = "/openapi";
13+
14+
@RegisterExtension
15+
static QuarkusUnitTest runner = new QuarkusUnitTest()
16+
.withApplicationRoot((jar) -> jar
17+
.addAsResource(
18+
new StringAsset("""
19+
quarkus.smallrye-openapi.auto-add-open-api-endpoint=true
20+
quarkus.smallrye-openapi.path=%s
21+
""".formatted(OPEN_API_PATH)),
22+
23+
"application.properties"));
24+
25+
@Test
26+
public void testNoResourcesDoesNotCrashFilter() {
27+
RestAssured.given().header("Accept", "application/json")
28+
.when().get(OPEN_API_PATH)
29+
.then()
30+
.header("Content-Type", "application/json;charset=UTF-8")
31+
.body("paths", Matchers.hasKey(OPEN_API_PATH));
32+
}
33+
34+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import org.hamcrest.Matchers;
4+
import org.jboss.shrinkwrap.api.asset.StringAsset;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.quarkus.test.QuarkusUnitTest;
9+
import io.restassured.RestAssured;
10+
11+
public class AutoAddOpenApiEndpointFilterTest {
12+
private static final String OPEN_API_PATH = "/openapi";
13+
14+
@RegisterExtension
15+
static QuarkusUnitTest runner = new QuarkusUnitTest()
16+
.withApplicationRoot((jar) -> jar
17+
.addClasses(OpenApiResource.class, ResourceBean.class)
18+
.addAsResource(
19+
new StringAsset("""
20+
quarkus.smallrye-openapi.auto-add-open-api-endpoint=true
21+
quarkus.smallrye-openapi.path=%s
22+
""".formatted(OPEN_API_PATH)),
23+
24+
"application.properties"));
25+
26+
@Test
27+
public void testOpenApiFilterResource() {
28+
var endpointPath = "paths.'%s'.get".formatted(OPEN_API_PATH);
29+
var contentPath = "paths.'%s'.get.responses.200.content".formatted(OPEN_API_PATH);
30+
RestAssured.given().header("Accept", "application/json")
31+
.when().get(OPEN_API_PATH)
32+
.then()
33+
.header("Content-Type", "application/json;charset=UTF-8")
34+
.body("paths", Matchers.hasKey(OPEN_API_PATH))
35+
.body(endpointPath + ".tags", Matchers.hasItem("openapi"))
36+
.body(endpointPath + ".responses", Matchers.hasKey("200"))
37+
.body(endpointPath + ".operationId", Matchers.equalTo("getOpenAPISpecification"))
38+
.body(contentPath, Matchers.hasKey("application/json"))
39+
.body(contentPath, Matchers.hasKey("application/yaml"))
40+
.body(contentPath + ".size()", Matchers.equalTo(2))
41+
.body("tags", Matchers.hasItem(Matchers.hasEntry("name", "openapi")));
42+
}
43+
44+
@Test
45+
public void testSpecificYamlEndpointFilterResource() {
46+
var yamlPath = OPEN_API_PATH + ".yaml";
47+
var endpointPath = "paths.'%s'.get".formatted(yamlPath);
48+
var contentPath = "paths.'%s'.get.responses.200.content".formatted(yamlPath);
49+
RestAssured.given().header("Accept", "application/json")
50+
.when().get(OPEN_API_PATH)
51+
.then()
52+
.header("Content-Type", "application/json;charset=UTF-8")
53+
.body("paths", Matchers.hasKey(yamlPath))
54+
.body(endpointPath + ".operationId", Matchers.equalTo("getOpenAPISpecificationYaml"))
55+
.body(contentPath, Matchers.hasKey("application/yaml"))
56+
.body(contentPath + ".size()", Matchers.equalTo(1));
57+
}
58+
59+
@Test
60+
public void testSpecificYmlEndpointFilterResource() {
61+
var ymlPath = OPEN_API_PATH + ".yml";
62+
var endpointPath = "paths.'%s'.get".formatted(ymlPath);
63+
var contentPath = "paths.'%s'.get.responses.200.content".formatted(ymlPath);
64+
RestAssured.given().header("Accept", "application/json")
65+
.when().get(OPEN_API_PATH)
66+
.then()
67+
.header("Content-Type", "application/json;charset=UTF-8")
68+
.body("paths", Matchers.hasKey(ymlPath))
69+
.body(endpointPath + ".operationId", Matchers.equalTo("getOpenAPISpecificationYml"))
70+
.body(contentPath, Matchers.hasKey("application/yaml"))
71+
.body(contentPath + ".size()", Matchers.equalTo(1));
72+
}
73+
74+
@Test
75+
public void testSpecificJsonEndpointFilterResource() {
76+
var jsonPath = OPEN_API_PATH + ".json";
77+
var endpointPath = "paths.'%s'.get".formatted(jsonPath);
78+
var contentPath = "paths.'%s'.get.responses.200.content".formatted(jsonPath);
79+
RestAssured.given().header("Accept", "application/json")
80+
.when().get(OPEN_API_PATH)
81+
.then()
82+
.header("Content-Type", "application/json;charset=UTF-8")
83+
.body("paths", Matchers.hasKey(jsonPath))
84+
.body(endpointPath + ".operationId", Matchers.equalTo("getOpenAPISpecificationJson"))
85+
.body(contentPath, Matchers.hasKey("application/json"))
86+
.body(contentPath + ".size()", Matchers.equalTo(1));
87+
}
88+
}

0 commit comments

Comments
 (0)