Skip to content

Commit

Permalink
Rest Data Panache: Correct Open API integration
Browse files Browse the repository at this point in the history
Before these changes, Open API could not deduct the return type when using Response.class (in Rest Data Panache classic). 
Also, for Rest Data Panache reactive, the return object for the endpoint '/get' was wrongly saying that was a single entity when is an array of entities. 
And finally, for the update and delete endpoints, the result status code was wrong (for update should be 201 and for delete 204).
After these changes, all the above issues are fixed and covered using the existing test.
Fix quarkusio#28786
  • Loading branch information
Sgitario authored and tmihalac committed Oct 27, 2022
1 parent 4e86b7c commit ec692da
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 3 deletions.
@@ -1,5 +1,7 @@
package io.quarkus.hibernate.orm.rest.data.panache.deployment.openapi;

import static org.hamcrest.Matchers.is;

import java.util.List;

import org.hamcrest.Matchers;
Expand All @@ -25,6 +27,7 @@
class OpenApiIntegrationTest {

private static final String OPEN_API_PATH = "/q/openapi";
private static final String COLLECTIONS_SCHEMA_REF = "#/components/schemas/Collection";

@RegisterExtension
static final QuarkusProdModeTest TEST = new QuarkusProdModeTest()
Expand All @@ -36,7 +39,7 @@ class OpenApiIntegrationTest {
.addAsResource("application.properties")
.addAsResource("import.sql"))
.setForcedDependencies(List.of(
Dependency.of("io.quarkus", "quarkus-smallrye-openapi", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-smallrye-openapi-deployment", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-jdbc-h2-deployment", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-resteasy-jsonb-deployment", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-security-deployment", Version.getVersion())))
Expand All @@ -51,14 +54,28 @@ public void testOpenApiForGeneratedResources() {
.body("info.title", Matchers.equalTo("quarkus-hibernate-orm-rest-data-panache-deployment API"))
.body("paths.'/collections'", Matchers.hasKey("get"))
.body("paths.'/collections'.get.tags", Matchers.hasItem("CollectionsResource"))
.body("paths.'/collections'.get.responses.'200'.content.'application/json'.schema.type", is("array"))
.body("paths.'/collections'.get.responses.'200'.content.'application/json'.schema.items.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections'", Matchers.hasKey("post"))
.body("paths.'/collections'.post.tags", Matchers.hasItem("CollectionsResource"))
.body("paths.'/collections'.post.requestBody.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections'.post.responses.'201'.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections'.post.security[0].SecurityScheme", Matchers.hasItem("user"))
.body("paths.'/collections/{id}'", Matchers.hasKey("get"))
.body("paths.'/collections/{id}'.get.responses.'200'.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections/{id}'.get.security[0].SecurityScheme", Matchers.hasItem("user"))
.body("paths.'/collections/{id}'", Matchers.hasKey("put"))
.body("paths.'/collections/{id}'.put.requestBody.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections/{id}'.put.responses.'201'.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections/{id}'.put.security[0].SecurityScheme", Matchers.hasItem("user"))
.body("paths.'/collections/{id}'", Matchers.hasKey("delete"))
.body("paths.'/collections/{id}'.delete.responses", Matchers.hasKey("204"))
.body("paths.'/collections/{id}'.delete.security[0].SecurityScheme", Matchers.hasItem("admin"))
.body("paths.'/empty-list-items'", Matchers.hasKey("get"))
.body("paths.'/empty-list-items'.get.tags", Matchers.hasItem("EmptyListItemsResource"))
Expand Down
@@ -1,5 +1,7 @@
package io.quarkus.hibernate.reactive.rest.data.panache.deployment.openapi;

import static org.hamcrest.Matchers.is;

import java.util.List;

import org.hamcrest.Matchers;
Expand All @@ -25,6 +27,7 @@
class OpenApiIntegrationTest {

private static final String OPEN_API_PATH = "/q/openapi";
private static final String COLLECTIONS_SCHEMA_REF = "#/components/schemas/Collection";

@RegisterExtension
static final QuarkusProdModeTest TEST = new QuarkusProdModeTest()
Expand All @@ -36,7 +39,7 @@ class OpenApiIntegrationTest {
.addAsResource("application.properties")
.addAsResource("import.sql"))
.setForcedDependencies(List.of(
Dependency.of("io.quarkus", "quarkus-smallrye-openapi", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-smallrye-openapi-deployment", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-reactive-pg-client-deployment", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-resteasy-reactive-jsonb-deployment", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-security-deployment", Version.getVersion())))
Expand All @@ -51,14 +54,28 @@ public void testOpenApiForGeneratedResources() {
.body("info.title", Matchers.equalTo("quarkus-hibernate-reactive-rest-data-panache-deployment API"))
.body("paths.'/collections'", Matchers.hasKey("get"))
.body("paths.'/collections'.get.tags", Matchers.hasItem("CollectionsResource"))
.body("paths.'/collections'.get.responses.'200'.content.'application/json'.schema.type", is("array"))
.body("paths.'/collections'.get.responses.'200'.content.'application/json'.schema.items.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections'", Matchers.hasKey("post"))
.body("paths.'/collections'.post.tags", Matchers.hasItem("CollectionsResource"))
.body("paths.'/collections'.post.requestBody.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections'.post.responses.'201'.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections'.post.security[0].SecurityScheme", Matchers.hasItem("user"))
.body("paths.'/collections/{id}'", Matchers.hasKey("get"))
.body("paths.'/collections/{id}'.get.responses.'200'.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections/{id}'.get.security[0].SecurityScheme", Matchers.hasItem("user"))
.body("paths.'/collections/{id}'", Matchers.hasKey("put"))
.body("paths.'/collections/{id}'.put.requestBody.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections/{id}'.put.responses.'201'.content.'application/json'.schema.$ref",
is(COLLECTIONS_SCHEMA_REF))
.body("paths.'/collections/{id}'.put.security[0].SecurityScheme", Matchers.hasItem("user"))
.body("paths.'/collections/{id}'", Matchers.hasKey("delete"))
.body("paths.'/collections/{id}'.delete.responses", Matchers.hasKey("204"))
.body("paths.'/collections/{id}'.delete.security[0].SecurityScheme", Matchers.hasItem("admin"))
.body("paths.'/empty-list-items'", Matchers.hasKey("get"))
.body("paths.'/empty-list-items'.get.tags", Matchers.hasItem("EmptyListItemsResource"))
Expand Down
Expand Up @@ -110,6 +110,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res
addConsumesAnnotation(methodCreator, APPLICATION_JSON);
addProducesJsonAnnotation(methodCreator, resourceProperties);
addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL);
addOpenApiResponseAnnotation(methodCreator, Response.Status.CREATED, resourceMetadata.getEntityType());
addSecurityAnnotations(methodCreator, resourceProperties);
// Add parameter annotations
if (hasValidatorCapability()) {
Expand Down
Expand Up @@ -80,6 +80,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res
addGetAnnotation(methodCreator);
addProducesAnnotation(methodCreator, APPLICATION_JSON);
addPathAnnotation(methodCreator, appendToPath(resourceProperties.getPath(RESOURCE_METHOD_NAME), RESOURCE_METHOD_NAME));
addOpenApiResponseAnnotation(methodCreator, Response.Status.OK, Long.class, false);
addSecurityAnnotations(methodCreator, resourceProperties);
if (!isResteasyClassic()) {
// We only add the Links annotation in Resteasy Reactive because Resteasy Classic ignores the REL parameter:
Expand Down
Expand Up @@ -90,6 +90,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res
addDeleteAnnotation(methodCreator);
addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id");
addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL);
addOpenApiResponseAnnotation(methodCreator, Response.Status.NO_CONTENT);
addSecurityAnnotations(methodCreator, resourceProperties);

ResultHandle resource = methodCreator.readInstanceField(resourceField, methodCreator.getThis());
Expand Down
Expand Up @@ -91,6 +91,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res
addPathAnnotation(methodCreator, appendToPath(resourceProperties.getPath(RESOURCE_METHOD_NAME), "{id}"));
addGetAnnotation(methodCreator);
addProducesJsonAnnotation(methodCreator, resourceProperties);
addOpenApiResponseAnnotation(methodCreator, Response.Status.OK, resourceMetadata.getEntityType());
addSecurityAnnotations(methodCreator, resourceProperties);

addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id");
Expand Down
Expand Up @@ -143,6 +143,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource
addPathAnnotation(methodCreator, resourceProperties.getPath(RESOURCE_METHOD_NAME));
addProducesAnnotation(methodCreator, APPLICATION_JSON);
addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL);
addOpenApiResponseAnnotation(methodCreator, Response.Status.OK, resourceMetadata.getEntityType(), true);
addSecurityAnnotations(methodCreator, resourceProperties);
addSortQueryParamValidatorAnnotation(methodCreator);
addQueryParamAnnotation(methodCreator.getParameterAnnotations(0), "sort");
Expand Down Expand Up @@ -209,6 +210,7 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou
addPathAnnotation(methodCreator, resourceProperties.getPath(RESOURCE_METHOD_NAME));
addProducesAnnotation(methodCreator, APPLICATION_JSON);
addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL);
addOpenApiResponseAnnotation(methodCreator, Response.Status.OK, resourceMetadata.getEntityType(), true);
addSecurityAnnotations(methodCreator, resourceProperties);
addQueryParamAnnotation(methodCreator.getParameterAnnotations(0), "sort");

Expand Down
Expand Up @@ -11,6 +11,7 @@
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import org.jboss.logging.Logger;

Expand All @@ -33,7 +34,11 @@
* A standard JAX-RS method implementor.
*/
public abstract class StandardMethodImplementor implements MethodImplementor {

private static final String OPENAPI_PACKAGE = "org.eclipse.microprofile.openapi.annotations";
private static final String OPENAPI_RESPONSE_ANNOTATION = OPENAPI_PACKAGE + ".responses.APIResponse";
private static final String OPENAPI_CONTENT_ANNOTATION = OPENAPI_PACKAGE + ".media.Content";
private static final String OPENAPI_SCHEMA_ANNOTATION = OPENAPI_PACKAGE + ".media.Schema";
private static final String SCHEMA_TYPE_ARRAY = "ARRAY";
private static final String ROLES_ALLOWED_ANNOTATION = "javax.annotation.security.RolesAllowed";
private static final Logger LOGGER = Logger.getLogger(StandardMethodImplementor.class);

Expand Down Expand Up @@ -156,6 +161,43 @@ protected void addSecurityAnnotations(AnnotatedElement element, ResourceProperti
}
}

protected void addOpenApiResponseAnnotation(AnnotatedElement element, Response.Status status) {
if (capabilities.isPresent(Capability.SMALLRYE_OPENAPI)) {
element.addAnnotation(OPENAPI_RESPONSE_ANNOTATION)
.add("responseCode", String.valueOf(status.getStatusCode()));
}
}

protected void addOpenApiResponseAnnotation(AnnotatedElement element, Response.Status status, String entityType) {
addOpenApiResponseAnnotation(element, status, entityType, false);
}

protected void addOpenApiResponseAnnotation(AnnotatedElement element, Response.Status status, String entityType,
boolean isList) {
if (capabilities.isPresent(Capability.SMALLRYE_OPENAPI)) {
addOpenApiResponseAnnotation(element, status, toClass(entityType), isList);
}
}

protected void addOpenApiResponseAnnotation(AnnotatedElement element, Response.Status status, Class<?> clazz,
boolean isList) {
if (capabilities.isPresent(Capability.SMALLRYE_OPENAPI)) {
AnnotationCreator schemaAnnotation = AnnotationCreator.of(OPENAPI_SCHEMA_ANNOTATION)
.add("implementation", clazz);

if (isList) {
schemaAnnotation.add("type", SCHEMA_TYPE_ARRAY);
}

element.addAnnotation(OPENAPI_RESPONSE_ANNOTATION)
.add("responseCode", String.valueOf(status.getStatusCode()))
.add("content", new Object[] { AnnotationCreator.of(OPENAPI_CONTENT_ANNOTATION)
.add("mediaType", APPLICATION_JSON)
.add("schema", schemaAnnotation)
});
}
}

protected String appendToPath(String path, String suffix) {
if (path.endsWith("/")) {
path = path.substring(0, path.lastIndexOf("/"));
Expand All @@ -181,4 +223,13 @@ protected boolean isResteasyClassic() {
protected boolean isNotReactivePanache() {
return !capabilities.isPresent(Capability.HIBERNATE_REACTIVE);
}

private static Class<?> toClass(String className) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
return classLoader.loadClass(className);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("The class (" + className + ") cannot be found during deployment.", e);
}
}
}
Expand Up @@ -143,6 +143,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res
addConsumesAnnotation(methodCreator, APPLICATION_JSON);
addProducesJsonAnnotation(methodCreator, resourceProperties);
addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL);
addOpenApiResponseAnnotation(methodCreator, Response.Status.CREATED, resourceMetadata.getEntityType());
addSecurityAnnotations(methodCreator, resourceProperties);
// Add parameter annotations
if (hasValidatorCapability()) {
Expand Down

0 comments on commit ec692da

Please sign in to comment.