Skip to content

Commit

Permalink
Merge pull request #1516 from jcarranzan/vertx-web-validation
Browse files Browse the repository at this point in the history
Implement Vert.x web validation module
  • Loading branch information
michalvavrik authored Nov 15, 2023
2 parents 61e216e + fbee453 commit 49c4a46
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,13 @@ Vert.x Mutiny webClient exploratory test.

Also see http/vertx-web-client/README.md

### `http/vertx-web-validation`
Ensure that you can deploy a simple Quarkus application with predefined routes using schema parser from vertx-json-schema and Router Vert.x approach,
incorporating web validation configuration through ValidationHandlerBuilder. One of the goal of vertx-web-validation functionality is to validate parameters and bodies of incoming requests.

It also verifies multiple deployment strategies like:
- Using Quarkus OpenShift extension

### `http/graphql`
This module covers some basic scenarios around GraphQL.

Expand Down
27 changes: 27 additions & 0 deletions http/vertx-web-validation/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.quarkus.ts.qe</groupId>
<artifactId>parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../..</relativePath>
</parent>
<artifactId>vertx-web-validation</artifactId>
<packaging>jar</packaging>
<name>Quarkus QE TS: HTTP: Vert.x-Web-Validation</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-validation</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-json-schema</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.ts.vertx.web.validation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/shoppinglist")
@Produces(MediaType.APPLICATION_JSON)
public class ShopResource {

private static List<ShoppingList> shoppingList = createSampleProductList();

private static List<ShoppingList> createSampleProductList() {
shoppingList = new ArrayList<>();
shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName1", 25,
new ArrayList<>(Arrays.asList("Carrots", "Water", "Cheese", "Beer"))));
shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName2", 80,
new ArrayList<>(Arrays.asList("Meat", "Wine", "Almonds", "Potatoes", "Cake"))));
return shoppingList;
}

@GET
public List<ShoppingList> get() {
return shoppingList;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.ts.vertx.web.validation;

import java.util.ArrayList;
import java.util.UUID;

public class ShoppingList {

public UUID id;

public String name;

public ArrayList<String> products;

public double price;

public ShoppingList(UUID id, String name, double price, ArrayList<String> products) {
this.id = id;
this.name = name;
this.price = price;
this.products = products;
}

public ArrayList<String> getProducts() {
return products;
}

public void setProducts(ArrayList<String> products) {
this.products = products;
}

public UUID getId() {
return id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

@Override
public String toString() {
return String.format(
"Shopping list{id=%s, name=%s, products=%s, price=%s}",
getId(),
getName(),
getProducts().toString(),
getPrice());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.quarkus.ts.vertx.web.validation;

import static io.vertx.ext.web.validation.builder.Parameters.param;
import static io.vertx.json.schema.common.dsl.Schemas.arraySchema;
import static io.vertx.json.schema.common.dsl.Schemas.numberSchema;
import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
import static io.vertx.json.schema.common.dsl.Schemas.stringSchema;
import static io.vertx.json.schema.draft7.dsl.Keywords.maximum;

import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Vertx;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.validation.BadRequestException;
import io.vertx.ext.web.validation.BodyProcessorException;
import io.vertx.ext.web.validation.ParameterProcessorException;
import io.vertx.ext.web.validation.RequestParameters;
import io.vertx.ext.web.validation.RequestPredicate;
import io.vertx.ext.web.validation.RequestPredicateException;
import io.vertx.ext.web.validation.ValidationHandler;
import io.vertx.ext.web.validation.builder.Bodies;
import io.vertx.ext.web.validation.builder.Parameters;
import io.vertx.ext.web.validation.builder.ValidationHandlerBuilder;
import io.vertx.json.schema.SchemaParser;
import io.vertx.json.schema.SchemaRouter;
import io.vertx.json.schema.SchemaRouterOptions;
import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder;

public class ValidationHandlerOnRoutes {
//TODO when Quarkus use vert.x version 4.4.6 we can use SchemaRepository instead of SchemaParser with SchemaRouter
//private SchemaRepository schemaRepository =SchemaRepository.create(new JsonSchemaOptions().setDraft(Draft.DRAFT7).setBaseUri(BASEURI));
private SchemaParser schemaParser;
private SchemaRouter schemaRouter;

@Inject
Vertx vertx;

private static ShopResource shopResource = new ShopResource();

private static final String ERROR_MESSAGE = "{\"error\": \"%s\"}";
private static final String SHOPPINGLIST_NOT_FOUND = "Shopping list not found in the list or does not exist with that name or price";

@PostConstruct
void initialize() {
schemaParser = createSchema();
}

private SchemaParser createSchema() {
schemaRouter = SchemaRouter.create(vertx, new SchemaRouterOptions());
schemaParser = SchemaParser.createDraft7SchemaParser(schemaRouter);
return schemaParser;
}

public void validateHandlerShoppingList(@Observes Router router) {
AtomicReference<String> queryAnswer = new AtomicReference<>();
router.get("/filterList")
.handler(ValidationHandlerBuilder
.create(schemaParser)
.queryParameter(param("shoppingListName", stringSchema()))
.queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100)))).build())
.handler(routingContext -> {
RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
String shoppingListName = parameters.queryParameter("shoppingListName").getString();
Double totalPrice = parameters.queryParameter("shoppingListPrice").getDouble();

// Logic to list shoppingList based on shoppingListName and totalPrice
String shoppingListFound = fetchProductDetailsFromQuery(shoppingListName, totalPrice);
queryAnswer.set(shoppingListFound);

if (queryAnswer.get().equalsIgnoreCase(SHOPPINGLIST_NOT_FOUND)) {
routingContext.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code());
}

routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN).end(queryAnswer.get());
}).failureHandler(routingContext -> {
// Error handling:
if (routingContext.failure() instanceof BadRequestException ||
routingContext.failure() instanceof ParameterProcessorException ||
routingContext.failure() instanceof BodyProcessorException ||
routingContext.failure() instanceof RequestPredicateException) {

String errorMessage = routingContext.failure().toString();
routingContext.response()
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.end(String.format(ERROR_MESSAGE, errorMessage));
} else {
routingContext.next();
}

});
// Create a ValidationHandlerBuilder with explodedParam and arraySchema to filter by array items
ObjectSchemaBuilder bodySchemaBuilder = objectSchema()
.property("shoppingListName", stringSchema());
ValidationHandlerBuilder
.create(schemaParser)
.body(Bodies.json(bodySchemaBuilder));
router.get("/filterByArrayItem")
.handler(
ValidationHandlerBuilder
.create(schemaParser)
.queryParameter(Parameters.explodedParam("shoppingArray", arraySchema().items(stringSchema())))
.body(Bodies.json(bodySchemaBuilder))
.build())
.handler(routingContext -> {
RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
JsonArray myArray = parameters.queryParameter("shoppingArray").getJsonArray();
// Retrieve the list of all shoppingLists
List<ShoppingList> shoppingLists = fetchProductDetailsFromArrayQuery(myArray);

routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.end(Json.encodeToBuffer(shoppingLists));
});
// Let's allow to create a new item
router.post("/createShoppingList").handler(
ValidationHandlerBuilder
.create(schemaParser)
.predicate(RequestPredicate.BODY_REQUIRED)
.queryParameter(param("shoppingListName", stringSchema()))
.queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100))))
.build())
.handler(routingContext -> {
routingContext.response().setStatusCode(HttpResponseStatus.OK.code()).end("Shopping list created");
});

}

public List<ShoppingList> fetchProductDetailsFromArrayQuery(JsonArray myArray) {
return shopResource.get().stream()
.filter(shoppingList -> myArray.contains(shoppingList.getName()))
.collect(Collectors.toList());
}

public String fetchProductDetailsFromQuery(String name, Double price) {
return shopResource.get().stream()
.filter(product -> name.equalsIgnoreCase(product.getName()) && price.equals(product.getPrice()))
.map(ShoppingList::toString)
.findFirst()
.orElse(SHOPPINGLIST_NOT_FOUND);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.ts.vertx.web.validation;

import io.quarkus.test.scenarios.OpenShiftScenario;

@OpenShiftScenario
public class OpenShiftVertxWebValidationIT extends VertxWebValidationIT {
}
Loading

0 comments on commit 49c4a46

Please sign in to comment.