Repository: tipsy/javalin (kotlin)

Issue:
Throw a custom exception when a validator has errors
At the moment, the only way to throw a custom exception when a validator has errors is using an `if`:
```java
app.get("/users/{id}", ctx -> { 
    Validator<Long> id = ctx.pathParamAsClass("id", Long.class);
    if (id.errors().isEmpty()) {
      throw new UserNotFoundException(id.errors());
    }
    ctx.json(usersRepository.getById(id.get()));
})
app.exception(UserNotFoundException.class, (e, ctx) -> ctx.render("user-not-found.peb", e.getModel()));
```
I think it would be nice to have a `getOrThrow()` method, similar to [`orElseThrow()` from `Optional<T>`](https://docs.oracle.com/en/java/javase/17/docs/api//java.base/java/util/Optional.html#orElseThrow(java.util.function.Supplier)). The custom exception could receive the validation `errors()` via parameter and retreive relevant information from there:

```java
app.get("/users/{id}", ctx -> { 
    Long id = ctx.pathParamAsClass("id", Long.class)
        .getOrThrow(errors -> new UserNotFoundException(errors));
    ctx.json(usersRepository.getById(id));
})
app.exception(UserNotFoundException.class, (e, ctx) -> ctx.render("user-not-found.peb", e.getModel()));
```

I could add this feature if you'd like.

Pull request description:
resolves #1534

Patch:
diff --git a/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt b/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt
index 035d1e2d0..b7666e7aa 100644
--- a/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt
+++ b/javalin/src/main/java/io/javalin/core/validation/BaseValidator.kt
@@ -69,9 +69,11 @@ open class BaseValidator<T>(val fieldName: String, protected var typedValue: T?,
         return this
     }
 
-    open fun get(): T? = when {
+    open fun get(): T? = getOrThrow { ValidationException(it) }
+
+    open fun getOrThrow(exceptionFunction: (Map<String, List<ValidationError<Any>>>) -> Exception): T? = when {
         errors.isEmpty() -> typedValue
-        else -> throw ValidationException(errors as Map<String, List<ValidationError<Any>>>)
+        else -> throw exceptionFunction(errors as Map<String, List<ValidationError<Any>>>)
     }
 
     fun errors(): Map<String, List<ValidationError<T>>> = errors
diff --git a/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt b/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt
index 1bc23cbfc..bbad2b218 100644
--- a/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt
+++ b/javalin/src/main/java/io/javalin/core/validation/BodyValidator.kt
@@ -19,4 +19,7 @@ open class BodyValidator<T>(stringValue: String?, clazz: Class<T>, jsonMapper: J
 
     @NotNull // there is a null-check in BaseValidator
     override fun get(): T = super.get()!!
+
+    @NotNull
+    override fun getOrThrow(exceptionFunction: (Map<String, List<ValidationError<Any>>>) -> Exception): T = super.getOrThrow(exceptionFunction)!!
 }
diff --git a/javalin/src/main/java/io/javalin/core/validation/Validator.kt b/javalin/src/main/java/io/javalin/core/validation/Validator.kt
index 3ec1c5411..d1d70c8e8 100644
--- a/javalin/src/main/java/io/javalin/core/validation/Validator.kt
+++ b/javalin/src/main/java/io/javalin/core/validation/Validator.kt
@@ -34,6 +34,9 @@ open class Validator<T>(fieldName: String, typedValue: T? = null, stringSource:
 
     fun getOrDefault(default: T) = if (hasValue()) super.get()!! else default
 
+    @NotNull
+    override fun getOrThrow(exceptionFunction: (Map<String, List<ValidationError<Any>>>) -> Exception) = super.getOrThrow(exceptionFunction)!!
+
     companion object {
         @JvmStatic
         fun <T> create(clazz: Class<T>, value: String?, fieldName: String) = if (JavalinValidation.hasConverter(clazz)) {


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestValidation.kt b/javalin/src/test/java/io/javalin/TestValidation.kt
index 95b25dd22..72c4faaa9 100644
--- a/javalin/src/test/java/io/javalin/TestValidation.kt
+++ b/javalin/src/test/java/io/javalin/TestValidation.kt
@@ -92,6 +92,17 @@ class TestValidation {
         assertThat(http.get("/").body).isEqualTo("788")
     }
 
+    class CustomException(message: String) : RuntimeException(message)
+
+    @Test
+    fun `getOrThrow works`() = TestUtil.test { app, http ->
+        app.get("/") { ctx ->
+            val myInt = ctx.queryParamAsClass<Int>("my-qp").getOrThrow { CustomException("'${it.keys.first()}' is not a number") }
+            ctx.result(myInt.toString())
+        }.exception(CustomException::class.java) { e, ctx -> ctx.result(e.message ?: "") }
+        assertThat(http.get("/").body).isEqualTo("'my-qp' is not a number")
+    }
+
     @Test
     fun `unregistered converter fails`() = TestUtil.test { app, http ->
         app.get("/duration") { it.queryParamAsClass<Duration>("from").get() }
diff --git a/javalin/src/test/java/io/javalin/TestValidation_Java.java b/javalin/src/test/java/io/javalin/TestValidation_Java.java
index d9465365b..744aebbf3 100644
--- a/javalin/src/test/java/io/javalin/TestValidation_Java.java
+++ b/javalin/src/test/java/io/javalin/TestValidation_Java.java
@@ -14,6 +14,11 @@
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class TestValidation_Java {
+    private static class CustomException extends RuntimeException {
+        public CustomException(String message) {
+            super(message);
+        }
+    }
 
     @Test
     public void validation_on_context_works_from_java() {
@@ -32,6 +37,19 @@ public void default_values_work_from_java() {
         });
     }
 
+    @Test
+    public void get_or_throw_works_from_java() {
+        TestUtil.test((app, http) -> {
+            app.get("/", ctx -> {
+               Integer myInt = ctx.queryParamAsClass("my-qp", Integer.class)
+                   .getOrThrow(e -> new CustomException("'my-qp' is not a number"));
+               ctx.result(myInt.toString());
+            });
+            app.exception(CustomException.class, (e, ctx) -> ctx.result(e.getMessage()));
+            assertThat(http.getBody("/")).isEqualTo("'my-qp' is not a number");
+        });
+    }
+
     @Test
     public void validator_works_from_java_too() {
         JavalinValidation.register(Instant.class, v -> Instant.ofEpochMilli(Long.parseLong(v)));