Skip to content

Commit 15f6b44

Browse files
authored
Merge pull request quarkusio#47290 from TimvdLippe/pr-timvdlippe-add-auto-add-api-endpoint-filter
Allow adding Open API specification endpoint to OpenAPI using config
2 parents 12fed44 + ec42adb commit 15f6b44

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)