Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow APIResponse on resource class #524

Merged
merged 2 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@
* }
* </pre>
* <p>
* When this annotation is applied to an <code>ExceptionMapper</code>, it allows developers to describe the API response
* that will be added to a generated OpenAPI operation based on a JAX-RS method that declares an <code>Exception</code>
* of the type handled by the <code>ExceptionMapper</code>.
* When this annotation is applied to a JAX-RS resource class, the response is added to the responses defined in all
* OpenAPI operations which correspond to a method on that class. If an operation already has a response with the
* specified responseCode the response is not added to that operation.
*
* <p>
* When this annotation is applied to an <code>ExceptionMapper</code> class or <code>toResponse</code> method, it allows
* developers to describe the API response that will be added to a generated OpenAPI operation based on a JAX-RS method
* that declares an <code>Exception</code> of the type handled by the <code>ExceptionMapper</code>.
*
* <pre>
* &#64;Provider
Expand All @@ -67,7 +72,7 @@
* @see "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject"
*
**/
@Target({ElementType.METHOD})
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Repeatable(APIResponses.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* @see <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responses-object">Responses
* Object</a>
**/
@Target({ElementType.METHOD})
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface APIResponses {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2022 Contributors to the Eclipse Foundation
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations
* under the License.
*/
package org.eclipse.microprofile.openapi.apps.airlines.exception;

@SuppressWarnings("serial")
public class ReviewRejectedException extends Exception {

public ReviewRejectedException(String rejectionReason, Throwable cause) {
super(rejectionReason, cause);
}

public ReviewRejectedException(String rejectionReason) {
super(rejectionReason);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2022 Contributors to the Eclipse Foundation
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations
* under the License.
*/
package org.eclipse.microprofile.openapi.apps.airlines.exception;

import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.apps.airlines.exception.ReviewRejectedExceptionMapper.RejectionResponse;

import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;

@APIResponse(responseCode = "400", description = "The review was rejected", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = RejectionResponse.class)))
public class ReviewRejectedExceptionMapper implements ExceptionMapper<ReviewRejectedException> {

@Override
public Response toResponse(ReviewRejectedException exception) {
RejectionResponse response = new RejectionResponse();
response.setReason(exception.getMessage());
return Response.status(Status.BAD_REQUEST).entity(response).build();
}

public static class RejectionResponse {
@Schema(description = "The reason the review was rejected")
private String reason;

public String getReason() {
return reason;
}

public void setReason(String reason) {
this.reason = reason;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.eclipse.microprofile.openapi.annotations.servers.Servers;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;
import org.eclipse.microprofile.openapi.apps.airlines.exception.ReviewRejectedException;
import org.eclipse.microprofile.openapi.apps.airlines.model.Airline;
import org.eclipse.microprofile.openapi.apps.airlines.model.Review;
import org.eclipse.microprofile.openapi.apps.airlines.model.User;
Expand Down Expand Up @@ -74,6 +75,8 @@
@Tag(name = "Reviews", description = "All the review methods"),
@Tag(name = "Ratings", description = "All the ratings methods")
})
@APIResponse(responseCode = "429", description = "Client is rate limited")
@APIResponse(responseCode = "500", description = "Server error")
public class ReviewResource {

private static Map<Integer, Review> reviews = new ConcurrentHashMap<Integer, Review>();
Expand Down Expand Up @@ -231,7 +234,7 @@ public Response getReviewByAirlineAndUser(
@Operation(summary = "Create a Review", operationId = "createReview")
@Consumes("application/json")
@Produces("application/json")
public Response createReview(Review review) {
public Response createReview(Review review) throws ReviewRejectedException {
reviews.put(currentId, review);
return Response.status(Status.CREATED).entity("{\"id\":" + currentId++ + "}").build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
@Produces({"application/json", "application/xml"})
@SecurityScheme(description = "user security scheme", type = SecuritySchemeType.HTTP, securitySchemeName = "httpSchemeForTest", scheme = "testScheme")
@SecurityRequirement(name = "httpSchemeForTest")
@APIResponse(responseCode = "400", description = "Invalid request")
public class UserResource {

private static UserData userData = new UserData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,17 @@ public void testAPIResponse(String type) {
vr.body("paths.'/bookings'.post.responses", aMapWithSize(1));
vr.body("paths.'/bookings'.post.responses.'201'.description", equalTo("Booking created"));

// @APIResponse at class level overridden at method level
vr.body("paths.'/user/{username}'.delete.responses", aMapWithSize(3));
vr.body("paths.'/user/{username}'.delete.responses.'200'.description", equalTo("User deleted successfully"));
vr.body("paths.'/user/{username}'.delete.responses.'400'.description", equalTo("Invalid username supplied"));
vr.body("paths.'/user/{username}'.delete.responses.'404'.description", equalTo("User not found"));

// @APIResponse at class level combined with method level
vr.body("paths.'/user/{username}'.patch.responses", aMapWithSize(2));
vr.body("paths.'/user/{username}'.patch.responses.'200'.description",
equalTo("Password was changed successfully"));
vr.body("paths.'/user/{username}'.patch.responses.'400'.description", equalTo("Invalid request"));
}

@RunAsClient
Expand All @@ -310,14 +317,17 @@ public void testAPIResponses(String type) {
vr.body("paths.'/bookings/{id}'.get.responses.'200'.description", equalTo("Booking retrieved"));
vr.body("paths.'/bookings/{id}'.get.responses.'404'.description", equalTo("Booking not found"));

vr.body("paths.'/reviews/users/{user}'.get.responses", aMapWithSize(2));
vr.body("paths.'/reviews/users/{user}'.get.responses.'200'.description", equalTo("Review(s) retrieved"));
vr.body("paths.'/reviews/users/{user}'.get.responses.'404'.description", equalTo("Review(s) not found"));

vr.body("paths.'/user/{username}'.put.responses", aMapWithSize(3));
vr.body("paths.'/user/{username}'.put.responses.'200'.description", equalTo("User updated successfully"));
vr.body("paths.'/user/{username}'.put.responses.'400'.description", equalTo("Invalid user supplied"));
vr.body("paths.'/user/{username}'.put.responses.'404'.description", equalTo("User not found"));

// @APIResponses on body combined with annotations on method
vr.body("paths.'/reviews/users/{user}'.get.responses", aMapWithSize(4));
vr.body("paths.'/reviews/users/{user}'.get.responses.'200'.description", equalTo("Review(s) retrieved"));
vr.body("paths.'/reviews/users/{user}'.get.responses.'404'.description", equalTo("Review(s) not found"));
vr.body("paths.'/reviews/users/{user}'.get.responses.'429'.description", equalTo("Client is rate limited"));
vr.body("paths.'/reviews/users/{user}'.get.responses.'500'.description", equalTo("Server error"));
}

@RunAsClient
Expand Down Expand Up @@ -1019,6 +1029,14 @@ public void testExceptionMappers(String type) {
vr.body("paths.'/user/{username}'.get.responses.'404'.description", equalTo("Not Found"));

vr.body("paths.'/user/{id}'.get.responses.'404'.content.'application/json'.schema", notNullValue());

vr.body("paths.'/reviews'.post.responses.'400'.description", equalTo("The review was rejected"));
vr.body("paths.'/reviews'.post.responses.'400'.content.'application/json'.schema", notNullValue());

String rejectedReviewSchema =
dereference(vr, "paths.'/reviews'.post.responses.'400'.content.'application/json'.schema");
vr.body(rejectedReviewSchema + ".type", equalTo("object"));
vr.body(rejectedReviewSchema + ".properties", hasKey("reason"));
}

}