From 8454a25ee41729157274edbe2b950fff6a86be23 Mon Sep 17 00:00:00 2001 From: Pascal <23715608+pk-work@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:33:30 +0100 Subject: [PATCH] fix: OpenAPI examples (#464) --- openapi-examples/README.adoc | 6 +- .../example/openapi/ValidateRequest.java | 28 ++- .../RequestValidationExample.java | 85 +++++-- .../ResponseValidationExample.java | 33 ++- .../example/web/openapi_router/petstore.json | 211 ------------------ .../example/web/openapi_router}/petstore.yaml | 14 +- 6 files changed, 128 insertions(+), 249 deletions(-) delete mode 100644 web-examples/src/main/resources/io/vertx/example/web/openapi_router/petstore.json rename web-examples/src/main/resources/{ => io/vertx/example/web/openapi_router}/petstore.yaml (88%) diff --git a/openapi-examples/README.adoc b/openapi-examples/README.adoc index f3075376b..bfb1feade 100644 --- a/openapi-examples/README.adoc +++ b/openapi-examples/README.adoc @@ -1,9 +1,9 @@ -= Vert.x JUnit 5 examples += Vert.x OpenAPIs examples This project illustrates how to use https://vertx.io/docs/vertx-openapi/java/[Vert.x OpenAPI] for validating incoming requests. -The link:src/test/java/io/vertx/example/openapi/CreateContract.java[CreateContract.java] file contains examples to explain how an OpenAPIContract class can be instantiated. +The link:src/main/java/io/vertx/example/openapi/CreateContract.java[CreateContract.java] file contains examples to explain how an OpenAPIContract class can be instantiated. -The link:src/test/java/io/vertx/example/openapi/ValidateRequest.java[ValidateRequest.java] file contains examples to explain how an incoming request can be validated against an OpenAPIContract. +The link:src/main/java/io/vertx/example/openapi/ValidateRequest.java[ValidateRequest.java] file contains examples to explain how an incoming request can be validated against an OpenAPIContract. It is highly recommended to also check the https://vertx.io/docs/vertx-web-openapi-router/java/[Vert.x OpenAPI Router], in case you haven't heard about it yet. diff --git a/openapi-examples/src/main/java/io/vertx/example/openapi/ValidateRequest.java b/openapi-examples/src/main/java/io/vertx/example/openapi/ValidateRequest.java index e3c67a93b..eea6aaf3a 100644 --- a/openapi-examples/src/main/java/io/vertx/example/openapi/ValidateRequest.java +++ b/openapi-examples/src/main/java/io/vertx/example/openapi/ValidateRequest.java @@ -13,6 +13,8 @@ package io.vertx.example.openapi; import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClient; import io.vertx.openapi.validation.RequestValidator; public class ValidateRequest { @@ -20,16 +22,32 @@ public class ValidateRequest { public static void main(String[] args) { Vertx vertx = Vertx.vertx(); - CreateContract.createContract(vertx).map(openAPIContract -> RequestValidator.create(vertx, openAPIContract)) - .onSuccess(requestValidator -> { + CreateContract.createContract(vertx).map(openAPIContract -> { + // creates the RequestValidator + return RequestValidator.create(vertx, openAPIContract); + }).onSuccess(requestValidator -> { System.out.println("RequestValidator created"); vertx.createHttpServer().requestHandler(req -> + // validate the request requestValidator.validate(req).onFailure(t -> { System.out.println("Request is invalid: " + t.getMessage()); req.response().setStatusCode(400).end(t.getMessage()); - }).onSuccess(validatedRequest -> System.out.println("Request is valid")) - ).listen(8080, "localhost"); - System.out.println("HttpServer created"); + }).onSuccess(validatedRequest -> { + System.out.println("Request is valid"); + req.response().setStatusCode(200).end(); + }) + ).listen(8080, "localhost").onSuccess(server -> { + System.out.println("HttpServer started on port " + server.actualPort()); + + WebClient.create(vertx) + .post(server.actualPort(), "localhost", "/v1/pets") + // send post request with a payload that does not fit to the contract + .sendJson(new JsonObject()) + .onSuccess(response -> { + System.out.println("Response status code expected 400: " + response.statusCode()); + System.exit(0); + }); + }); }).onFailure(t -> { t.printStackTrace(); System.exit(1); diff --git a/web-examples/src/main/java/io/vertx/example/web/openapi_router/RequestValidationExample.java b/web-examples/src/main/java/io/vertx/example/web/openapi_router/RequestValidationExample.java index 9c82064b7..83478ddb0 100644 --- a/web-examples/src/main/java/io/vertx/example/web/openapi_router/RequestValidationExample.java +++ b/web-examples/src/main/java/io/vertx/example/web/openapi_router/RequestValidationExample.java @@ -2,7 +2,10 @@ import io.vertx.core.Future; import io.vertx.core.VerticleBase; +import io.vertx.core.http.HttpServer; +import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; +import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.launcher.application.VertxApplication; import io.vertx.openapi.contract.OpenAPIContract; @@ -16,38 +19,92 @@ public class RequestValidationExample extends VerticleBase { public static void main(String[] args) { - VertxApplication.main(new String[]{ResponseValidationExample.class.getName()}); + VertxApplication.main(new String[]{RequestValidationExample.class.getName()}); } private String getContractFilePath() { - Path resourceDir = Paths.get("src", "test", "resources"); + Path resourceDir = Paths.get("src", "main", "resources"); Path packagePath = Paths.get(this.getClass().getPackage().getName().replace(".", "/")); - return resourceDir.resolve(packagePath).resolve("petstore.json").toString(); + Path filePath = resourceDir.resolve(packagePath).resolve("petstore.yaml"); + if (filePath.toAbsolutePath().toString().contains("web-examples")) { + return filePath.toString(); + } else { + return Path.of("web-examples").resolve(filePath).toString(); + } } + + private JsonObject buildPet(int id, String name) { + return new JsonObject().put("id", id).put("name", name); + } + + @Override public Future start() { - return OpenAPIContract + JsonObject expectedPet = buildPet(1337, "Foo"); + + Future serverStarted = OpenAPIContract .from(vertx, getContractFilePath()) .compose(contract -> { // Create the RouterBuilder RouterBuilder routerBuilder = RouterBuilder.create(vertx, contract); // Add handler for Operation showPetById - routerBuilder.getRoute("showPetById").addHandler(routingContext -> { + routerBuilder.getRoute("createPets").addHandler(routingContext -> { // Get the validated request ValidatedRequest validatedRequest = routingContext.get(KEY_META_DATA_VALIDATED_REQUEST); // Get the parameter value - int petId = validatedRequest.getPathParameters().get("petId").getInteger(); + JsonObject newPet = validatedRequest.getBody().getJsonObject(); + if (newPet.equals(expectedPet)) { + System.out.println("Request is valid"); + } + + routingContext.response().setStatusCode(201).end(); + }); + + Router basePathRouter = Router.router(vertx); + // Create the OpenAPi Router and mount it on the base path (must match the contract) + basePathRouter.route("/v1/*").subRouter(routerBuilder.createRouter()); + + return vertx.createHttpServer().requestHandler(basePathRouter).listen(0, "localhost"); + }).onSuccess(server -> { + System.out.println("Server started on port " + server.actualPort()); + }).onFailure(t -> { + t.printStackTrace(); + System.exit(1); + }); + + /** + * Send a request that does fit to the contract + */ + serverStarted.onSuccess(server -> { + WebClient.create(vertx) + .post(server.actualPort(), "localhost", "/v1/pets") + // send post request with a payload that does fit to the contract + .sendJson(expectedPet) + .onSuccess(response -> { + System.out.println("Response status code expected 201: " + response.statusCode()); + }).onFailure(t -> { + t.printStackTrace(); + System.exit(1); + }); + }); - // Due to the fact that we don't validate the resonse here, you can send back a response, - // that doesn't fit to the contract - routingContext.response().setStatusCode(200).end(); + /** + * Send a request that does not fit to the contract + */ + serverStarted.onSuccess(server -> { + WebClient.create(vertx) + .post(server.actualPort(), "localhost", "/v1/pets") + // send post request with a payload that does not fit to the contract + .sendJson(new JsonObject()) + .onSuccess(response -> { + System.out.println("Response status code expected 400: " + response.statusCode()); + }).onFailure(t -> { + t.printStackTrace(); + System.exit(1); }); + }); - // Create the Router - Router router = routerBuilder.createRouter(); - return vertx.createHttpServer().requestHandler(router).listen(0, "localhost"); - }) - .onSuccess(server -> System.out.println("Server started on port " + server.actualPort())); + return serverStarted; } } diff --git a/web-examples/src/main/java/io/vertx/example/web/openapi_router/ResponseValidationExample.java b/web-examples/src/main/java/io/vertx/example/web/openapi_router/ResponseValidationExample.java index 812943f7a..65e7be70e 100644 --- a/web-examples/src/main/java/io/vertx/example/web/openapi_router/ResponseValidationExample.java +++ b/web-examples/src/main/java/io/vertx/example/web/openapi_router/ResponseValidationExample.java @@ -4,6 +4,7 @@ import io.vertx.core.VerticleBase; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; +import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.openapi.router.OpenAPIRoute; import io.vertx.ext.web.openapi.router.RouterBuilder; import io.vertx.launcher.application.VertxApplication; @@ -21,9 +22,14 @@ public static void main(String[] args) { } private String getContractFilePath() { - Path resourceDir = Paths.get("src", "test", "resources"); + Path resourceDir = Paths.get("src", "main", "resources"); Path packagePath = Paths.get(this.getClass().getPackage().getName().replace(".", "/")); - return resourceDir.resolve(packagePath).resolve("petstore.json").toString(); + Path filePath = resourceDir.resolve(packagePath).resolve("petstore.yaml"); + if (filePath.toAbsolutePath().toString().contains("web-examples")) { + return filePath.toString(); + } else { + return Path.of("web-examples").resolve(filePath).toString(); + } } private JsonObject buildPet(int id, String name) { @@ -55,9 +61,24 @@ public Future start() { }); // Create the Router - Router router = routerBuilder.createRouter(); - return vertx.createHttpServer().requestHandler(router).listen(0, "localhost"); - }) - .onSuccess(server -> System.out.println("Server started on port " + server.actualPort())); + Router basePathRouter = Router.router(vertx); + // Create the OpenAPi Router and mount it on the base path (must match the contract) + basePathRouter.route("/v1/*").subRouter(routerBuilder.createRouter()); + + return vertx.createHttpServer().requestHandler(basePathRouter).listen(0, "localhost"); + }).onSuccess(server -> { + System.out.println("Server started on port " + server.actualPort()); + + WebClient.create(vertx) + .get(server.actualPort(), "localhost", "/v1/pets/1337") + // send request with a payload that does fit to the contract + .send().onSuccess(response -> { + System.out.println("Response status code expected 200: " + response.statusCode()); + System.exit(0); + }).onFailure(t -> { + t.printStackTrace(); + System.exit(1); + }); + }); } } diff --git a/web-examples/src/main/resources/io/vertx/example/web/openapi_router/petstore.json b/web-examples/src/main/resources/io/vertx/example/web/openapi_router/petstore.json deleted file mode 100644 index 759487107..000000000 --- a/web-examples/src/main/resources/io/vertx/example/web/openapi_router/petstore.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "version": "1.0.0", - "title": "Swagger Petstore", - "license": { - "identifier": "MIT", - "name": "MIT License" - } - }, - "servers": [ - { - "url": "https://petstore.swagger.io/petstore" - } - ], - "security": [ - { - "BasicAuth": [] - } - ], - "paths": { - "/pets": { - "get": { - "summary": "List all pets", - "operationId": "listPets", - "tags": [ - "pets" - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "How many items to return at one time (max 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "A paged array of pets", - "headers": { - "x-next": { - "description": "A link to the next page of responses", - "schema": { - "type": "string" - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pets" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - }, - "post": { - "summary": "Create a pet", - "operationId": "createPets", - "tags": [ - "pets" - ], - "requestBody": { - "description": "Pet to add to the store", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewPet" - } - } - } - }, - "responses": { - "201": { - "description": "Null response" - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/pets/{petId}": { - "get": { - "summary": "Info for a specific pet", - "operationId": "showPetById", - "tags": [ - "pets" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "required": true, - "description": "The id of the pet to retrieve", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Expected response to a valid request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - } - }, - "404": { - "description": "Pet not found" - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "Pet": { - "allOf": [ - { - "$ref": "#/components/schemas/NewPet" - }, - { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64" - } - } - } - ] - }, - "NewPet": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - }, - "tag": { - "type": "string" - } - } - }, - "Pets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - }, - "Error": { - "type": "object", - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - } - } - } -} \ No newline at end of file diff --git a/web-examples/src/main/resources/petstore.yaml b/web-examples/src/main/resources/io/vertx/example/web/openapi_router/petstore.yaml similarity index 88% rename from web-examples/src/main/resources/petstore.yaml rename to web-examples/src/main/resources/io/vertx/example/web/openapi_router/petstore.yaml index 4649bb7c3..794789f50 100644 --- a/web-examples/src/main/resources/petstore.yaml +++ b/web-examples/src/main/resources/io/vertx/example/web/openapi_router/petstore.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.0 +openapi: 3.1.0 info: version: 1.0.0 title: Swagger Petstore @@ -62,8 +62,6 @@ paths: get: summary: Info for a specific pet operationId: showPetById - security: - - api_key: [] tags: - pets parameters: @@ -79,7 +77,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Pets" + $ref: "#/components/schemas/Pet" + 404: + description: Pet not found default: description: unexpected error content: @@ -116,9 +116,3 @@ components: format: int32 message: type: string - securitySchemes: - api_key: - type: apiKey - name: api_key - in: header - openIdConnectUrl: "http://www.example.com" # I don't know why but this parameter is required even if type is basic or apiKey. I think It's OAS bug