diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/Components.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/Components.java index abb1d56c..122db138 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/Components.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/Components.java @@ -107,6 +107,15 @@ */ Callback[] callbacks() default {}; + /** + * An object to hold reusable Path Item Objects. + * + * @return the reusable PathItem objects. + * + * @since 4.0 + */ + PathItem[] pathItems() default {}; + /** * List of extensions to be added to the {@link org.eclipse.microprofile.openapi.models.Components Components} model * corresponding to the containing annotation. diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java index 2459313f..9f2da51e 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/OpenAPIDefinition.java @@ -93,6 +93,15 @@ */ ExternalDocumentation externalDocs() default @ExternalDocumentation; + /** + * An array of webhook definitions which the API may be instructed to call using out-of-band mechanisms. + * + * @return the array of webhooks + * + * @since 4.0 + */ + PathItem[] webhooks() default {}; + /** * An element to hold a set of reusable objects for different aspects of the OpenAPI Specification (OAS). * diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/PathItem.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/PathItem.java new file mode 100644 index 00000000..c6238096 --- /dev/null +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/PathItem.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + *
+ * 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 + *
+ * http://www.apache.org/licenses/LICENSE-2.0 + *
+ * 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.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.servers.Server; + +/** + * Describes a set of operations available on a single path. + * + * @since 4.0 + */ +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PathItem { + + /** + * The name of the Path Item object, used as the map key when the path item is used under + * {@link Components#pathItems()} or {@link OpenAPIDefinition#webhooks()} + * + * @return the path item name + */ + String name(); + + /** + * Reference value to a PathItem object. + *
+ * This property provides a reference to an object defined elsewhere. + *
+ * Unlike {@code ref} on most MP OpenAPI annotations, this property is not mutually exclusive with other + * properties. + * + * @return reference to a path item object definition + **/ + String ref() default ""; + + /** + * The summary of the path item. + * + * @return the summary + */ + String summary() default ""; + + /** + * The description of the path item. + * + * @return the description + */ + String description() default ""; + + /** + * The operations available under this Path Item. + *
+ * The {@link PathItemOperation#method() method} MUST be defined for each operation. + * + * @return the list of operations + */ + PathItemOperation[] operations() default {}; + + /** + * A list of servers to be used for this Path Item, overriding those defined for the whole API. + * + * @return the list of servers + */ + Server[] servers() default {}; + + /** + * A list of parameters which apply to all operations under this path item. + * + * @return the list of parameters + */ + Parameter[] parameters() default {}; + + /** + * List of extensions to be added to the {@link org.eclipse.microprofile.openapi.models.PathItem PathItem} model + * corresponding to the containing annotation. + * + * @return the list of extensions + */ + Extension[] extensions() default {}; +} diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/PathItemOperation.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/PathItemOperation.java new file mode 100644 index 00000000..261928a7 --- /dev/null +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/PathItemOperation.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + *
+ * 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 + *
+ * http://www.apache.org/licenses/LICENSE-2.0 + *
+ * 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.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.microprofile.openapi.annotations.callbacks.Callback; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirementsSet; +import org.eclipse.microprofile.openapi.annotations.servers.Server; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * Describes an Operation + *
+ * This annotation is only used for operations defined under a {@link PathItem}. For operations provided by the API + * itself, it's more common to use the {@link Operation} annotation applied to a Jakarta REST resource method instead. + * + * @since 4.0 + */ +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PathItemOperation { + + /** + * The HTTP method for this operation. + * + * @return the HTTP method of this operation + **/ + String method(); + + /** + * The tags which apply to this operation. + * + * @return the list of tags + */ + Tag[] tags() default {}; + + /** + * Provides a brief description of what this operation does. + * + * @return a summary of this operation + **/ + String summary() default ""; + + /** + * A verbose description of the operation behavior. CommonMark syntax MAY be used for rich text representation. + * + * @return a description of this operation + **/ + String description() default ""; + + /** + * Additional external documentation for this operation. + * + * @return external documentation associated with this operation instance + **/ + ExternalDocumentation externalDocs() default @ExternalDocumentation(); + + /** + * Unique string used to identify the operation. The id MUST be unique among all operations described in the API. + *
+ * Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to + * follow common programming naming conventions. + *
+ * + * @return the ID of this operation + **/ + String operationId() default ""; + + /** + * An array of parameters applicable for this operation. + *+ * The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a name and + * location. + *
+ * + * @return the list of parameters for this operation + **/ + Parameter[] parameters() default {}; + + /** + * The request body for this operation. + * + * @return the request body of this operation + **/ + RequestBody requestBody() default @RequestBody(); + + /** + * The list of possible responses that can be returned when executing this operation. + * + * @return the list of responses for this operation + **/ + APIResponse[] responses() default {}; + + /** + * A list of possible out-of-band callbacks related to this operation. Each entry represents a set of requests that + * may be initiated by the API provided and an expression, evaluated at runtime, that identifies the URL used to + * make those requests. + * + * @return the list of callbacks for this operation + */ + Callback[] callbacks() default {}; + + /** + * Allows an operation to be marked as deprecated + *+ * Consumers SHOULD refrain from usage of a deprecated operation. + *
+ * + * @return whether or not this operation is deprecated + **/ + boolean deprecated() default false; + + /** + * A declaration of which security mechanisms can be used for this callback operation. Only one of the security + * requirement objects need to be satisfied to authorize a request. + *+ * Adding a {@code SecurityRequirement} to this array is equivalent to adding a {@code SecurityRequirementsSet} + * containing a single {@code SecurityRequirement} to {@link #securitySets()}. + *
+ * This definition overrides any declared top-level security. To remove a top-level security declaration, an empty + * array can be used. + * + * @return the list of security mechanisms for this operation + */ + SecurityRequirement[] security() default {}; + + /** + * A declaration of which security mechanisms can be used for this callback operation. All of the security + * requirements within any one of the sets needs needs to be satisfied to authorize a request. + *
+ * This definition overrides any declared top-level security. To remove a top-level security declaration, an empty + * array can be used. + *
+ * Including an empty set within this list indicates that the other requirements are optional. + * + * @return the list of security mechanisms for this operation + */ + SecurityRequirementsSet[] securitySets() default {}; + + /** + * A list of servers to be used for this operation, overriding those defined for the parent path item or for the + * whole API. + * + * @return the list of servers + */ + Server[] servers() default {}; + + /** + * List of extensions to be added to the {@link org.eclipse.microprofile.openapi.models.Operation Operation} model + * corresponding to the containing annotation. + * + * @return array of extensions + */ + Extension[] extensions() default {}; +} diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/Callback.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/Callback.java index 94d3b526..9106a388 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/Callback.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/Callback.java @@ -77,6 +77,16 @@ **/ String ref() default ""; + /** + * Reference value to a Path Item object, to be referenced by the Callback object. + *
+ * This property provides a reference to a Path Item object defined elsewhere. {@code #callbackUrlExpression()} is + * REQUIRED when this property is set. The referenced Path Item will be used for the URL expression specified. + * + * @since 4.0 + */ + String pathItemRef() default ""; + /** * List of extensions to be added to the {@link org.eclipse.microprofile.openapi.models.callbacks.Callback Callback} * model corresponding to the containing annotation. diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/package-info.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/package-info.java index fcb926f5..4eb880e7 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/package-info.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/callbacks/package-info.java @@ -44,6 +44,6 @@ * */ -@org.osgi.annotation.versioning.Version("1.1") +@org.osgi.annotation.versioning.Version("1.2") @org.osgi.annotation.versioning.ProviderType package org.eclipse.microprofile.openapi.annotations.callbacks; \ No newline at end of file diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/package-info.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/package-info.java index 8963a720..6bbd7b54 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/package-info.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/package-info.java @@ -34,5 +34,5 @@ * */ -@org.osgi.annotation.versioning.Version("1.1") +@org.osgi.annotation.versioning.Version("1.2") package org.eclipse.microprofile.openapi.annotations; \ No newline at end of file diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/JAXRSApp.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/JAXRSApp.java index 6ab78821..21fe1053 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/JAXRSApp.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/JAXRSApp.java @@ -20,6 +20,8 @@ import org.eclipse.microprofile.openapi.annotations.Components; import org.eclipse.microprofile.openapi.annotations.ExternalDocumentation; import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.PathItem; +import org.eclipse.microprofile.openapi.annotations.PathItemOperation; import org.eclipse.microprofile.openapi.annotations.callbacks.Callback; import org.eclipse.microprofile.openapi.annotations.callbacks.CallbackOperation; import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn; @@ -60,6 +62,7 @@ import jakarta.ws.rs.core.Application; import jakarta.ws.rs.core.MediaType; +@SuppressWarnings("checkstyle:linelength") // indentation of annotations leads to long lines @ApplicationPath("/") @OpenAPIDefinition( tags = {@Tag(name = "user", description = "Operations about user"), @@ -107,6 +110,27 @@ extensions = @Extension(name = "x-server", value = "test-server")), @Server(url = "https://test-server.com:80/basePath", description = "The test API server") }, + webhooks = { + @PathItem(name = "bookingEvent", + description = "Notifies about booking creation and deletion", + summary = "Booking Events", + operations = { + @PathItemOperation(method = "put", + summary = "Notifies that a booking has been created", + requestBody = @RequestBody(content = @Content(mediaType = "application/json", + schema = @Schema(ref = "#/components/schemas/Booking"))), + responses = @APIResponse(responseCode = "204", + description = "Indicates that the creation event was processed successfully")), + @PathItemOperation(method = "delete", + summary = "Notifies that a booking has been deleted", + requestBody = @RequestBody(content = @Content(mediaType = "application/json", + schema = @Schema(ref = "#/components/schemas/Booking"))), + responses = @APIResponse(responseCode = "204", + description = "Indicates that the deletion event was processed successfully")) + }, + extensions = @Extension(name = "x-webhook", value = "test-webhook")), + @PathItem(name = "userEvent", ref = "UserEvent") + }, components = @Components( schemas = { @Schema(name = "Bookings", title = "Bookings", @@ -228,7 +252,101 @@ @APIResponse(ref = "FoundBookings") })), @Callback(name = "GetBookingsARef", - ref = "#/components/callbacks/GetBookings") + ref = "#/components/callbacks/GetBookings"), + @Callback(name = "UserEvents", + callbackUrlExpression = "http://localhost:9080/users/events", + pathItemRef = "UserEvent") + }, + pathItems = { + @PathItem(name = "UserEvent", + description = "Standard definition for receiving events about users", + summary = "User Event reception API", + operations = { + @PathItemOperation( + method = "PUT", + summary = "User added event", + description = "A user was added", + externalDocs = @ExternalDocumentation(url = "http://example.com/docs"), + operationId = "userAddedEvent", + parameters = @Parameter(name = "authenticated", + description = "Whether the user is authenticated", + in = ParameterIn.QUERY, + schema = @Schema(type = SchemaType.BOOLEAN), + required = false), + requestBody = @RequestBody( + description = "The added user", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(ref = "User"))), + responses = { + @APIResponse(responseCode = "200", + description = "Event received"), + @APIResponse(responseCode = "429", + description = "Server is too busy to process the event. It will be sent again later") + }), + @PathItemOperation( + method = "DELETE", + summary = "A user was deleted", + parameters = @Parameter(name = "id", + in = ParameterIn.QUERY, + schema = @Schema(type = SchemaType.STRING), + required = true), + responses = { + @APIResponse(responseCode = "200", + description = "Event received") + }) + }), + @PathItem(name = "UserEventARef", + ref = "#/components/pathItems/UserEvent", + description = "UserEvent reference", + summary = "Referenced PathItem", + operations = @PathItemOperation( + method = "POST", + summary = "User updated event", + description = "A user was modified", + requestBody = @RequestBody( + description = "The modified user", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(ref = "User"))), + responses = { + @APIResponse(responseCode = "200", + description = "Event received") + })), + @PathItem(name = "CallbackPathItem", + operations = @PathItemOperation( + method = "POST", + responses = @APIResponse(responseCode = "200"), + callbacks = @Callback(name = "getBookings", + ref = "#/components/callbacks/GetBookings"))), + // Test remaining properties on PathItemOperation + @PathItem(name = "OperationTest", + operations = @PathItemOperation( + method = "POST", + responses = @APIResponse(responseCode = "200"), + deprecated = true, + tags = { + @Tag(ref = "create"), + @Tag(name = "pathItemTest", + description = "part of the path item tests") + }, + security = @SecurityRequirement(name = "testScheme1"), + securitySets = @SecurityRequirementsSet({}), + servers = @Server(url = "http://old.example.com/api"), + extensions = @Extension(name = "x-operation", + value = "test operation"))), + // Test remaining properties on PathItem + @PathItem(name = "PathItemTest", + operations = { + @PathItemOperation(method = "POST", + responses = @APIResponse(responseCode = "200")), + @PathItemOperation(method = "PUT", + responses = @APIResponse(responseCode = "200")) + }, + servers = @Server(url = "http://example.com"), + parameters = @Parameter(name = "id", + in = ParameterIn.PATH, + schema = @Schema(type = SchemaType.STRING)), + extensions = @Extension(name = "x-pathItem", + value = "test path item")) }, extensions = @Extension(name = "x-components", value = "test-components")), extensions = @Extension(name = "x-openapi-definition", value = "test-openapi-definition")) diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java index 1642682d..8f5092bc 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java @@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasEntry; @@ -472,6 +473,10 @@ public void testCallbackAnnotations(String type) { endpoint = "paths.'/bookings'.post.callbacks"; vr.body(endpoint, hasKey("bookingCallback")); vr.body(endpoint + ".'bookingCallback'", hasKey("http://localhost:9080/airlines/bookings")); + + endpoint = "components.callbacks.UserEvents"; + vr.body(endpoint, hasKey("http://localhost:9080/users/events")); + vr.body(endpoint + ".'http://localhost:9080/users/events'.$ref", equalTo("#/components/pathItems/UserEvent")); } @Test(dataProvider = "formatProvider") @@ -894,6 +899,7 @@ public void testComponents(String type) { vr.body("components.securitySchemes.httpTestScheme", notNullValue()); vr.body("components.links.UserName", notNullValue()); vr.body("components.callbacks.GetBookings", notNullValue()); + vr.body("components.pathItems.UserEvent", notNullValue()); // Test an extension on the components object itself vr.body("components.x-components", equalTo("test-components")); @@ -1209,6 +1215,82 @@ public void testRef(String type) { vr.body("components.callbacks.GetBookingsARef.$ref", equalTo("#/components/callbacks/GetBookings")); + + vr.body("components.pathItems.UserEventARef.$ref", equalTo("#/components/pathItems/UserEvent")); + vr.body("components.pathItems.UserEventARef.description", equalTo("UserEvent reference")); + vr.body("components.pathItems.UserEventARef.summary", equalTo("Referenced PathItem")); + } + + @Test(dataProvider = "formatProvider") + public void testPathItem(String type) { + ValidatableResponse vr = callEndpoint(type); + + String pathItem = "components.pathItems.UserEvent"; + vr.body(pathItem + ".description", equalTo("Standard definition for receiving events about users")); + vr.body(pathItem + ".summary", equalTo("User Event reception API")); + vr.body(pathItem + ".put", notNullValue()); + vr.body(pathItem + ".delete", notNullValue()); + + pathItem = "components.pathItems.PathItemTest"; + vr.body(pathItem + ".servers[0].url", equalTo("http://example.com")); + vr.body(pathItem + ".parameters[0].name", equalTo("id")); + vr.body(pathItem + ".x-pathItem", equalTo("test path item")); + + pathItem = "components.pathItems.UserEventARef"; + vr.body(pathItem + ".$ref", equalTo("#/components/pathItems/UserEvent")); + vr.body(pathItem + ".post", notNullValue()); + vr.body(pathItem + ".post.summary", equalTo("User updated event")); + } + + @Test(dataProvider = "formatProvider") + public void testPathItemOperation(String type) { + ValidatableResponse vr = callEndpoint(type); + + String op = "components.pathItems.UserEvent.put"; + vr.body(op, notNullValue()); + vr.body(op + ".summary", equalTo("User added event")); + vr.body(op + ".description", equalTo("A user was added")); + vr.body(op + ".externalDocs.url", equalTo("http://example.com/docs")); + vr.body(op + ".operationId", equalTo("userAddedEvent")); + vr.body(op + ".parameters[0].name", equalTo("authenticated")); + vr.body(op + ".requestBody.description", equalTo("The added user")); + vr.body(op + ".responses.'200'.description", equalTo("Event received")); + vr.body(op + ".responses.'429'.description", containsString("Server is too busy")); + + op = "components.pathItems.CallbackPathItem.post"; + vr.body(op, notNullValue()); + vr.body(op + ".callbacks.getBookings.$ref", equalTo("#/components/callbacks/GetBookings")); + + op = "components.pathItems.OperationTest.post"; + vr.body(op, notNullValue()); + vr.body(op + ".tags", containsInAnyOrder("create", "pathItemTest")); + vr.body(op + ".deprecated", equalTo(true)); + vr.body(op + ".security", hasSize(2)); + vr.body(op + ".security", hasItem(anEmptyMap())); + // JsonPath syntax sucks - this expects security to contain two items, one of which + // maps "testScheme1" to an empty list and the other of which doesn't have a "testScheme1" entry. + vr.body(op + ".security.testScheme1", containsInAnyOrder(emptyIterable(), nullValue())); + vr.body(op + ".servers[0].url", equalTo("http://old.example.com/api")); + vr.body(op + ".x-operation", equalTo("test operation")); + + // Check the new tag was created + vr.body("tags.findAll { it.name == 'pathItemTest'}.description", contains("part of the path item tests")); } + @Test(dataProvider = "formatProvider") + public void testWebhooks(String type) { + ValidatableResponse vr = callEndpoint(type); + + String webhook = "webhooks.bookingEvent"; + vr.body(webhook, notNullValue()); + vr.body(webhook + ".description", equalTo("Notifies about booking creation and deletion")); + vr.body(webhook + ".summary", equalTo("Booking Events")); + vr.body(webhook + ".put", notNullValue()); + vr.body(webhook + ".delete", notNullValue()); + vr.body(webhook + ".x-webhook", equalTo("test-webhook")); + + webhook = "webhooks.userEvent"; + vr.body(webhook, notNullValue()); + vr.body(webhook + ".$ref", equalTo("#/components/pathItems/UserEvent")); + } }