From eba2414a1daa28e05b892974c31d77c6dd37cd40 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Fri, 6 Sep 2024 01:06:42 -0400
Subject: [PATCH 1/4] working
---
modules/jooby-avaje-validator/pom.xml | 112 ++++++++++
.../avaje/validator/AvajeValidatorModule.java | 176 +++++++++++++++
.../validator/ConstraintViolationHandler.java | 95 +++++++++
.../jooby/avaje/validator/package-info.java | 1 +
.../src/main/java/module-info.java | 17 ++
.../validator/AvajeValidatorModuleTest.java | 200 ++++++++++++++++++
.../io/jooby/hibernate/validator/app/App.java | 24 +++
.../hibernate/validator/app/Controller.java | 32 +++
.../validator/app/NewAccountRequest.java | 59 ++++++
.../validator/app/PasswordsShouldMatch.java | 22 ++
.../app/PasswordsShouldMatchValidator.java | 15 ++
.../jooby/hibernate/validator/app/Person.java | 35 +++
pom.xml | 28 ++-
13 files changed, 810 insertions(+), 6 deletions(-)
create mode 100644 modules/jooby-avaje-validator/pom.xml
create mode 100644 modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
create mode 100644 modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java
create mode 100644 modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/package-info.java
create mode 100644 modules/jooby-avaje-validator/src/main/java/module-info.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java
diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml
new file mode 100644
index 0000000000..a2fa20eb51
--- /dev/null
+++ b/modules/jooby-avaje-validator/pom.xml
@@ -0,0 +1,112 @@
+
+
+
+
+ io.jooby
+ modules
+ 3.3.1-SNAPSHOT
+
+
+ 4.0.0
+ jooby-avaje-validator
+
+
+
+ io.jooby
+ jooby
+
+
+
+ io.jooby
+ jooby-validation
+ ${project.version}
+
+
+
+
+ io.avaje
+ avaje-validator
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+
+
+ io.jooby
+ jooby-netty
+ test
+
+
+
+ io.jooby
+ jooby-jackson
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+ io.jooby
+ jooby-test
+ test
+
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+ org.assertj
+ assertj-core
+ 3.26.3
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ test
+ test-compile
+
+
+
+
+ -parameters
+
+
+
+ io.jooby
+ jooby-apt
+
+
+ io.avaje
+ avaje-validator-generator
+
+
+
+
+
+
+
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
new file mode 100644
index 0000000000..8b324edc2c
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
@@ -0,0 +1,176 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.avaje.validator;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.avaje.validation.ConstraintViolationException;
+import io.avaje.validation.Validator;
+import io.jooby.Extension;
+import io.jooby.Jooby;
+import io.jooby.StatusCode;
+import io.jooby.validation.MvcValidator;
+
+/**
+ * Avaje Validator Module: https://jooby.io/modules/avaje-validator.
+ *
+ *
{@code
+ * {
+ * install(new AvajeValidatorModule());
+ *
+ * }
+ *
+ * public class Controller {
+ *
+ * @POST("/create")
+ * public void create(@Valid Bean bean) {
+ * }
+ *
+ * }
+ * }
+ *
+ * Supports validation of a single bean, list, array, or map.
+ *
+ *
The module also provides a built-in error handler that catches {@link
+ * ConstraintViolationException} and transforms it into a {@link
+ * io.jooby.validation.ValidationResult}
+ *
+ * @authors kliushnichenko, SentryMan
+ * @since 3.2.10
+ */
+public class AvajeValidatorModule implements Extension {
+
+ private Consumer configurer;
+ private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY;
+ private String title = "Validation failed";
+ private boolean disableDefaultViolationHandler = false;
+
+ /**
+ * Setups a configurer callback.
+ *
+ * @param configurer Configurer callback.
+ * @return This module.
+ */
+ public AvajeValidatorModule doWith(@NonNull final Consumer configurer) {
+ this.configurer = configurer;
+ return this;
+ }
+
+ /**
+ * Overrides the default status code for the errors produced by validation. Default code is
+ * UNPROCESSABLE_ENTITY(422)
+ *
+ * @param statusCode new status code
+ * @return This module.
+ */
+ public AvajeValidatorModule statusCode(@NonNull StatusCode statusCode) {
+ this.statusCode = statusCode;
+ return this;
+ }
+
+ /**
+ * Overrides the default title for the errors produced by validation. Default title is "Validation
+ * failed"
+ *
+ * @param title new title
+ * @return This module.
+ */
+ public AvajeValidatorModule validationTitle(@NonNull String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Disables default constraint violation handler. By default {@link AvajeValidatorModule} provides
+ * built-in error handler for the {@link ConstraintViolationException} Such exceptions are
+ * transformed into response of {@link io.jooby.validation.ValidationResult} Use this flag to
+ * disable default error handler and provide your custom.
+ *
+ * @return This module.
+ */
+ public AvajeValidatorModule disableViolationHandler() {
+ this.disableDefaultViolationHandler = true;
+ return this;
+ }
+
+ @Override
+ public void install(@NonNull Jooby app) throws Exception {
+
+ var props = app.getEnvironment();
+
+ final var locales = new ArrayList();
+ final var builder = Validator.builder();
+ Optional.ofNullable(props.getProperty("validation.failFast", "false"))
+ .map(Boolean::valueOf)
+ .ifPresent(builder::failFast);
+
+ Optional.ofNullable(props.getProperty("validation.resourcebundle.names"))
+ .map(s -> s.split(","))
+ .ifPresent(builder::addResourceBundles);
+
+ Optional.ofNullable(props.getProperty("validation.locale.default"))
+ .map(Locale::forLanguageTag)
+ .ifPresent(
+ l -> {
+ builder.setDefaultLocale(l);
+ locales.add(l);
+ });
+
+ Optional.ofNullable(props.getProperty("validation.locale.addedLocales")).stream()
+ .flatMap(s -> Arrays.stream(s.split(",")))
+ .map(Locale::forLanguageTag)
+ .forEach(
+ l -> {
+ builder.addLocales(l);
+ locales.add(l);
+ });
+
+ Optional.ofNullable(props.getProperty("validation.temporal.tolerance.value"))
+ .map(Long::valueOf)
+ .ifPresent(
+ duration -> {
+ final var unit =
+ Optional.ofNullable(props.getProperty("validation.temporal.tolerance.chronoUnit"))
+ .map(ChronoUnit::valueOf)
+ .orElse(ChronoUnit.MILLIS);
+ builder.temporalTolerance(Duration.of(duration, unit));
+ });
+
+ if (configurer != null) {
+ configurer.accept(builder);
+ }
+
+ Validator validator = builder.build();
+ app.getServices().put(Validator.class, validator);
+ app.getServices().put(MvcValidator.class, new MvcValidatorImpl(validator));
+
+ if (!disableDefaultViolationHandler) {
+ app.error(
+ ConstraintViolationException.class, new ConstraintViolationHandler(statusCode, title));
+ }
+ }
+
+ static class MvcValidatorImpl implements MvcValidator {
+
+ private final Validator validator;
+
+ MvcValidatorImpl(Validator validator) {
+ this.validator = validator;
+ }
+
+ @Override
+ public void validate(Object bean) throws ConstraintViolationException {
+ validator.validate(bean);
+ }
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java
new file mode 100644
index 0000000000..76181c7343
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java
@@ -0,0 +1,95 @@
+package io.jooby.avaje.validator;
+
+import static io.jooby.validation.ValidationResult.ErrorType.FIELD;
+import static io.jooby.validation.ValidationResult.ErrorType.GLOBAL;
+import static java.util.stream.Collectors.groupingBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.avaje.validation.ConstraintViolation;
+import io.avaje.validation.ConstraintViolationException;
+import io.jooby.Context;
+import io.jooby.ErrorHandler;
+import io.jooby.StatusCode;
+import io.jooby.validation.ValidationResult;
+
+/**
+ * Catches and transform {@link ConstraintViolationException} into {@link ValidationResult}
+ *
+ * Payload example:
+ *
+ *
{@code
+ * {
+ * "title": "Validation failed",
+ * "status": 422,
+ * "errors": [
+ * {
+ * "field": null,
+ * "messages": [
+ * "Passwords should match"
+ * ],
+ * "type": "GLOBAL"
+ * },
+ * {
+ * "field": "firstName",
+ * "messages": [
+ * "must not be empty",
+ * "must not be null"
+ * ],
+ * "type": "FIELD"
+ * }
+ * ]
+ * }
+ * }
+ *
+ * @author kliushnichenko
+ * @since 3.2.10
+ */
+public class ConstraintViolationHandler implements ErrorHandler {
+
+ private static final String ROOT_VIOLATIONS_PATH = "";
+
+ private final StatusCode statusCode;
+ private final String title;
+
+ public ConstraintViolationHandler(StatusCode statusCode, String title) {
+ this.statusCode = statusCode;
+ this.title = title;
+ }
+
+ @Override
+ public void apply(@NonNull Context ctx, @NonNull Throwable cause, @NonNull StatusCode code) {
+ var ex = (ConstraintViolationException) cause;
+
+ var violations = ex.violations();
+
+ Map> groupedByPath =
+ violations.stream().collect(groupingBy(violation -> violation.path().toString()));
+
+ List errors = collectErrors(groupedByPath);
+
+ ValidationResult result = new ValidationResult(title, statusCode.value(), errors);
+ ctx.setResponseCode(statusCode).render(result);
+ }
+
+ private List collectErrors(
+ Map> groupedViolations) {
+ List errors = new ArrayList<>();
+ for (Map.Entry> entry : groupedViolations.entrySet()) {
+ var path = entry.getKey();
+ if (ROOT_VIOLATIONS_PATH.equals(path)) {
+ errors.add(new ValidationResult.Error(null, extractMessages(entry.getValue()), GLOBAL));
+ } else {
+ errors.add(new ValidationResult.Error(path, extractMessages(entry.getValue()), FIELD));
+ }
+ }
+ return errors;
+ }
+
+ private List extractMessages(List violations) {
+ return violations.stream().map(ConstraintViolation::message).toList();
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/package-info.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/package-info.java
new file mode 100644
index 0000000000..aaf68dc3bb
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/package-info.java
@@ -0,0 +1 @@
+package io.jooby.avaje.validator;
diff --git a/modules/jooby-avaje-validator/src/main/java/module-info.java b/modules/jooby-avaje-validator/src/main/java/module-info.java
new file mode 100644
index 0000000000..d6ecdf13b8
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/main/java/module-info.java
@@ -0,0 +1,17 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+/**
+ * Avaje Validator Module.
+ */
+module io.jooby.avaje.validator {
+ exports io.jooby.avaje.validator;
+
+ requires transitive io.jooby;
+ requires static com.github.spotbugs.annotations;
+ requires typesafe.config;
+ requires transitive io.avaje.validation;
+ requires transitive io.jooby.validation;
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java
new file mode 100644
index 0000000000..b9034f3238
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java
@@ -0,0 +1,200 @@
+package io.jooby.hibernate.validator;
+
+import io.jooby.hibernate.validator.app.App;
+import io.jooby.hibernate.validator.app.NewAccountRequest;
+import io.jooby.hibernate.validator.app.Person;
+import io.jooby.test.JoobyTest;
+import io.jooby.validation.ValidationResult;
+import io.restassured.RestAssured;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static io.jooby.StatusCode.UNPROCESSABLE_ENTITY_CODE;
+import static io.jooby.hibernate.validator.app.App.DEFAULT_TITLE;
+import static io.restassured.RestAssured.given;
+
+@JoobyTest(value = App.class, port = 8099)
+public class AvajeValidatorModuleTest {
+
+ protected static RequestSpecification SPEC = new RequestSpecBuilder()
+ .setPort(8099)
+ .setContentType(ContentType.JSON)
+ .setAccept(ContentType.JSON)
+ .build();
+
+ static {
+ RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+ }
+
+ @Test
+ public void validate_personBean_shouldDetect2Violations() {
+ Person person = new Person(null, "Last Name");
+
+ ValidationResult actualResult = given().spec(SPEC).
+ with()
+ .body(person)
+ .post("/create-person")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract().as(ValidationResult.class);
+
+ var fieldError = new ValidationResult.Error(
+ "firstName",
+ List.of("must not be empty", "must not be null"),
+ ValidationResult.ErrorType.FIELD
+ );
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_arrayOfPerson_shouldDetect2Violations() {
+ Person person1 = new Person("First Name", "Last Name");
+ Person person2 = new Person(null, "Last Name 2");
+
+ ValidationResult actualResult = given().spec(SPEC).
+ with()
+ .body(new Person[]{person1, person2})
+ .post("/create-array-of-persons")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract().as(ValidationResult.class);
+
+ var fieldError = new ValidationResult.Error(
+ "firstName",
+ List.of("must not be empty", "must not be null"),
+ ValidationResult.ErrorType.FIELD
+ );
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_listOfPerson_shouldDetect2Violations() {
+ Person person1 = new Person("First Name", "Last Name");
+ Person person2 = new Person(null, "Last Name 2");
+
+ ValidationResult actualResult = given().spec(SPEC).
+ with()
+ .body(List.of(person1, person2))
+ .post("/create-list-of-persons")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract().as(ValidationResult.class);
+
+ var fieldError = new ValidationResult.Error(
+ "firstName",
+ List.of("must not be empty", "must not be null"),
+ ValidationResult.ErrorType.FIELD
+ );
+ ValidationResult expectedResult = buildResult( List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_mapOfPerson_shouldDetect2Violations() {
+ Person person1 = new Person("First Name", "Last Name");
+ Person person2 = new Person(null, "Last Name 2");
+
+ ValidationResult actualResult = given().spec(SPEC).
+ with()
+ .body(Map.of("1", person1, "2", person2))
+ .post("/create-map-of-persons")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract().as(ValidationResult.class);
+
+ var fieldError = new ValidationResult.Error(
+ "firstName",
+ List.of("must not be empty", "must not be null"),
+ ValidationResult.ErrorType.FIELD
+ );
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_newAccountBean_shouldDetect6Violations() {
+ NewAccountRequest request = new NewAccountRequest();
+ request.setLogin("jk");
+ request.setPassword("123");
+ request.setConfirmPassword("1234");
+ request.setPerson(new Person(null, "Last Name"));
+
+ ValidationResult actualResult = given().spec(SPEC).
+ with()
+ .body(request)
+ .post("/create-new-account")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract().as(ValidationResult.class);
+
+ List errors = new ArrayList<>() {{
+ add(new ValidationResult.Error(
+ null,
+ List.of("Passwords should match"),
+ ValidationResult.ErrorType.GLOBAL)
+ );
+ add(new ValidationResult.Error(
+ "person.firstName",
+ List.of("must not be empty", "must not be null"),
+ ValidationResult.ErrorType.FIELD)
+ );
+ add(new ValidationResult.Error(
+ "login",
+ List.of("size must be between 3 and 16"),
+ ValidationResult.ErrorType.FIELD)
+ );
+ add(new ValidationResult.Error(
+ "password",
+ List.of("size must be between 8 and 24"),
+ ValidationResult.ErrorType.FIELD)
+ );
+ add(new ValidationResult.Error(
+ "confirmPassword",
+ List.of("size must be between 8 and 24"),
+ ValidationResult.ErrorType.FIELD)
+ );
+ }};
+
+ ValidationResult expectedResult = buildResult(errors);
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors")
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ private ValidationResult buildResult(List errors) {
+ return new ValidationResult(DEFAULT_TITLE, UNPROCESSABLE_ENTITY_CODE, errors);
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java
new file mode 100644
index 0000000000..d85a9ec216
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java
@@ -0,0 +1,24 @@
+package io.jooby.hibernate.validator.app;
+
+import io.jooby.Jooby;
+import io.jooby.StatusCode;
+import io.jooby.avaje.validator.AvajeValidatorModule;
+import io.jooby.avaje.validator.ConstraintViolationHandler;
+import io.jooby.jackson.JacksonModule;
+import jakarta.validation.ConstraintViolationException;
+
+public class App extends Jooby {
+
+ private static final StatusCode STATUS_CODE = StatusCode.UNPROCESSABLE_ENTITY;
+ public static final String DEFAULT_TITLE = "Validation failed";
+
+ {
+ install(new JacksonModule());
+ install(new AvajeValidatorModule());
+
+ mvc(new Controller());
+
+ error(ConstraintViolationException.class, new ConstraintViolationHandler(STATUS_CODE, DEFAULT_TITLE));
+ }
+
+}
\ No newline at end of file
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java
new file mode 100644
index 0000000000..bf6ac84465
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java
@@ -0,0 +1,32 @@
+package io.jooby.hibernate.validator.app;
+
+import io.jooby.annotation.POST;
+import io.jooby.annotation.Path;
+import jakarta.validation.Valid;
+
+import java.util.List;
+import java.util.Map;
+
+@Path("")
+public class Controller {
+
+ @POST("/create-person")
+ public void createPerson(@Valid Person person) {
+ }
+
+ @POST("/create-array-of-persons")
+ public void createArrayOfPersons(@Valid Person[] persons) {
+ }
+
+ @POST("/create-list-of-persons")
+ public void createListOfPersons(@Valid List persons) {
+ }
+
+ @POST("/create-map-of-persons")
+ public void createMapOfPersons(@Valid Map persons) {
+ }
+
+ @POST("/create-new-account")
+ public void createNewAccount(@Valid NewAccountRequest request) {
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java
new file mode 100644
index 0000000000..e67bcae8e4
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java
@@ -0,0 +1,59 @@
+package io.jooby.hibernate.validator.app;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+@PasswordsShouldMatch
+public class NewAccountRequest {
+ @NotNull
+ @NotEmpty
+ @Size(min = 3, max = 16)
+ private String login;
+
+ @NotNull
+ @NotEmpty
+ @Size(min = 8, max = 24)
+ private String password;
+
+ @NotNull
+ @NotEmpty
+ @Size(min = 8, max = 24)
+ private String confirmPassword;
+
+ @Valid
+ private Person person;
+
+ public String getLogin() {
+ return login;
+ }
+
+ public void setLogin(String login) {
+ this.login = login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getConfirmPassword() {
+ return confirmPassword;
+ }
+
+ public void setConfirmPassword(String confirmPassword) {
+ this.confirmPassword = confirmPassword;
+ }
+
+ public Person getPerson() {
+ return person;
+ }
+
+ public void setPerson(Person person) {
+ this.person = person;
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java
new file mode 100644
index 0000000000..dcd01c1852
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java
@@ -0,0 +1,22 @@
+package io.jooby.hibernate.validator.app;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Constraint(validatedBy = PasswordsShouldMatchValidator.class)
+@Target({TYPE, ANNOTATION_TYPE})
+@Retention(RUNTIME)
+public @interface PasswordsShouldMatch {
+ String message() default "Passwords should match";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java
new file mode 100644
index 0000000000..e1fbbf0eb4
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java
@@ -0,0 +1,15 @@
+package io.jooby.hibernate.validator.app;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+public class PasswordsShouldMatchValidator implements ConstraintValidator {
+
+ @Override
+ public boolean isValid(NewAccountRequest request, ConstraintValidatorContext constraintContext) {
+ if (request.getPassword() == null || request.getConfirmPassword() == null) {
+ return false;
+ }
+ return request.getPassword().equals(request.getConfirmPassword());
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java
new file mode 100644
index 0000000000..f0cbba3e71
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java
@@ -0,0 +1,35 @@
+package io.jooby.hibernate.validator.app;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
+@Valid
+public class Person {
+
+ @NotEmpty
+ @NotNull
+ private String firstName;
+ private String lastName;
+
+ public Person(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+}
diff --git a/pom.xml b/pom.xml
index 870dd9dd6a..95dd5d737c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,6 @@
3.2.2
2.17.2
2.11.0
- 2.1
3.0.1
3.0.4
1.4.0
@@ -47,7 +46,6 @@
1.4.3
- 10.3
7.0.0
@@ -80,6 +78,11 @@
2.5.2
+
+ 10.3
+ 2.1
+ 2.1
+
2.0.1
3.1.0
@@ -673,26 +676,39 @@
io.avaje
avaje-inject
- ${avaje-inject.version}
+ ${avaje.inject.version}
io.avaje
avaje-inject-generator
- ${avaje-inject.version}
+ ${avaje.inject.version}
io.avaje
avaje-jsonb
- ${avaje-jsonb.version}
+ ${avaje.jsonb.version}
io.avaje
avaje-jsonb-generator
- ${avaje-jsonb.version}
+ ${avaje.jsonb.version}
+
+
+
+
+ io.avaje
+ avaje-validator
+ ${avaje.validator.version}
+
+
+
+ io.avaje
+ avaje-validator-generator
+ ${avaje.validator.version}
From d099beb479e8cd35c97f1ce0152c7ab5a8a329ec Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Fri, 6 Sep 2024 10:49:33 -0400
Subject: [PATCH 2/4] tests
---
docs/asciidoc/modules/avaje-inject.adoc | 2 +-
modules/jooby-avaje-inject/pom.xml | 35 +--
modules/jooby-avaje-validator/pom.xml | 29 +--
.../avaje/validator/AvajeValidatorModule.java | 5 +-
.../validator/AvajeValidatorModuleTest.java | 197 +++++++++++++++++
.../io/jooby/avaje/validator/app/App.java | 25 +++
.../jooby/avaje/validator/app/Controller.java | 27 +++
.../validator/app/NewAccountRequest.java | 59 ++++++
.../validator/app/PasswordsShouldMatch.java | 10 +-
.../app/PasswordsShouldMatchValidator.java | 21 ++
.../io/jooby/avaje/validator/app/Person.java | 33 +++
.../validator/AvajeValidatorModuleTest.java | 200 ------------------
.../io/jooby/hibernate/validator/app/App.java | 24 ---
.../hibernate/validator/app/Controller.java | 32 ---
.../validator/app/NewAccountRequest.java | 59 ------
.../app/PasswordsShouldMatchValidator.java | 15 --
.../jooby/hibernate/validator/app/Person.java | 35 ---
.../validator/HibernateValidatorModule.java | 3 +-
.../io/jooby/validation/BeanValidator.java | 24 +--
.../io/jooby/validation/MvcValidator.java | 5 +-
modules/pom.xml | 1 +
pom.xml | 6 +
22 files changed, 415 insertions(+), 432 deletions(-)
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/AvajeValidatorModuleTest.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/App.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Controller.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/NewAccountRequest.java
rename modules/jooby-avaje-validator/src/test/java/io/jooby/{hibernate => avaje}/validator/app/PasswordsShouldMatch.java (63%)
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatchValidator.java
create mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Person.java
delete mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java
delete mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java
delete mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java
delete mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java
delete mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java
delete mode 100644 modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java
diff --git a/docs/asciidoc/modules/avaje-inject.adoc b/docs/asciidoc/modules/avaje-inject.adoc
index eeb9913742..753e839515 100644
--- a/docs/asciidoc/modules/avaje-inject.adoc
+++ b/docs/asciidoc/modules/avaje-inject.adoc
@@ -21,7 +21,7 @@
io.avaje
avaje-inject-generator
- 10.0
+ 10.3
diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml
index 5da897dfcf..30f4e6e877 100644
--- a/modules/jooby-avaje-inject/pom.xml
+++ b/modules/jooby-avaje-inject/pom.xml
@@ -11,7 +11,11 @@
4.0.0
jooby-avaje-inject
-
+
+
+ full
+
+
com.github.spotbugs
@@ -21,7 +25,6 @@
io.jooby
jooby
- ${jooby.version}
@@ -33,7 +36,7 @@
io.avaje
avaje-inject-generator
- provided
+ test
@@ -74,30 +77,4 @@
test
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
- test
- test-compile
-
-
-
-
- -parameters
-
-
-
- io.avaje
- avaje-inject-generator
-
-
-
-
-
-
diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml
index a2fa20eb51..174a57ff50 100644
--- a/modules/jooby-avaje-validator/pom.xml
+++ b/modules/jooby-avaje-validator/pom.xml
@@ -11,7 +11,11 @@
4.0.0
jooby-avaje-validator
-
+
+
+ full
+
+
io.jooby
@@ -41,7 +45,18 @@
jooby-netty
test
-
+
+
+ io.jooby
+ jooby-apt
+ test
+
+
+ io.avaje
+ avaje-validator-generator
+ test
+
+
io.jooby
jooby-jackson
@@ -95,16 +110,6 @@
-parameters
-
-
- io.jooby
- jooby-apt
-
-
- io.avaje
- avaje-validator-generator
-
-
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
index 8b324edc2c..a509da90bc 100644
--- a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
@@ -16,6 +16,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import io.avaje.validation.ConstraintViolationException;
import io.avaje.validation.Validator;
+import io.jooby.Context;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.StatusCode;
@@ -169,8 +170,8 @@ static class MvcValidatorImpl implements MvcValidator {
}
@Override
- public void validate(Object bean) throws ConstraintViolationException {
- validator.validate(bean);
+ public void validate(Context ctx, Object bean) throws ConstraintViolationException {
+ validator.validate(bean, ctx.locale());
}
}
}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/AvajeValidatorModuleTest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/AvajeValidatorModuleTest.java
new file mode 100644
index 0000000000..f1c9e74e6c
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/AvajeValidatorModuleTest.java
@@ -0,0 +1,197 @@
+package io.jooby.avaje.validator;
+
+import io.jooby.avaje.validator.app.App;
+import io.jooby.avaje.validator.app.NewAccountRequest;
+import io.jooby.avaje.validator.app.Person;
+import io.jooby.test.JoobyTest;
+import io.jooby.validation.ValidationResult;
+import io.restassured.RestAssured;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static io.jooby.StatusCode.UNPROCESSABLE_ENTITY_CODE;
+import static io.jooby.avaje.validator.app.App.DEFAULT_TITLE;
+import static io.restassured.RestAssured.given;
+
+@JoobyTest(value = App.class, port = 8099)
+public class AvajeValidatorModuleTest {
+
+ protected static RequestSpecification SPEC =
+ new RequestSpecBuilder()
+ .setPort(8099)
+ .setContentType(ContentType.JSON)
+ .setAccept(ContentType.JSON)
+ .build();
+
+ static {
+ RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+ }
+
+ @Test
+ public void validate_personBean_shouldDetect2Violations() {
+ Person person = new Person(null, "Last Name");
+
+ ValidationResult actualResult =
+ given()
+ .spec(SPEC)
+ .with()
+ .body(person)
+ .post("/create-person")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract()
+ .as(ValidationResult.class);
+
+ var fieldError =
+ new ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_arrayOfPerson_shouldDetect2Violations() {
+ Person person1 = new Person("First Name", "Last Name");
+ Person person2 = new Person(null, "Last Name 2");
+
+ ValidationResult actualResult =
+ given()
+ .spec(SPEC)
+ .with()
+ .body(new Person[] {person1, person2})
+ .post("/create-array-of-persons")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract()
+ .as(ValidationResult.class);
+
+ var fieldError =
+ new ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_listOfPerson_shouldDetect2Violations() {
+ Person person1 = new Person("First Name", "Last Name");
+ Person person2 = new Person(null, "Last Name 2");
+
+ ValidationResult actualResult =
+ given()
+ .spec(SPEC)
+ .with()
+ .body(List.of(person1, person2))
+ .post("/create-list-of-persons")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract()
+ .as(ValidationResult.class);
+
+ var fieldError =
+ new ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_mapOfPerson_shouldDetect2Violations() {
+ Person person1 = new Person("First Name", "Last Name");
+ Person person2 = new Person(null, "Last Name 2");
+
+ ValidationResult actualResult =
+ given()
+ .spec(SPEC)
+ .with()
+ .body(Map.of("1", person1, "2", person2))
+ .post("/create-map-of-persons")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract()
+ .as(ValidationResult.class);
+
+ var fieldError =
+ new ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ @Test
+ public void validate_newAccountBean_shouldDetect6Violations() {
+ NewAccountRequest request = new NewAccountRequest();
+ request.setLogin("jk");
+ request.setPassword("123");
+ request.setConfirmPassword("1234");
+ request.setPerson(new Person(null, "Last Name"));
+
+ ValidationResult actualResult =
+ given()
+ .spec(SPEC)
+ .with()
+ .body(request)
+ .post("/create-new-account")
+ .then()
+ .assertThat()
+ .statusCode(UNPROCESSABLE_ENTITY_CODE)
+ .extract()
+ .as(ValidationResult.class);
+
+ List errors =
+ List.of(
+ new ValidationResult.Error(
+ "password",
+ List.of("length must be between 8 and 24"),
+ ValidationResult.ErrorType.FIELD),
+ new ValidationResult.Error(
+ "person.firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD),
+ new ValidationResult.Error(
+ "confirmPassword",
+ List.of("length must be between 8 and 24"),
+ ValidationResult.ErrorType.FIELD),
+ new ValidationResult.Error(
+ "login",
+ List.of("length must be between 3 and 16"),
+ ValidationResult.ErrorType.FIELD));
+
+ ValidationResult expectedResult = buildResult(errors);
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors")
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ private ValidationResult buildResult(List errors) {
+ return new ValidationResult(DEFAULT_TITLE, UNPROCESSABLE_ENTITY_CODE, errors);
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/App.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/App.java
new file mode 100644
index 0000000000..885feaabfc
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/App.java
@@ -0,0 +1,25 @@
+package io.jooby.avaje.validator.app;
+
+import io.jooby.Jooby;
+import io.jooby.StatusCode;
+import io.jooby.avaje.validator.AvajeValidatorModule;
+import io.jooby.avaje.validator.ConstraintViolationHandler;
+import io.jooby.jackson.JacksonModule;
+import jakarta.validation.ConstraintViolationException;
+
+public class App extends Jooby {
+
+ private static final StatusCode STATUS_CODE = StatusCode.UNPROCESSABLE_ENTITY;
+ public static final String DEFAULT_TITLE = "Validation failed";
+
+ {
+ install(new JacksonModule());
+ install(new AvajeValidatorModule());
+
+ mvc(new Controller());
+
+ error(
+ ConstraintViolationException.class,
+ new ConstraintViolationHandler(STATUS_CODE, DEFAULT_TITLE));
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Controller.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Controller.java
new file mode 100644
index 0000000000..1adfb5ae11
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Controller.java
@@ -0,0 +1,27 @@
+package io.jooby.avaje.validator.app;
+
+import io.jooby.annotation.POST;
+import io.jooby.annotation.Path;
+import jakarta.validation.Valid;
+
+import java.util.List;
+import java.util.Map;
+
+@Path("")
+public class Controller {
+
+ @POST("/create-person")
+ public void createPerson(@Valid Person person) {}
+
+ @POST("/create-array-of-persons")
+ public void createArrayOfPersons(@Valid Person[] persons) {}
+
+ @POST("/create-list-of-persons")
+ public void createListOfPersons(@Valid List persons) {}
+
+ @POST("/create-map-of-persons")
+ public void createMapOfPersons(@Valid Map persons) {}
+
+ @POST("/create-new-account")
+ public void createNewAccount(@Valid NewAccountRequest request) {}
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/NewAccountRequest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/NewAccountRequest.java
new file mode 100644
index 0000000000..129618e7ce
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/NewAccountRequest.java
@@ -0,0 +1,59 @@
+package io.jooby.avaje.validator.app;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+@Valid
+@PasswordsShouldMatch
+public class NewAccountRequest {
+ @NotNull
+ @NotEmpty
+ @Size(min = 3, max = 16)
+ private String login;
+
+ @NotNull
+ @NotEmpty
+ @Size(min = 8, max = 24)
+ private String password;
+
+ @NotNull
+ @NotEmpty
+ @Size(min = 8, max = 24)
+ private String confirmPassword;
+
+ @Valid private Person person;
+
+ public String getLogin() {
+ return login;
+ }
+
+ public void setLogin(String login) {
+ this.login = login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getConfirmPassword() {
+ return confirmPassword;
+ }
+
+ public void setConfirmPassword(String confirmPassword) {
+ this.confirmPassword = confirmPassword;
+ }
+
+ public Person getPerson() {
+ return person;
+ }
+
+ public void setPerson(Person person) {
+ this.person = person;
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatch.java
similarity index 63%
rename from modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java
rename to modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatch.java
index dcd01c1852..ae11aea568 100644
--- a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatch.java
@@ -1,4 +1,4 @@
-package io.jooby.hibernate.validator.app;
+package io.jooby.avaje.validator.app;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@@ -10,13 +10,11 @@
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-@Constraint(validatedBy = PasswordsShouldMatchValidator.class)
+@Constraint(validatedBy = {})
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface PasswordsShouldMatch {
- String message() default "Passwords should match";
+ String message() default "Passwords should match";
- Class>[] groups() default {};
-
- Class extends Payload>[] payload() default {};
+ Class>[] groups() default {};
}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatchValidator.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatchValidator.java
new file mode 100644
index 0000000000..073c025057
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatchValidator.java
@@ -0,0 +1,21 @@
+package io.jooby.avaje.validator.app;
+
+import io.avaje.validation.adapter.AbstractConstraintAdapter;
+import io.avaje.validation.adapter.ConstraintAdapter;
+import io.avaje.validation.adapter.ValidationContext.AdapterCreateRequest;
+
+@ConstraintAdapter(PasswordsShouldMatch.class)
+public class PasswordsShouldMatchValidator extends AbstractConstraintAdapter {
+
+ public PasswordsShouldMatchValidator(AdapterCreateRequest request) {
+ super(request);
+ }
+
+ @Override
+ public boolean isValid(NewAccountRequest request) {
+ if (request.getPassword() == null || request.getConfirmPassword() == null) {
+ return false;
+ }
+ return request.getPassword().equals(request.getConfirmPassword());
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Person.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Person.java
new file mode 100644
index 0000000000..0687bc0ac1
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Person.java
@@ -0,0 +1,33 @@
+package io.jooby.avaje.validator.app;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
+@Valid
+public class Person {
+
+ @NotEmpty private String firstName;
+ private String lastName;
+
+ public Person(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java
deleted file mode 100644
index b9034f3238..0000000000
--- a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/AvajeValidatorModuleTest.java
+++ /dev/null
@@ -1,200 +0,0 @@
-package io.jooby.hibernate.validator;
-
-import io.jooby.hibernate.validator.app.App;
-import io.jooby.hibernate.validator.app.NewAccountRequest;
-import io.jooby.hibernate.validator.app.Person;
-import io.jooby.test.JoobyTest;
-import io.jooby.validation.ValidationResult;
-import io.restassured.RestAssured;
-import io.restassured.builder.RequestSpecBuilder;
-import io.restassured.http.ContentType;
-import io.restassured.specification.RequestSpecification;
-import org.assertj.core.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import static io.jooby.StatusCode.UNPROCESSABLE_ENTITY_CODE;
-import static io.jooby.hibernate.validator.app.App.DEFAULT_TITLE;
-import static io.restassured.RestAssured.given;
-
-@JoobyTest(value = App.class, port = 8099)
-public class AvajeValidatorModuleTest {
-
- protected static RequestSpecification SPEC = new RequestSpecBuilder()
- .setPort(8099)
- .setContentType(ContentType.JSON)
- .setAccept(ContentType.JSON)
- .build();
-
- static {
- RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
- }
-
- @Test
- public void validate_personBean_shouldDetect2Violations() {
- Person person = new Person(null, "Last Name");
-
- ValidationResult actualResult = given().spec(SPEC).
- with()
- .body(person)
- .post("/create-person")
- .then()
- .assertThat()
- .statusCode(UNPROCESSABLE_ENTITY_CODE)
- .extract().as(ValidationResult.class);
-
- var fieldError = new ValidationResult.Error(
- "firstName",
- List.of("must not be empty", "must not be null"),
- ValidationResult.ErrorType.FIELD
- );
- ValidationResult expectedResult = buildResult(List.of(fieldError));
-
- Assertions.assertThat(expectedResult)
- .usingRecursiveComparison()
- .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
- .isEqualTo(actualResult);
- }
-
- @Test
- public void validate_arrayOfPerson_shouldDetect2Violations() {
- Person person1 = new Person("First Name", "Last Name");
- Person person2 = new Person(null, "Last Name 2");
-
- ValidationResult actualResult = given().spec(SPEC).
- with()
- .body(new Person[]{person1, person2})
- .post("/create-array-of-persons")
- .then()
- .assertThat()
- .statusCode(UNPROCESSABLE_ENTITY_CODE)
- .extract().as(ValidationResult.class);
-
- var fieldError = new ValidationResult.Error(
- "firstName",
- List.of("must not be empty", "must not be null"),
- ValidationResult.ErrorType.FIELD
- );
- ValidationResult expectedResult = buildResult(List.of(fieldError));
-
- Assertions.assertThat(expectedResult)
- .usingRecursiveComparison()
- .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
- .isEqualTo(actualResult);
- }
-
- @Test
- public void validate_listOfPerson_shouldDetect2Violations() {
- Person person1 = new Person("First Name", "Last Name");
- Person person2 = new Person(null, "Last Name 2");
-
- ValidationResult actualResult = given().spec(SPEC).
- with()
- .body(List.of(person1, person2))
- .post("/create-list-of-persons")
- .then()
- .assertThat()
- .statusCode(UNPROCESSABLE_ENTITY_CODE)
- .extract().as(ValidationResult.class);
-
- var fieldError = new ValidationResult.Error(
- "firstName",
- List.of("must not be empty", "must not be null"),
- ValidationResult.ErrorType.FIELD
- );
- ValidationResult expectedResult = buildResult( List.of(fieldError));
-
- Assertions.assertThat(expectedResult)
- .usingRecursiveComparison()
- .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
- .isEqualTo(actualResult);
- }
-
- @Test
- public void validate_mapOfPerson_shouldDetect2Violations() {
- Person person1 = new Person("First Name", "Last Name");
- Person person2 = new Person(null, "Last Name 2");
-
- ValidationResult actualResult = given().spec(SPEC).
- with()
- .body(Map.of("1", person1, "2", person2))
- .post("/create-map-of-persons")
- .then()
- .assertThat()
- .statusCode(UNPROCESSABLE_ENTITY_CODE)
- .extract().as(ValidationResult.class);
-
- var fieldError = new ValidationResult.Error(
- "firstName",
- List.of("must not be empty", "must not be null"),
- ValidationResult.ErrorType.FIELD
- );
- ValidationResult expectedResult = buildResult(List.of(fieldError));
-
- Assertions.assertThat(expectedResult)
- .usingRecursiveComparison()
- .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
- .isEqualTo(actualResult);
- }
-
- @Test
- public void validate_newAccountBean_shouldDetect6Violations() {
- NewAccountRequest request = new NewAccountRequest();
- request.setLogin("jk");
- request.setPassword("123");
- request.setConfirmPassword("1234");
- request.setPerson(new Person(null, "Last Name"));
-
- ValidationResult actualResult = given().spec(SPEC).
- with()
- .body(request)
- .post("/create-new-account")
- .then()
- .assertThat()
- .statusCode(UNPROCESSABLE_ENTITY_CODE)
- .extract().as(ValidationResult.class);
-
- List errors = new ArrayList<>() {{
- add(new ValidationResult.Error(
- null,
- List.of("Passwords should match"),
- ValidationResult.ErrorType.GLOBAL)
- );
- add(new ValidationResult.Error(
- "person.firstName",
- List.of("must not be empty", "must not be null"),
- ValidationResult.ErrorType.FIELD)
- );
- add(new ValidationResult.Error(
- "login",
- List.of("size must be between 3 and 16"),
- ValidationResult.ErrorType.FIELD)
- );
- add(new ValidationResult.Error(
- "password",
- List.of("size must be between 8 and 24"),
- ValidationResult.ErrorType.FIELD)
- );
- add(new ValidationResult.Error(
- "confirmPassword",
- List.of("size must be between 8 and 24"),
- ValidationResult.ErrorType.FIELD)
- );
- }};
-
- ValidationResult expectedResult = buildResult(errors);
-
- Assertions.assertThat(expectedResult)
- .usingRecursiveComparison()
- .ignoringCollectionOrderInFieldsMatchingRegexes("errors")
- .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
- .isEqualTo(actualResult);
- }
-
- private ValidationResult buildResult(List errors) {
- return new ValidationResult(DEFAULT_TITLE, UNPROCESSABLE_ENTITY_CODE, errors);
- }
-}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java
deleted file mode 100644
index d85a9ec216..0000000000
--- a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/App.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.jooby.hibernate.validator.app;
-
-import io.jooby.Jooby;
-import io.jooby.StatusCode;
-import io.jooby.avaje.validator.AvajeValidatorModule;
-import io.jooby.avaje.validator.ConstraintViolationHandler;
-import io.jooby.jackson.JacksonModule;
-import jakarta.validation.ConstraintViolationException;
-
-public class App extends Jooby {
-
- private static final StatusCode STATUS_CODE = StatusCode.UNPROCESSABLE_ENTITY;
- public static final String DEFAULT_TITLE = "Validation failed";
-
- {
- install(new JacksonModule());
- install(new AvajeValidatorModule());
-
- mvc(new Controller());
-
- error(ConstraintViolationException.class, new ConstraintViolationHandler(STATUS_CODE, DEFAULT_TITLE));
- }
-
-}
\ No newline at end of file
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java
deleted file mode 100644
index bf6ac84465..0000000000
--- a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package io.jooby.hibernate.validator.app;
-
-import io.jooby.annotation.POST;
-import io.jooby.annotation.Path;
-import jakarta.validation.Valid;
-
-import java.util.List;
-import java.util.Map;
-
-@Path("")
-public class Controller {
-
- @POST("/create-person")
- public void createPerson(@Valid Person person) {
- }
-
- @POST("/create-array-of-persons")
- public void createArrayOfPersons(@Valid Person[] persons) {
- }
-
- @POST("/create-list-of-persons")
- public void createListOfPersons(@Valid List persons) {
- }
-
- @POST("/create-map-of-persons")
- public void createMapOfPersons(@Valid Map persons) {
- }
-
- @POST("/create-new-account")
- public void createNewAccount(@Valid NewAccountRequest request) {
- }
-}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java
deleted file mode 100644
index e67bcae8e4..0000000000
--- a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package io.jooby.hibernate.validator.app;
-
-import jakarta.validation.Valid;
-import jakarta.validation.constraints.NotEmpty;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Size;
-
-@PasswordsShouldMatch
-public class NewAccountRequest {
- @NotNull
- @NotEmpty
- @Size(min = 3, max = 16)
- private String login;
-
- @NotNull
- @NotEmpty
- @Size(min = 8, max = 24)
- private String password;
-
- @NotNull
- @NotEmpty
- @Size(min = 8, max = 24)
- private String confirmPassword;
-
- @Valid
- private Person person;
-
- public String getLogin() {
- return login;
- }
-
- public void setLogin(String login) {
- this.login = login;
- }
-
- public String getPassword() {
- return password;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-
- public String getConfirmPassword() {
- return confirmPassword;
- }
-
- public void setConfirmPassword(String confirmPassword) {
- this.confirmPassword = confirmPassword;
- }
-
- public Person getPerson() {
- return person;
- }
-
- public void setPerson(Person person) {
- this.person = person;
- }
-}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java
deleted file mode 100644
index e1fbbf0eb4..0000000000
--- a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.jooby.hibernate.validator.app;
-
-import jakarta.validation.ConstraintValidator;
-import jakarta.validation.ConstraintValidatorContext;
-
-public class PasswordsShouldMatchValidator implements ConstraintValidator {
-
- @Override
- public boolean isValid(NewAccountRequest request, ConstraintValidatorContext constraintContext) {
- if (request.getPassword() == null || request.getConfirmPassword() == null) {
- return false;
- }
- return request.getPassword().equals(request.getConfirmPassword());
- }
-}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java
deleted file mode 100644
index f0cbba3e71..0000000000
--- a/modules/jooby-avaje-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package io.jooby.hibernate.validator.app;
-
-import jakarta.validation.Valid;
-import jakarta.validation.constraints.NotEmpty;
-import jakarta.validation.constraints.NotNull;
-
-@Valid
-public class Person {
-
- @NotEmpty
- @NotNull
- private String firstName;
- private String lastName;
-
- public Person(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
-
- public String getFirstName() {
- return firstName;
- }
-
- public void setFirstName(String firstName) {
- this.firstName = firstName;
- }
-
- public String getLastName() {
- return lastName;
- }
-
- public void setLastName(String lastName) {
- this.lastName = lastName;
- }
-}
diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java
index 970191425b..73cc55a541 100644
--- a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java
+++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java
@@ -7,6 +7,7 @@
import com.typesafe.config.Config;
import edu.umd.cs.findbugs.annotations.NonNull;
+import io.jooby.Context;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.StatusCode;
@@ -142,7 +143,7 @@ static class MvcValidatorImpl implements MvcValidator {
}
@Override
- public void validate(Object bean) throws ConstraintViolationException {
+ public void validate(Context ctx, Object bean) throws ConstraintViolationException {
Set> violations = validator.validate(bean);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java b/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java
index d445ff20d0..4d81969f9d 100644
--- a/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java
+++ b/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java
@@ -20,33 +20,27 @@
*/
public final class BeanValidator {
+ private BeanValidator() {}
+
public static T validate(Context ctx, T bean) {
MvcValidator validator = ctx.require(MvcValidator.class);
if (bean instanceof Collection>) {
- validateCollection(validator, (Collection>) bean);
+ validateCollection(validator, ctx, (Collection>) bean);
} else if (bean.getClass().isArray()) {
- validateCollection(validator, Arrays.asList((Object[]) bean));
+ validateCollection(validator, ctx, Arrays.asList((Object[]) bean));
} else if (bean instanceof Map, ?>) {
- validateCollection(validator, ((Map, ?>) bean).values());
+ validateCollection(validator, ctx, ((Map, ?>) bean).values());
} else {
- validateObject(validator, bean);
+ validator.validate(ctx, bean);
}
return bean;
}
- private static void validateCollection(MvcValidator validator, Collection> beans) {
- for (Object item : beans) {
- validateObject(validator, item);
- }
- }
-
- private static void validateObject(MvcValidator validator, Object bean) {
- try {
- validator.validate(bean);
- } catch (Throwable e) {
- SneakyThrows.propagate(e);
+ private static void validateCollection(MvcValidator validator, Context ctx, Collection> beans) {
+ for (var item : beans) {
+ validator.validate(ctx, item);
}
}
}
diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java b/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java
index d17b7b6eb7..7fcca35fbb 100644
--- a/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java
+++ b/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java
@@ -1,5 +1,7 @@
package io.jooby.validation;
+import io.jooby.Context;
+
/**
* This interface should be implemented by modules that provide bean validation functionality.
* An instance of this interface must be registered in the Jooby service registry.
@@ -11,7 +13,8 @@ public interface MvcValidator {
/**
* Method should validate the bean and throw an exception if any constraint violations are detected
* @param bean bean to be validated
+ * @param ctx request context
* @throws RuntimeException an exception with violations to be thrown (e.g. ConstraintViolationException)
*/
- void validate(Object bean) throws RuntimeException;
+ void validate(Context ctx, Object bean) throws RuntimeException;
}
diff --git a/modules/pom.xml b/modules/pom.xml
index 3369be6e33..d18d100347 100644
--- a/modules/pom.xml
+++ b/modules/pom.xml
@@ -60,6 +60,7 @@
jooby-validation
+ jooby-avaje-validator
jooby-hibernate-validator
jooby-pac4j
diff --git a/pom.xml b/pom.xml
index 95dd5d737c..18ffc63809 100644
--- a/pom.xml
+++ b/pom.xml
@@ -349,6 +349,12 @@
${jooby.version}
+
+ io.jooby
+ jooby-avaje-validator
+ ${jooby.version}
+
+
io.jooby
jooby-hibernate
From f910c06ba662e4246ebb4d103e7b32d758a7a1b1 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Fri, 6 Sep 2024 13:15:09 -0400
Subject: [PATCH 3/4] Create avaje-validator.adoc
---
docs/asciidoc/modules/avaje-validator.adoc | 274 +++++++++++++++++++++
1 file changed, 274 insertions(+)
create mode 100644 docs/asciidoc/modules/avaje-validator.adoc
diff --git a/docs/asciidoc/modules/avaje-validator.adoc b/docs/asciidoc/modules/avaje-validator.adoc
new file mode 100644
index 0000000000..5cbde7de88
--- /dev/null
+++ b/docs/asciidoc/modules/avaje-validator.adoc
@@ -0,0 +1,274 @@
+== Avaje Validator
+
+Bean validation via https://avaje.io/validator/[Avaje Validator].
+
+=== Usage
+
+1) Add the dependency:
+
+[dependency, artifactId="jooby-avaje-validator"]
+.
+
+2) Install
+
+.Java
+[source, java, role="primary"]
+----
+import io.jooby.avaje.validator.AvajeValidatorModule;
+
+{
+ install(new AvajeValidatorModule());
+}
+----
+
+.Kotlin
+[source, kt, role="secondary"]
+----
+import io.jooby.avaje.validator.AvajeValidatorModule
+
+{
+ install(new AvajeValidatorModule())
+}
+----
+
+3) Usage in MVC routes
+
+.Java
+[source,java,role="primary"]
+----
+import io.jooby.annotation.*;
+import jakarta.validation.Valid;
+
+@Path("/mvc")
+public class Controller {
+
+ @POST("/validate-body")
+ public void validateBody(@Valid Bean bean) { // <1>
+ ...
+ }
+
+ @POST("/validate-query")
+ public void validateQuery(@Valid @QueryParam Bean bean) { // <2>
+ ...
+ }
+
+ @POST("/validate-list")
+ public void validateList(@Valid List beans) { // <3>
+ ...
+ }
+
+ @POST("/validate-map")
+ public void validateMap(@Valid Map beans) { // <4>
+ ...
+ }
+}
+----
+
+.Kotlin
+[source, kt, role="secondary"]
+----
+import io.jooby.annotation.*;
+import jakarta.validation.Valid
+
+@Path("/mvc")
+class Controller {
+
+ @POST("/validate-body")
+ fun validateBody(@Valid bean: Bean) : Unit { // <1>
+ ...
+ }
+
+ @POST("/validate-query")
+ fun validateQuery(@Valid @QueryParam bean: Bean) : Unit { // <2>
+ ...
+ }
+
+ @POST("/validate-list")
+ fun validateList(@Valid beans: List) : Unit { // <3>
+ ...
+ }
+
+ @POST("/validate-map")
+ fun validateMap(@Valid beans: Map) : Unit { // <4>
+ ...
+ }
+}
+----
+
+<1> Validate a bean decoded from the request body
+<2> Validate a bean parsed from query parameters. This works the same for `@FormParam` or `@BindParam`
+<3> Validate a list of beans. This also applies to arrays `@Valid Bean[] beans`
+<4> Validate a map of beans
+
+4) Usage in in script/lambda routes
+
+Jooby doesn't provide fully native bean validation in script/lambda at the moment,
+but you can use a helper that we utilize under the hood in MVC routes:
+
+.Java
+[source, java, role="primary"]
+----
+import io.jooby.validation.BeanValidator;
+
+{
+ post("/validate", ctx -> {
+ Bean bean = BeanValidator.validate(ctx, ctx.body(Bean.class));
+ ...
+ });
+}
+----
+
+.Kotlin
+[source, kt, role="secondary"]
+----
+import io.jooby.validation.BeanValidator
+
+{
+ post("/validate") {
+ val bean = BeanValidator.validate(ctx, ctx.body(Bean.class))
+ ...
+ }
+}
+----
+
+`BeanValidator.validate()` behaves identically to validation in MVC routes.
+It also supports validating list, array, and map of beans
+
+=== Constraint Violations Rendering
+
+`AvajeValidatorModule` provides default built-in error handler that
+catches `ConstraintViolationException` and transforms it into the following response:
+
+.JSON:
+----
+{
+ "title": "Validation failed",
+ "status": 422,
+ "errors": [
+ {
+ "field": "firstName",
+ "messages": [
+ "must not be empty",
+ "must not be null"
+ ],
+ "type": "FIELD"
+ },
+ {
+ "field": null,
+ "messages": [
+ "passwords are not the same"
+ ],
+ "type": "GLOBAL"
+ }
+ ]
+}
+----
+
+It is possible to override the `title` and `status` code of the response above:
+
+[source, java]
+----
+
+{
+ install(new AvajeJsonbModule());
+ install(new AvajeValidatorModule()
+ .statusCode(StatusCode.BAD_REQUEST)
+ .validationTitle("Incorrect input data")
+ );
+}
+----
+
+If the default error handler doesn't fully meet your needs, you can always disable it and provide your own:
+
+[source, java]
+----
+
+{
+ install(new AvajeJsonbModule());
+ install(new AvajeValidatorModule().disableViolationHandler());
+
+ error(ConstraintViolationException.class, new MyConstraintViolationHandler());
+}
+----
+
+=== Manual Validation
+
+The module exposes `Validator` as a service, allowing you to run validation manually at any time.
+
+==== Script/lambda:
+
+[source, java]
+----
+import io.avaje.validation.Validator;
+
+{
+ post("/validate", ctx -> {
+ Validator validator = require(Validator.class);
+ validator.validate(ctx.body(Bean.class));
+ ...
+ });
+}
+----
+
+==== MVC routes with dependency injection:
+
+1) Install DI framework at first.
+
+[source, java]
+----
+import io.jooby.avaje.validator.AvajeValidatorModule;
+
+{
+ install(AvajeInjectModule.of()); // <1>
+ install(new AvajeValidatorModule());
+}
+----
+
+<1> `Avaje` is just an example, you can achieve the same with `Dagger` or `Guice`
+
+2) Inject `Validator` in controller, service etc.
+
+[source, java]
+----
+import io.avaje.validation.Validator;
+import jakarta.inject.Inject;
+
+@Path("/mvc")
+public class Controller {
+
+ private final Validator validator;
+
+ @Inject
+ public Controller(Validator validator) {
+ this.validator = validator;
+ }
+
+ @POST("/validate")
+ public void validate(Bean bean) {
+ Set> violations = validator.validate(bean);
+ ...
+ }
+}
+----
+
+=== Configuration
+Any property defined at `validation` will be added automatically:
+
+.application.conf
+[source, properties]
+----
+validation.fail_fast = true
+----
+
+Or programmatically:
+
+[source, java]
+----
+import io.jooby.avaje.validator.AvajeValidatorModule;
+
+{
+ install(new AvajeValidatorModule().doWith(cfg -> {
+ cfg.failFast(true);
+ }));
+}
+----
\ No newline at end of file
From 77f5825304630fe3bb69a2961d0cc0334a887da4 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Fri, 6 Sep 2024 16:03:51 -0400
Subject: [PATCH 4/4] PR comments
---
docs/asciidoc/modules/avaje-validator.adoc | 41 ++++++++++++++++++-
docs/asciidoc/modules/modules.adoc | 1 +
modules/jooby-avaje-validator/pom.xml | 2 +-
.../avaje/validator/AvajeValidatorModule.java | 2 +-
.../validator/ConstraintViolationHandler.java | 2 +-
.../validator/HibernateValidatorModule.java | 2 +-
6 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/docs/asciidoc/modules/avaje-validator.adoc b/docs/asciidoc/modules/avaje-validator.adoc
index 5cbde7de88..542d646eeb 100644
--- a/docs/asciidoc/modules/avaje-validator.adoc
+++ b/docs/asciidoc/modules/avaje-validator.adoc
@@ -9,7 +9,44 @@ Bean validation via https://avaje.io/validator/[Avaje Validator].
[dependency, artifactId="jooby-avaje-validator"]
.
-2) Install
+2) Configure annotation processor
+
+.Maven
+[source, xml, role = "primary"]
+----
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ...
+
+
+
+ io.avaje
+ avaje-validator-generator
+ 2.1
+
+
+
+
+
+
+----
+
+.Gradle
+[source, kotlin, role = "secondary"]
+----
+plugins {
+ id "org.jetbrains.kotlin.kapt" version "1.9.10"
+}
+
+dependencies {
+ kapt 'io.avaje:avaje-validator-generator:2.1'
+}
+----
+
+3) Install
.Java
[source, java, role="primary"]
@@ -31,7 +68,7 @@ import io.jooby.avaje.validator.AvajeValidatorModule
}
----
-3) Usage in MVC routes
+4) Usage in MVC routes
.Java
[source,java,role="primary"]
diff --git a/docs/asciidoc/modules/modules.adoc b/docs/asciidoc/modules/modules.adoc
index 98537fc089..5a957840bf 100644
--- a/docs/asciidoc/modules/modules.adoc
+++ b/docs/asciidoc/modules/modules.adoc
@@ -27,6 +27,7 @@ Available modules are listed next.
* link:/modules/redis[Redis]: Redis module.
=== Validation
+ * link:/modules/avaje-validator[Avaje Validator]: Avaje Validator module.
* link:/modules/hibernate-validator[Hibernate Validator]: Hibernate Validator module.
=== Development Tools
diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml
index 174a57ff50..ae0ee8095e 100644
--- a/modules/jooby-avaje-validator/pom.xml
+++ b/modules/jooby-avaje-validator/pom.xml
@@ -28,7 +28,7 @@
${project.version}
-
+
io.avaje
avaje-validator
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
index a509da90bc..bdc50beae5 100644
--- a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
@@ -47,7 +47,7 @@
* io.jooby.validation.ValidationResult}
*
* @authors kliushnichenko, SentryMan
- * @since 3.2.10
+ * @since 3.3.1
*/
public class AvajeValidatorModule implements Extension {
diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java
index ad35352567..dcc5f16bbc 100644
--- a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java
+++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java
@@ -46,7 +46,7 @@
* }
*
* @author kliushnichenko
- * @since 3.2.10
+ * @since 3.3.1
*/
public class ConstraintViolationHandler implements ErrorHandler {
diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java
index 73cc55a541..5f7a3afe95 100644
--- a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java
+++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java
@@ -48,7 +48,7 @@
* and transforms it into a {@link io.jooby.validation.ValidationResult}
*
* @author kliushnichenko
- * @since 3.2.10
+ * @since 3.3.1
*/
public class HibernateValidatorModule implements Extension {