diff --git a/changelog.md b/changelog.md index b428d97..e5a6fe2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,25 @@ # Vertx Utils changelog -## [1.9.0] - UNRELEASED +## [2.0.0] - UNRELEASED ### Breaking Changes -* None. +* Rebuilt to be Kotlin, so pretty much everything has changed at least a bit. If you want to use Java, keep using v1. +* The `RouteRegisterLogger` class has been replaced with the `logRegisteredRoutes` factory function. +* The following functions have been replaced with `contains`, allowing use of the Kotlin `in` operator: + * `PathParams.exists`. + * `QueryParams.exists`. + * `RouteVersion.includes`. +* `RouteVersionUtils.forVersion` has been replaced with a Kotlin inline equivalent that only needs the factory, not the available routes enum. +* All `JsonUtils` extract helpers are now extensions. ### New Features * None. ### Enhancements -* None. +* `ChunkedJsonResponse` has been upgraded to a DSL style builder. +* Added inline reified factories for the following, removing the need to pass a class: + * `ExceptionHandler.of` that only needs the handler. + * `ParamType.ofEnum`. +* `HttpChunkedJsonResponse` can now change the status of the underlying response until a `write` has occurred. +* `CaptureChunkedJsonResponse` can now be cleared, allowing it to be reused for capturing additional messages. ### Fixes * None. diff --git a/pom.xml b/pom.xml index a42021e..ef45850 100644 --- a/pom.xml +++ b/pom.xml @@ -10,13 +10,13 @@ com.zepben.maven evolve-super-pom - 0.48.0 + 0.49.0 4.0.0 com.zepben vertx-utils - 1.9.0b2 + 2.0.0b1 ${project.groupId}:${project.artifactId} Helpers and utils for working with Vert.x in Zepben projects. @@ -68,17 +68,9 @@ - com.zepben - annotations - 1.3.0 - - - org.jetbrains - annotations - RELEASE - compile + org.jetbrains.kotlin + kotlin-stdlib - io.vertx vertx-core @@ -87,37 +79,44 @@ io.vertx vertx-web - - - org.apache.commons - commons-text + io.vertx + vertx-lang-kotlin + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk7 + + - org.apache.commons - commons-collections4 + org.slf4j + slf4j-api + com.google.guava guava - - org.slf4j - slf4j-api - - - - commons-io - commons-io - test - com.zepben test-utils - 3.2.0b1 + 3.3.1 test @@ -126,7 +125,6 @@ io.rest-assured rest-assured - 5.3.2 provided @@ -137,13 +135,12 @@ org.mockito mockito-core - ${mockito.version} provided - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - ${kotlin.version} + org.mockito.kotlin + mockito-kotlin + test org.jetbrains.kotlin @@ -151,6 +148,11 @@ ${kotlin.version} test + + org.slf4j + slf4j-simple + test + diff --git a/src/main/java/com/zepben/vertxutils/json/Collectors.java b/src/main/java/com/zepben/vertxutils/json/Collectors.java deleted file mode 100644 index 700f1f8..0000000 --- a/src/main/java/com/zepben/vertxutils/json/Collectors.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonArray; - -import java.util.stream.Collector; - -@EverythingIsNonnullByDefault -@SuppressWarnings("WeakerAccess") -public class Collectors { - - public static Collector toJsonArray() { - return Collector.of(JsonArray::new, JsonArray::add, JsonArray::addAll); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/Collectors.kt b/src/main/java/com/zepben/vertxutils/json/Collectors.kt new file mode 100644 index 0000000..de2fe3c --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/Collectors.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import io.vertx.core.json.JsonArray + +object Collectors { + + fun Sequence.toJsonArray(): JsonArray = + JsonArray(toList()) + +} diff --git a/src/main/java/com/zepben/vertxutils/json/JsonArrayValueExtractor.java b/src/main/java/com/zepben/vertxutils/json/JsonArrayValueExtractor.java deleted file mode 100644 index 0021663..0000000 --- a/src/main/java/com/zepben/vertxutils/json/JsonArrayValueExtractor.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import io.vertx.core.json.JsonArray; - -@FunctionalInterface -public interface JsonArrayValueExtractor { - T extractValue(JsonArray jsonArray, int index); -} diff --git a/src/main/java/com/zepben/vertxutils/json/JsonUtils.java b/src/main/java/com/zepben/vertxutils/json/JsonUtils.java deleted file mode 100644 index 1f942ad..0000000 --- a/src/main/java/com/zepben/vertxutils/json/JsonUtils.java +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import org.jetbrains.annotations.Contract; - -import javax.annotation.Nullable; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class JsonUtils { - - @EverythingIsNonnullByDefault - public static class ParsingException extends Exception { - - public ParsingException(String message) { - super(message); - } - - public ParsingException(String message, Throwable cause) { - super(message, cause); - } - - } - - /** - * Get the value from the specified key. - * - * @param json json object to extract the value from - * @param key the key containing the value - * @return optional of the value - */ - public static Optional extractOptionalValue(JsonObject json, String key) { - // NOTE: We don't use the generic extract optional here to avoid having the - // un-throwable checked exception added to the signature. - return Optional.ofNullable(json.getValue(key)); - } - - /** - * Get the value from the specified key. - * - * @param json json object to extract the value from - * @param key the key containing the value - * @return the value - * @throws ParsingException if the key is not found - */ - public static Object extractRequiredValue(JsonObject json, String key) throws ParsingException { - return extractRequired(json, key, JsonObject::getValue, "value"); - } - - /** - * Get the object value from the specified key. - * - * @param json json object to extract the object from - * @param key the key containing the object - * @return optional of the value - * @throws ParsingException if the value is not an object - */ - public static Optional extractOptionalObject(JsonObject json, String key) throws ParsingException { - return extractOptional(json, key, JsonObject::getJsonObject, "object"); - } - - /** - * Get the object value from the specified key. - * - * @param json json object to extract the object from - * @param key the key containing the object - * @return the value - * @throws ParsingException if the value is not an object, or is not found - */ - public static JsonObject extractRequiredObject(JsonObject json, String key) throws ParsingException { - return extractRequired(json, key, JsonObject::getJsonObject, "object"); - } - - /** - * Get the array value from the specified key. - * - * @param json json object to extract the array from - * @param key the key containing the array - * @return optional of the value - * @throws ParsingException if the value is not an array - */ - public static Optional extractOptionalArray(JsonObject json, String key) throws ParsingException { - return extractOptional(json, key, JsonObject::getJsonArray, "array"); - } - - /** - * Get the array value from the specified key. - * - * @param json json object to extract the array from - * @param key the key containing the array - * @return the value - * @throws ParsingException if the value is not an array, or is not found - */ - public static JsonArray extractRequiredArray(JsonObject json, String key) throws ParsingException { - return extractRequired(json, key, JsonObject::getJsonArray, "array"); - } - - /** - * Get the string value from the specified key. - * - * @param json json object to extract the string from - * @param key the key containing the string - * @return optional of the value - * @throws ParsingException if the value is not a string - */ - public static Optional extractOptionalString(JsonObject json, String key) throws ParsingException { - return extractOptional(json, key, JsonValueExtractors::getStringStrict, "string"); - } - - /** - * Get the string value from the specified key. - * - * @param json json object to extract the string from - * @param key the key containing the string - * @return the value - * @throws ParsingException if the value is not a string, or is not found - */ - public static String extractRequiredString(JsonObject json, String key) throws ParsingException { - return extractRequired(json, key, JsonValueExtractors::getStringStrict, "string"); - } - - /** - * Get the integer value from the specified key. - * - * @param json json object to extract the integer from - * @param key the key containing the integer - * @return optional of the value - * @throws ParsingException if the value is not an integer - */ - public static Optional extractOptionalInt(JsonObject json, String key) throws ParsingException { - return extractOptional(json, key, JsonObject::getInteger, "integer"); - } - - /** - * Get the integer value from the specified key. - * - * @param json json object to extract the integer from - * @param key the key containing the integer - * @return the value - * @throws ParsingException if the value is not an integer, or is not found - */ - public static int extractRequiredInt(JsonObject json, String key) throws ParsingException { - return extractRequired(json, key, JsonObject::getInteger, "integer"); - } - - /** - * Get the double value from the specified key. - * - * @param json json object to extract the double from - * @param key the key containing the double - * @return optional of the value - * @throws ParsingException if the value is not a double - */ - public static Optional extractOptionalDouble(JsonObject json, String key) throws ParsingException { - return extractOptional(json, key, JsonValueExtractors::getDouble, "double"); - } - - /** - * Get the double value from the specified key. - * - * @param json json object to extract the double from - * @param key the key containing the double - * @return the value - * @throws ParsingException if the value is not a double, or is not found - */ - public static double extractRequiredDouble(JsonObject json, String key) throws ParsingException { - return extractRequired(json, key, JsonValueExtractors::getDouble, "double"); - } - - /** - * Get the path value from the specified key. - * - * @param json json object to extract the path from - * @param key the key containing the path - * @return optional of the value - * @throws ParsingException if the value is not a path - */ - public static Optional extractOptionalPath(JsonObject json, String key) throws ParsingException { - return extractOptional(json, key, JsonValueExtractors::getPath, "path"); - } - - /** - * Get the path value from the specified key. - * - * @param json json object to extract the path from - * @param key the key containing the path - * @return the value - * @throws ParsingException if the value is not a path, or is not found - */ - public static Path extractRequiredPath(JsonObject json, String key) throws ParsingException { - return extractRequired(json, key, JsonValueExtractors::getPath, "path"); - } - - /** - * Get the JsonObjects from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return optional of a list containing each of the JsonObjects contained in the specified array. - * @throws ParsingException if the value is not a list of objects. - */ - public static Optional> extractOptionalObjectList(JsonObject json, String key) throws ParsingException { - return extractOptionalList(json, key, JsonArray::getJsonObject, "objects"); - } - - /** - * Get the JsonObjects from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return a list containing each of the JsonObjects contained in the specified array. - * @throws ParsingException if the value is not a list of objects, or is not found. - */ - public static List extractRequiredObjectList(JsonObject json, String key) throws ParsingException { - return extractRequiredList(json, key, JsonArray::getJsonObject, "objects"); - } - - /** - * Get the strings from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return optional of a list containing each of the strings contained in the specified array. - * @throws ParsingException if the value is not a list of strings. - */ - public static Optional> extractOptionalStringList(JsonObject json, String key) throws ParsingException { - return extractOptionalList(json, key, JsonValueExtractors::getStringStrict, "strings"); - } - - /** - * Get the strings from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return a list containing each of the strings contained in the specified array. - * @throws ParsingException if the value is not a list of strings, or is not found. - */ - public static List extractRequiredStringList(JsonObject json, String key) throws ParsingException { - return extractRequiredList(json, key, JsonValueExtractors::getStringStrict, "strings"); - } - - /** - * Get the integers from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return optional of a list containing each of the integers contained in the specified array. - * @throws ParsingException if the value is not a list of integers. - */ - public static Optional> extractOptionalIntList(JsonObject json, String key) throws ParsingException { - return extractOptionalList(json, key, JsonArray::getInteger, "integers"); - } - - /** - * Get the integers from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return a list containing each of the integers contained in the specified array. - * @throws ParsingException if the value is not a list of integers, or is not found. - */ - public static List extractRequiredIntList(JsonObject json, String key) throws ParsingException { - return extractRequiredList(json, key, JsonArray::getInteger, "integers"); - } - - /** - * Get the doubles from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return optional of a list containing each of the doubles contained in the specified array. - * @throws ParsingException if the value is not a list of doubles. - */ - public static Optional> extractOptionalDoubleList(JsonObject json, String key) throws ParsingException { - return extractOptionalList(json, key, JsonValueExtractors::getDouble, "doubles"); - } - - /** - * Get the doubles from a JsonArray with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return a list containing each of the doubles contained in the specified array. - * @throws ParsingException if the value is not a list of doubles, or is not found. - */ - public static List extractRequiredDoubleList(JsonObject json, String key) throws ParsingException { - return extractRequiredList(json, key, JsonValueExtractors::getDouble, "doubles"); - } - - /** - * Get the JsonObjects from a JsonArray of JsonArrays with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return optional of a list of lists containing each of the JsonObjects contained in the specified array or arrays. - * @throws ParsingException if the value is not an object list. - */ - public static Optional>> extractOptionalObjectListOfList(JsonObject json, String key) throws ParsingException { - try { - return extractOptionalList(json, key, JsonArray::getJsonArray, "object lists") - .map(jsonArrays -> jsonArrays - .stream() - .map(ja -> uncheckedConvertToList(ja, JsonArray::getJsonObject)) - .collect(Collectors.toList())); - } catch (ClassCastException e) { - throw new ParsingException(String.format("Value for '%s' is not a valid list of object lists.", key), e); - } - } - - /** - * Get the JsonObjects from a JsonArray of JsonArrays with the specified key as a list. - * - * @param json the object containing the array. - * @param key the key of the array in the object. - * @return a list of lists containing each of the JsonObjects contained in the specified array or arrays. - * @throws ParsingException if the value is not an object list. - */ - public static List> extractRequiredObjectListOfList(JsonObject json, String key) throws ParsingException { - Optional>> lists = extractOptionalObjectListOfList(json, key); - return ensureRequired(key, lists::isPresent, lists::get); - } - - /** - * @param jsonArray the array containing the objects. - * @return a list of the objects contained in the array. - * @throws ParsingException if the array does not contain objects. - */ - public static List convertToObjectList(JsonArray jsonArray) throws ParsingException { - return convertToList(jsonArray, JsonArray::getJsonObject); - } - - /** - * @param jsonArray the array containing the objects. - * @param valueExtractor the method used to extract the value from the array. - * @return a list of the objects contained in the array. - * @throws ParsingException if the array does not contain objects. - */ - public static List convertToList(JsonArray jsonArray, JsonArrayValueExtractor valueExtractor) throws ParsingException { - try { - return uncheckedConvertToList(jsonArray, valueExtractor); - } catch (ClassCastException e) { - throw new ParsingException("JSON array is not a collection of expected types.", e); - } - } - - /** - * @param jsonArray the array containing the objects. - * @param valueExtractor the method used to extract the value from the array. - * @param expectedCount the expected number of entries in the list. - * @return a list of the objects contained in the array. - * @throws ParsingException if the array does not contain objects. - */ - public static List convertToList(JsonArray jsonArray, JsonArrayValueExtractor valueExtractor, int expectedCount) throws ParsingException { - if (jsonArray.size() != expectedCount) - throw new ParsingException(String.format("Invalid number of records in list. Expected exactly %d, found %d.", expectedCount, jsonArray.size())); - - return convertToList(jsonArray, valueExtractor); - } - - private static Optional extractOptional(JsonObject json, - String key, - ValueExtractor valueExtractor, - String description) throws ParsingException { - try { - return Optional.ofNullable(valueExtractor.extract(json, key)); - } catch (Exception e) { - throw new ParsingException(String.format("Value for '%s' is not a valid %s.", key, description), e); - } - } - - private static Optional> extractOptionalList(JsonObject json, - String key, - JsonArrayValueExtractor valueExtractor, - String description) throws ParsingException { - try { - return Optional.ofNullable(uncheckedConvertToList(extractOptionalArray(json, key).orElse(null), valueExtractor)); - } catch (ClassCastException e) { - throw new ParsingException(String.format("Value for '%s' is not a valid list of %s.", key, description), e); - } - } - - public static T extractRequired(JsonObject json, - String key, - ValueExtractor valueExtractor, - String description) throws ParsingException { - Optional value = extractOptional(json, key, valueExtractor, description); - return ensureRequired(key, value::isPresent, value::get); - } - - public static List extractRequiredList(JsonObject json, - String key, - JsonArrayValueExtractor valueExtractor, - String description) throws ParsingException { - Optional> value = extractOptionalList(json, key, valueExtractor, description); - return ensureRequired(key, value::isPresent, value::get); - } - - public static T ensureRequired(String key, Supplier isPresentSupplier, Supplier valueSupplier) throws ParsingException { - if (!isPresentSupplier.get()) - throw new ParsingException(String.format("No value found for required key '%s'.", key)); - - return valueSupplier.get(); - } - - @Nullable - @Contract("null, _ -> null; !null, _ -> !null") - private static List uncheckedConvertToList(@Nullable JsonArray jsonArray, JsonArrayValueExtractor valueExtractor) { - if (jsonArray != null) - return IntStream.range(0, jsonArray.size()).mapToObj(i -> valueExtractor.extractValue(jsonArray, i)).collect(Collectors.toList()); - else - return null; - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/JsonUtils.kt b/src/main/java/com/zepben/vertxutils/json/JsonUtils.kt new file mode 100644 index 0000000..c6c2c58 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/JsonUtils.kt @@ -0,0 +1,348 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import java.nio.file.Path + +typealias JsonArrayValueExtractor = (jsonArray: JsonArray, pos: Int) -> T? + +object JsonUtils { + + /** + * Get the value from the specified key. + * + * @receiver json object to extract the value from + * @param key the key containing the value + * @return optional of the value + */ + fun JsonObject.extractOptionalValue(key: String): Any? = + getValue(key) + + /** + * Get the value from the specified key. + * + * @receiver json object to extract the value from + * @param key the key containing the value + * @return the value + * @throws ParsingException if the key is not found + */ + fun JsonObject.extractRequiredValue(key: String): Any = + this.extractRequired(key, "value") { obj, key -> obj.getValue(key) } + + /** + * Get the object value from the specified key. + * + * @receiver json object to extract the object from + * @param key the key containing the object + * @return optional of the value + * @throws ParsingException if the value is not an object + */ + fun JsonObject.extractOptionalObject(key: String): JsonObject? = + this.extractOptional(key, "object") { obj, key -> obj.getJsonObject(key) } + + /** + * Get the object value from the specified key. + * + * @receiver json object to extract the object from + * @param key the key containing the object + * @return the value + * @throws ParsingException if the value is not an object, or is not found + */ + fun JsonObject.extractRequiredObject(key: String): JsonObject = + this.extractRequired(key, "object") { obj, key -> obj.getJsonObject(key) } + + /** + * Get the array value from the specified key. + * + * @receiver json object to extract the array from + * @param key the key containing the array + * @return optional of the value + * @throws ParsingException if the value is not an array + */ + fun JsonObject.extractOptionalArray(key: String): JsonArray? = + this.extractOptional(key, "array") { obj, key -> obj.getJsonArray(key) } + + /** + * Get the array value from the specified key. + * + * @receiver json object to extract the array from + * @param key the key containing the array + * @return the value + * @throws ParsingException if the value is not an array, or is not found + */ + fun JsonObject.extractRequiredArray(key: String): JsonArray = + this.extractRequired(key, "array") { obj, key -> obj.getJsonArray(key) } + + /** + * Get the string value from the specified key. + * + * @receiver json object to extract the string from + * @param key the key containing the string + * @return optional of the value + * @throws ParsingException if the value is not a string + */ + fun JsonObject.extractOptionalString(key: String): String? = + this.extractOptional(key, "string") { obj, key -> JsonValueExtractors.getStringStrict(obj, key) } + + /** + * Get the string value from the specified key. + * + * @receiver json object to extract the string from + * @param key the key containing the string + * @return the value + * @throws ParsingException if the value is not a string, or is not found + */ + fun JsonObject.extractRequiredString(key: String): String = + this.extractRequired(key, "string") { obj, key -> JsonValueExtractors.getStringStrict(obj, key) } + + /** + * Get the integer value from the specified key. + * + * @receiver json object to extract the integer from + * @param key the key containing the integer + * @return optional of the value + * @throws ParsingException if the value is not an integer + */ + fun JsonObject.extractOptionalInt(key: String): Int? = + this.extractOptional(key, "integer") { obj, key -> obj.getInteger(key) } + + /** + * Get the integer value from the specified key. + * + * @receiver json object to extract the integer from + * @param key the key containing the integer + * @return the value + * @throws ParsingException if the value is not an integer, or is not found + */ + fun JsonObject.extractRequiredInt(key: String): Int = + this.extractRequired(key, "integer") { obj, key -> obj.getInteger(key) } + + /** + * Get the double value from the specified key. + * + * @receiver json object to extract the double from + * @param key the key containing the double + * @return optional of the value + * @throws ParsingException if the value is not a double + */ + fun JsonObject.extractOptionalDouble(key: String): Double? = + this.extractOptional(key, "double") { obj, key -> JsonValueExtractors.getDouble(obj, key) } + + /** + * Get the double value from the specified key. + * + * @receiver json object to extract the double from + * @param key the key containing the double + * @return the value + * @throws ParsingException if the value is not a double, or is not found + */ + fun JsonObject.extractRequiredDouble(key: String): Double = + this.extractRequired(key, "double") { obj, key -> JsonValueExtractors.getDouble(obj, key) } + + /** + * Get the path value from the specified key. + * + * @receiver json object to extract the path from + * @param key the key containing the path + * @return optional of the value + * @throws ParsingException if the value is not a path + */ + fun JsonObject.extractOptionalPath(key: String): Path? = + this.extractOptional(key, "path") { obj, key -> JsonValueExtractors.getPath(obj, key) } + + /** + * Get the path value from the specified key. + * + * @receiver json object to extract the path from + * @param key the key containing the path + * @return the value + * @throws ParsingException if the value is not a path, or is not found + */ + fun JsonObject.extractRequiredPath(key: String): Path = + this.extractRequired(key, "path") { obj, key -> JsonValueExtractors.getPath(obj, key) } + + /** + * Get the JsonObjects from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return optional of a list containing each of the JsonObjects contained in the specified array. + * @throws ParsingException if the value is not a list of objects. + */ + fun JsonObject.extractOptionalObjectList(key: String): List? = + this.extractOptionalList(key, "objects") { jsonArray, pos -> jsonArray.getJsonObject(pos) } + + /** + * Get the JsonObjects from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return a list containing each of the JsonObjects contained in the specified array. + * @throws ParsingException if the value is not a list of objects, or is not found. + */ + fun JsonObject.extractRequiredObjectList(key: String): List = + this.extractRequiredList(key, "objects") { jsonArray, pos -> jsonArray.getJsonObject(pos) } + + /** + * Get the strings from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return optional of a list containing each of the strings contained in the specified array. + * @throws ParsingException if the value is not a list of strings. + */ + fun JsonObject.extractOptionalStringList(key: String): List? = + this.extractOptionalList(key, "strings") { jsonArray, pos -> JsonValueExtractors.getStringStrict(jsonArray, pos) } + + /** + * Get the strings from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return a list containing each of the strings contained in the specified array. + * @throws ParsingException if the value is not a list of strings, or is not found. + */ + fun JsonObject.extractRequiredStringList(key: String): List = + this.extractRequiredList(key, "strings") { jsonArray, pos -> JsonValueExtractors.getStringStrict(jsonArray, pos) } + + /** + * Get the integers from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return optional of a list containing each of the integers contained in the specified array. + * @throws ParsingException if the value is not a list of integers. + */ + fun JsonObject.extractOptionalIntList(key: String): List? = + this.extractOptionalList(key, "integers") { jsonArray, pos -> jsonArray.getInteger(pos) } + + /** + * Get the integers from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return a list containing each of the integers contained in the specified array. + * @throws ParsingException if the value is not a list of integers, or is not found. + */ + fun JsonObject.extractRequiredIntList(key: String): List = + this.extractRequiredList(key, "integers") { jsonArray, pos -> jsonArray.getInteger(pos) } + + /** + * Get the doubles from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return optional of a list containing each of the doubles contained in the specified array. + * @throws ParsingException if the value is not a list of doubles. + */ + fun JsonObject.extractOptionalDoubleList(key: String): List? = + this.extractOptionalList(key, "doubles") { jsonArray, pos -> JsonValueExtractors.getDouble(jsonArray, pos) } + + /** + * Get the doubles from a JsonArray with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return a list containing each of the doubles contained in the specified array. + * @throws ParsingException if the value is not a list of doubles, or is not found. + */ + fun JsonObject.extractRequiredDoubleList(key: String): List = + this.extractRequiredList(key, "doubles") { jsonArray, pos -> JsonValueExtractors.getDouble(jsonArray, pos) } + + /** + * Get the JsonObjects from a JsonArray of JsonArrays with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return optional of a list of lists containing each of the JsonObjects contained in the specified array or arrays. + * @throws ParsingException if the value is not an object list. + */ + fun JsonObject.extractOptionalObjectListOfList(key: String): List?>? = + try { + this.extractOptionalList(key, "object lists") { jsonArray, pos -> jsonArray.getJsonArray(pos) } + ?.map { it?.doConvertToList { jsonArray, pos -> jsonArray.getJsonObject(pos) } } + } catch (e: ClassCastException) { + throw ParsingException("Value for '$key' is not a valid list of object lists.", e) + } + + /** + * Get the JsonObjects from a JsonArray of JsonArrays with the specified key as a list. + * + * @receiver the object containing the array. + * @param key the key of the array in the object. + * @return a list of lists containing each of the JsonObjects contained in the specified array or arrays. + * @throws ParsingException if the value is not an object list. + */ + fun JsonObject.extractRequiredObjectListOfList(key: String): List?> = + ensureRequired(key, extractOptionalObjectListOfList(key)) + + /** + * @receiver the array containing the objects. + * @return a list of the objects contained in the array. + * @throws ParsingException if the array does not contain objects. + */ + fun JsonArray.convertToObjectList(): List = + this.convertToList { jsonArray, pos -> jsonArray.getJsonObject(pos) } + + /** + * @receiver the array containing the objects. + * @param valueExtractor the method used to extract the value from the array. + * @return a list of the objects contained in the array. + * @throws ParsingException if the array does not contain objects. + */ + fun JsonArray.convertToList(valueExtractor: JsonArrayValueExtractor): List = + try { + doConvertToList(valueExtractor) + } catch (e: ClassCastException) { + throw ParsingException("JSON array is not a collection of expected types.", e) + } + + /** + * @receiver the array containing the objects. + * @param valueExtractor the method used to extract the value from the array. + * @param expectedCount the expected number of entries in the list. + * @return a list of the objects contained in the array. + * @throws ParsingException if the array does not contain objects. + */ + fun JsonArray.convertToList(expectedCount: Int, valueExtractor: JsonArrayValueExtractor): List = + if (size() == expectedCount) + convertToList(valueExtractor) + else + throw ParsingException("Invalid number of records in list. Expected exactly $expectedCount, found ${size()}.") + + private fun JsonObject.extractOptional(key: String, description: String, valueExtractor: (JsonObject, String) -> T?): T? = + try { + valueExtractor(this, key) + } catch (e: Exception) { + throw ParsingException("Value for '$key' is not a valid $description.", e) + } + + private fun JsonObject.extractOptionalList(key: String, description: String, valueExtractor: JsonArrayValueExtractor): List? = + try { + extractOptionalArray(key)?.doConvertToList(valueExtractor) + } catch (e: ClassCastException) { + throw ParsingException("Value for '$key' is not a valid list of $description.", e) + } + + fun JsonObject.extractRequired(key: String, description: String, valueExtractor: (JsonObject, String) -> T?): T = + ensureRequired(key, extractOptional(key, description, valueExtractor)) + + fun JsonObject.extractRequiredList(key: String, description: String, valueExtractor: JsonArrayValueExtractor): List = + ensureRequired(key, extractOptionalList(key, description, valueExtractor)) + + fun ensureRequired(key: String, value: T?): T = + value ?: throw ParsingException("No value found for required key '$key'.") + + private fun JsonArray.doConvertToList(valueExtractor: JsonArrayValueExtractor): List = + (0.. valueExtractor(this, pos) }.toList() + + class ParsingException(message: String, cause: Throwable? = null) : Exception(message, cause) + +} diff --git a/src/main/java/com/zepben/vertxutils/json/JsonValueExtractors.java b/src/main/java/com/zepben/vertxutils/json/JsonValueExtractors.java deleted file mode 100644 index 59938a2..0000000 --- a/src/main/java/com/zepben/vertxutils/json/JsonValueExtractors.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; - -import javax.annotation.Nullable; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Objects; - -@EverythingIsNonnullByDefault -@SuppressWarnings("WeakerAccess") -public class JsonValueExtractors { - - @Nullable - public static Double getDouble(JsonObject jsonObject, String key) { - try { - return jsonObject.getDouble(key); - } catch (ClassCastException e) { - return checkForNaN(Objects.requireNonNull(getStringStrict(jsonObject, key)), e); - } - } - - @Nullable - public static Double getDouble(JsonArray jsonArray, int index) { - try { - return jsonArray.getDouble(index); - } catch (ClassCastException e) { - return checkForNaN(Objects.requireNonNull(getStringStrict(jsonArray, index)), e); - } - } - - @Nullable - public static Path getPath(JsonObject json, String key) { - String string = getStringStrict(json, key); - if (string != null) - return Paths.get(string); - else - return null; - } - - /** - * JsonObject::getString in VertX automatically converts some non-strings to strings, such as numbers. - * This function ensures that the value in the object is actually a string. - * @return jsonObject[key] if it's a string, null if it isn't found - * @throws ClassCastException if jsonObject[key] is not a string - */ - @Nullable - public static String getStringStrict(JsonObject jsonObject, String key) throws ClassCastException { - return (String) jsonObject.getValue(key); - } - - /** - * JsonObject::getString in VertX automatically converts some non-strings to strings, such as numbers. - * This function ensures that the value in the array is actually a string. - * @return jsonArray[index] if it's a string, null if it isn't found - * @throws ClassCastException if jsonArray[index] is not a string - */ - @Nullable - public static String getStringStrict(JsonArray jsonArray, int index) throws ClassCastException { - return (String) jsonArray.getValue(index); - } - - private static Double checkForNaN(String value, ClassCastException e) { - if (value.equals("NaN")) - return Double.NaN; - else - throw e; - } -} diff --git a/src/main/java/com/zepben/vertxutils/json/JsonValueExtractors.kt b/src/main/java/com/zepben/vertxutils/json/JsonValueExtractors.kt new file mode 100644 index 0000000..c2b10d2 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/JsonValueExtractors.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import java.nio.file.Path +import java.nio.file.Paths + +object JsonValueExtractors { + + /** + * A wrapper for the built in `getDouble` that also supports "NaN" strings. + */ + fun getDouble(jsonObject: JsonObject, key: String): Double? = + try { + jsonObject.getDouble(key) + } catch (e: ClassCastException) { + checkForNaN(requireNotNull(getStringStrict(jsonObject, key)), e) + } + + /** + * A wrapper for the built in `getDouble` that also supports "NaN" strings. + */ + fun getDouble(jsonArray: JsonArray, pos: Int): Double? = + try { + jsonArray.getDouble(pos) + } catch (e: ClassCastException) { + checkForNaN(requireNotNull(getStringStrict(jsonArray, pos)), e) + } + + fun getPath(json: JsonObject, key: String): Path? = + getStringStrict(json, key)?.let { Paths.get(it) } + + /** + * JsonObject::getString in VertX automatically converts some non-strings to strings, such as numbers. + * This function ensures that the value in the object is actually a string. + * @return jsonObject[key] if it's a string, null if it isn't found + * @throws ClassCastException if jsonObject[key] is not a string + */ + @Throws(ClassCastException::class) + fun getStringStrict(jsonObject: JsonObject, key: String): String? = + jsonObject.getValue(key) as String? + + /** + * JsonObject::getString in VertX automatically converts some non-strings to strings, such as numbers. + * This function ensures that the value in the array is actually a string. + * @return jsonArray[pos] if it's a string, null if it isn't found + * @throws ClassCastException if jsonArray[pos] is not a string + */ + @Throws(ClassCastException::class) + fun getStringStrict(jsonArray: JsonArray, pos: Int): String? = + jsonArray.getValue(pos) as String? + + private fun checkForNaN(value: String, e: ClassCastException): Double = + when (value) { + "NaN" -> Double.NaN + else -> throw e + } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/LazyJsonArray.java b/src/main/java/com/zepben/vertxutils/json/LazyJsonArray.java deleted file mode 100644 index 9f53646..0000000 --- a/src/main/java/com/zepben/vertxutils/json/LazyJsonArray.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; - -import java.time.Instant; -import java.util.Iterator; -import java.util.List; -import java.util.Spliterator; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; - -@EverythingIsNonnullByDefault -public class LazyJsonArray extends io.vertx.core.json.JsonArray { - - private final Supplier supplier; - private boolean materialised = false; - - public LazyJsonArray(Supplier supplier) { - this.supplier = supplier; - } - - @Override - public String getString(int pos) { - checkLoad(); - return super.getString(pos); - } - - @Override - public Integer getInteger(int pos) { - checkLoad(); - return super.getInteger(pos); - } - - @Override - public Long getLong(int pos) { - checkLoad(); - return super.getLong(pos); - } - - @Override - public Double getDouble(int pos) { - checkLoad(); - return super.getDouble(pos); - } - - @Override - public Float getFloat(int pos) { - checkLoad(); - return super.getFloat(pos); - } - - @Override - public Boolean getBoolean(int pos) { - checkLoad(); - return super.getBoolean(pos); - } - - @Override - public JsonObject getJsonObject(int pos) { - checkLoad(); - return super.getJsonObject(pos); - } - - @Override - public JsonArray getJsonArray(int pos) { - checkLoad(); - return super.getJsonArray(pos); - } - - @Override - public byte[] getBinary(int pos) { - checkLoad(); - return super.getBinary(pos); - } - - @Override - public Instant getInstant(int pos) { - checkLoad(); - return super.getInstant(pos); - } - - @Override - public Object getValue(int pos) { - checkLoad(); - return super.getValue(pos); - } - - @Override - public boolean hasNull(int pos) { - checkLoad(); - return super.hasNull(pos); - } - - @Override - public boolean contains(Object value) { - checkLoad(); - return super.contains(value); - } - - @Override - public boolean remove(Object value) { - checkLoad(); - return super.remove(value); - } - - @Override - public Object remove(int pos) { - checkLoad(); - return super.remove(pos); - } - - @Override - public int size() { - checkLoad(); - return super.size(); - } - - @Override - public boolean isEmpty() { - checkLoad(); - return super.isEmpty(); - } - - @Override - public List getList() { - checkLoad(); - return super.getList(); - } - - @Override - public Iterator iterator() { - checkLoad(); - return super.iterator(); - } - - @Override - public String encode() { - checkLoad(); - return super.encode(); - } - - @Override - public Buffer toBuffer() { - checkLoad(); - return super.toBuffer(); - } - - @Override - public String encodePrettily() { - checkLoad(); - return super.encodePrettily(); - } - - @Override - public JsonArray copy() { - checkLoad(); - return super.copy(); - } - - @Override - public Stream stream() { - checkLoad(); - return super.stream(); - } - - @Override - public String toString() { - checkLoad(); - return super.toString(); - } - - @Override - public boolean equals(Object o) { - checkLoad(); - return super.equals(o); - } - - @Override - public int hashCode() { - checkLoad(); - return super.hashCode(); - } - - @Override - public void writeToBuffer(Buffer buffer) { - checkLoad(); - super.writeToBuffer(buffer); - } - - @Override - public int readFromBuffer(int pos, Buffer buffer) { - checkLoad(); - return super.readFromBuffer(pos, buffer); - } - - @Override - public void forEach(Consumer action) { - checkLoad(); - super.forEach(action); - } - - @Override - public Spliterator spliterator() { - checkLoad(); - return super.spliterator(); - } - - private void checkLoad() { - if (!materialised) { - materialised = true; - super.addAll(supplier.get()); - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/LazyJsonArray.kt b/src/main/java/com/zepben/vertxutils/json/LazyJsonArray.kt new file mode 100644 index 0000000..700f68f --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/LazyJsonArray.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import io.vertx.core.buffer.Buffer +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import java.time.Instant +import java.util.* +import java.util.function.Consumer +import java.util.stream.Stream + +class LazyJsonArray(private val supplier: () -> JsonArray) : JsonArray() { + + private var materialised = false + + override fun getString(pos: Int): String? { + checkLoad() + return super.getString(pos) + } + + override fun getInteger(pos: Int): Int? { + checkLoad() + return super.getInteger(pos) + } + + override fun getLong(pos: Int): Long? { + checkLoad() + return super.getLong(pos) + } + + override fun getDouble(pos: Int): Double? { + checkLoad() + return super.getDouble(pos) + } + + override fun getFloat(pos: Int): Float? { + checkLoad() + return super.getFloat(pos) + } + + override fun getBoolean(pos: Int): Boolean? { + checkLoad() + return super.getBoolean(pos) + } + + override fun getJsonObject(pos: Int): JsonObject? { + checkLoad() + return super.getJsonObject(pos) + } + + override fun getJsonArray(pos: Int): JsonArray? { + checkLoad() + return super.getJsonArray(pos) + } + + override fun getBinary(pos: Int): ByteArray? { + checkLoad() + return super.getBinary(pos) + } + + override fun getInstant(pos: Int): Instant? { + checkLoad() + return super.getInstant(pos) + } + + override fun getValue(pos: Int): Any? { + checkLoad() + return super.getValue(pos) + } + + override fun hasNull(pos: Int): Boolean { + checkLoad() + return super.hasNull(pos) + } + + override operator fun contains(value: Any): Boolean { + checkLoad() + return super.contains(value) + } + + override fun remove(value: Any): Boolean { + checkLoad() + return super.remove(value) + } + + override fun remove(pos: Int): Any { + checkLoad() + return super.remove(pos) + } + + override fun size(): Int { + checkLoad() + return super.size() + } + + override fun isEmpty(): Boolean { + checkLoad() + return super.isEmpty() + } + + override fun getList(): MutableList<*> { + checkLoad() + return super.getList() + } + + override fun iterator(): MutableIterator { + checkLoad() + return super.iterator() + } + + override fun encode(): String { + checkLoad() + return super.encode() + } + + override fun toBuffer(): Buffer { + checkLoad() + return super.toBuffer() + } + + override fun encodePrettily(): String { + checkLoad() + return super.encodePrettily() + } + + override fun copy(): JsonArray { + checkLoad() + return super.copy() + } + + override fun stream(): Stream { + checkLoad() + return super.stream() + } + + override fun toString(): String { + checkLoad() + return super.toString() + } + + override fun equals(o: Any?): Boolean { + checkLoad() + return super.equals(o) + } + + override fun hashCode(): Int { + checkLoad() + return super.hashCode() + } + + override fun writeToBuffer(buffer: Buffer) { + checkLoad() + super.writeToBuffer(buffer) + } + + override fun readFromBuffer(pos: Int, buffer: Buffer): Int { + checkLoad() + return super.readFromBuffer(pos, buffer) + } + + override fun forEach(action: Consumer) { + checkLoad() + super.forEach(action) + } + + fun forEach(action: (Any?) -> Unit) { + checkLoad() + super.forEach(action) + } + + override fun spliterator(): Spliterator { + checkLoad() + return super.spliterator() + } + + private fun checkLoad() { + if (!materialised) { + materialised = true + super.addAll(supplier()) + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/LazyJsonObject.java b/src/main/java/com/zepben/vertxutils/json/LazyJsonObject.java deleted file mode 100644 index 2975773..0000000 --- a/src/main/java/com/zepben/vertxutils/json/LazyJsonObject.java +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import org.apache.commons.lang3.ObjectUtils; - -import javax.annotation.Nullable; -import java.time.Instant; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; - -@EverythingIsNonnullByDefault -@SuppressWarnings({"WeakerAccess"}) -public class LazyJsonObject extends JsonObject { - - @Nullable - private Supplier jsonObjectSupplier; - private boolean materialised = false; - private final Map> fieldSuppliers = new HashMap<>(); - - public LazyJsonObject(Supplier supplier) { - this.jsonObjectSupplier = supplier; - } - - public LazyJsonObject() { - } - - @Override - public T mapTo(Class type) { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.mapTo(type); - } - - @Override - public String getString(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getString(key); - } - - @Override - public Integer getInteger(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getInteger(key); - } - - @Override - public Long getLong(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getLong(key); - } - - @Override - public Double getDouble(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getDouble(key); - } - - @Override - public Float getFloat(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getFloat(key); - } - - @Override - public Boolean getBoolean(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getBoolean(key); - } - - @Override - public JsonObject getJsonObject(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getJsonObject(key); - } - - @Override - public JsonArray getJsonArray(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getJsonArray(key); - } - - @Override - public byte[] getBinary(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getBinary(key); - } - - @Override - public Instant getInstant(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getInstant(key); - } - - @Override - public Object getValue(String key) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getValue(key); - } - - @Override - public String getString(String key, String def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getString(key, def); - } - - @Override - public Integer getInteger(String key, Integer def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getInteger(key, def); - } - - @Override - public Long getLong(String key, Long def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getLong(key, def); - } - - @Override - public Double getDouble(String key, Double def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getDouble(key, def); - } - - @Override - public Float getFloat(String key, Float def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getFloat(key, def); - } - - @Override - public Boolean getBoolean(String key, Boolean def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getBoolean(key, def); - } - - @Override - public JsonObject getJsonObject(String key, JsonObject def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getJsonObject(key, def); - } - - @Override - public JsonArray getJsonArray(String key, JsonArray def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getJsonArray(key, def); - } - - @Override - public byte[] getBinary(String key, byte[] def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getBinary(key, def); - } - - @Override - public Instant getInstant(String key, Instant def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getInstant(key, def); - } - - @Override - public Object getValue(String key, Object def) { - checkObjectMaterialised(); - checkFieldMaterialised(key); - return super.getValue(key, def); - } - - @Override - public boolean containsKey(String key) { - checkObjectMaterialised(); - return super.containsKey(key) || fieldSuppliers.containsKey(key); - } - - @Override - public Set fieldNames() { - checkObjectMaterialised(); - Set names = new HashSet<>(super.fieldNames()); - names.addAll(fieldSuppliers.keySet()); - return names; - } - - // Warning: This may return an instance of Supplier - @Override - public Object remove(String key) { - checkObjectMaterialised(); - return ObjectUtils.firstNonNull(fieldSuppliers.remove(key), super.remove(key)); - } - - // Note: if other is a LazyJsonObject, mergeIn will replace Suppliers rather than recursively merging them - @Override - public JsonObject mergeIn(JsonObject other, int depth) { - if (depth < 1) { - return this; - } - - checkObjectMaterialised(); - - if (other instanceof LazyJsonObject) { - LazyJsonObject ljo = (LazyJsonObject) other; - for (String f : other.fieldNames()) { - super.remove(f); - fieldSuppliers.remove(f); - } - fieldSuppliers.putAll(ljo.fieldSuppliers); - } - - return super.mergeIn(other, depth); - } - - @Override - public String encode() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.encode(); - } - - @Override - public String encodePrettily() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.encodePrettily(); - } - - @Override - public Buffer toBuffer() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.toBuffer(); - } - - @Override - public JsonObject copy() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.copy(); - } - - @Override - public Map getMap() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.getMap(); - } - - @Override - public Stream> stream() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.stream(); - } - - @Override - public Iterator> iterator() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.iterator(); - } - - @Override - public int size() { - checkObjectMaterialised(); - return super.size() + fieldSuppliers.size(); - } - - @Override - public boolean isEmpty() { - checkObjectMaterialised(); - return super.isEmpty() && fieldSuppliers.isEmpty(); - } - - @Override - public String toString() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.toString(); - } - - @Override - public boolean equals(Object o) { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.equals(o); - } - - @Override - public int hashCode() { - checkObjectMaterialised(); - return super.hashCode() + fieldSuppliers.hashCode(); - } - - @Override - public void writeToBuffer(Buffer buffer) { - checkObjectMaterialised(); - checkFieldsMaterialised(); - super.writeToBuffer(buffer); - } - - @Override - public void forEach(Consumer> action) { - checkObjectMaterialised(); - checkFieldsMaterialised(); - super.forEach(action); - } - - @Override - public Spliterator> spliterator() { - checkObjectMaterialised(); - checkFieldsMaterialised(); - return super.spliterator(); - } - - public void put(String key, Supplier supplier) { - fieldSuppliers.put(key, supplier); - } - - public LazyJsonObject lazyPut(String key, Supplier supplier) { - fieldSuppliers.put(key, supplier); - return this; - } - - private void checkObjectMaterialised() { - if (!materialised && jsonObjectSupplier != null) { - materialised = true; - mergeIn(jsonObjectSupplier.get()); - } - } - - private void checkFieldsMaterialised() { - fieldSuppliers.forEach((key, supplier) -> super.put(key, supplier.get())); - fieldSuppliers.clear(); - } - - private void checkFieldMaterialised(String key) { - Supplier supplier = fieldSuppliers.remove(key); - if (supplier != null) { - super.put(key, supplier.get()); - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/LazyJsonObject.kt b/src/main/java/com/zepben/vertxutils/json/LazyJsonObject.kt new file mode 100644 index 0000000..5f13b52 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/LazyJsonObject.kt @@ -0,0 +1,321 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import io.vertx.core.buffer.Buffer +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import java.time.Instant +import java.util.* +import java.util.function.Consumer +import java.util.stream.Stream + +class LazyJsonObject( + private val jsonObjectSupplier: (() -> JsonObject)? = null, +) : JsonObject() { + + private var materialised = false + private val fieldSuppliers = mutableMapOf Any?>() + + override fun mapTo(type: Class): T? { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.mapTo(type) + } + + override fun getString(key: String): String? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getString(key) + } + + override fun getInteger(key: String): Int? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getInteger(key) + } + + override fun getLong(key: String): Long? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getLong(key) + } + + override fun getDouble(key: String): Double? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getDouble(key) + } + + override fun getFloat(key: String): Float? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getFloat(key) + } + + override fun getBoolean(key: String): Boolean? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getBoolean(key) + } + + override fun getJsonObject(key: String): JsonObject? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getJsonObject(key) + } + + override fun getJsonArray(key: String): JsonArray? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getJsonArray(key) + } + + override fun getBinary(key: String): ByteArray? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getBinary(key) + } + + override fun getInstant(key: String): Instant? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getInstant(key) + } + + override fun getValue(key: String): Any? { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getValue(key) + } + + override fun getString(key: String, def: String): String { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getString(key, def) + } + + override fun getInteger(key: String, def: Int): Int { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getInteger(key, def) + } + + override fun getLong(key: String, def: Long): Long { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getLong(key, def) + } + + override fun getDouble(key: String, def: Double): Double { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getDouble(key, def) + } + + override fun getFloat(key: String, def: Float): Float { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getFloat(key, def) + } + + override fun getBoolean(key: String, def: Boolean): Boolean { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getBoolean(key, def) + } + + override fun getJsonObject(key: String, def: JsonObject): JsonObject { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getJsonObject(key, def) + } + + override fun getJsonArray(key: String, def: JsonArray): JsonArray { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getJsonArray(key, def) + } + + override fun getBinary(key: String, def: ByteArray): ByteArray { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getBinary(key, def) + } + + override fun getInstant(key: String, def: Instant): Instant { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getInstant(key, def) + } + + override fun getValue(key: String, def: Any): Any { + checkObjectMaterialised() + checkFieldMaterialised(key) + return super.getValue(key, def) + } + + override fun containsKey(key: String): Boolean { + checkObjectMaterialised() + return super.containsKey(key) || fieldSuppliers.containsKey(key) + } + + override fun fieldNames(): Set { + checkObjectMaterialised() + return super.fieldNames() + fieldSuppliers.keys + } + + // Warning: This may return an instance of Supplier + override fun remove(key: String): Any? { + checkObjectMaterialised() + return sequenceOf(fieldSuppliers.remove(key), super.remove(key)).filterNotNull().firstOrNull() + } + + // Note: if other is a LazyJsonObject, mergeIn will replace Suppliers rather than recursively merging them + override fun mergeIn(other: JsonObject, depth: Int): JsonObject { + if (depth < 1) { + return this + } + + checkObjectMaterialised() + + if (other is LazyJsonObject) { + val ljo = other + other.fieldNames().forEach { f -> + super.remove(f) + fieldSuppliers.remove(f) + } + fieldSuppliers.putAll(ljo.fieldSuppliers) + } + + return super.mergeIn(other, depth) + } + + override fun encode(): String { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.encode() + } + + override fun encodePrettily(): String { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.encodePrettily() + } + + override fun toBuffer(): Buffer { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.toBuffer() + } + + override fun copy(): JsonObject { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.copy() + } + + override fun getMap(): MutableMap { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.getMap() + } + + override fun stream(): Stream> { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.stream() + } + + override fun iterator(): MutableIterator> { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.iterator() + } + + override fun size(): Int { + checkObjectMaterialised() + return super.size() + fieldSuppliers.size + } + + override fun isEmpty(): Boolean { + checkObjectMaterialised() + return super.isEmpty() && fieldSuppliers.isEmpty() + } + + override fun toString(): String { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.toString() + } + + override fun equals(o: Any?): Boolean { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.equals(o) + } + + override fun hashCode(): Int { + checkObjectMaterialised() + return super.hashCode() + fieldSuppliers.hashCode() + } + + override fun writeToBuffer(buffer: Buffer) { + checkObjectMaterialised() + checkFieldsMaterialised() + super.writeToBuffer(buffer) + } + + override fun forEach(action: Consumer>) { + checkObjectMaterialised() + checkFieldsMaterialised() + super.forEach(action) + } + + fun forEach(action: (MutableMap.MutableEntry) -> Unit) { + checkObjectMaterialised() + checkFieldsMaterialised() + super.forEach(action) + } + + override fun spliterator(): Spliterator> { + checkObjectMaterialised() + checkFieldsMaterialised() + return super.spliterator() + } + + operator fun set(key: String, supplier: () -> Any?) { + fieldSuppliers[key] = supplier + } + + fun put(key: String, supplier: () -> Any?) { + fieldSuppliers[key] = supplier + } + + fun lazyPut(key: String, supplier: () -> Any?): LazyJsonObject { + fieldSuppliers[key] = supplier + return this + } + + private fun checkObjectMaterialised() { + if (!materialised && (jsonObjectSupplier != null)) { + materialised = true + mergeIn(jsonObjectSupplier()) + } + } + + private fun checkFieldsMaterialised() { + fieldSuppliers.forEach { (key, supplier) -> super.put(key, supplier()) } + fieldSuppliers.clear() + } + + private fun checkFieldMaterialised(key: String) { + fieldSuppliers.remove(key)?.also { supplier -> super.put(key, supplier()) } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/OptionalValueExtractor.java b/src/main/java/com/zepben/vertxutils/json/OptionalValueExtractor.java deleted file mode 100644 index 52c1b80..0000000 --- a/src/main/java/com/zepben/vertxutils/json/OptionalValueExtractor.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonObject; - -import java.util.Optional; - -@FunctionalInterface -@EverythingIsNonnullByDefault -public interface OptionalValueExtractor { - Optional extract(JsonObject json, String key) throws Exception; -} diff --git a/src/main/java/com/zepben/vertxutils/json/ValueExtractor.java b/src/main/java/com/zepben/vertxutils/json/ValueExtractor.java deleted file mode 100644 index c214e76..0000000 --- a/src/main/java/com/zepben/vertxutils/json/ValueExtractor.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonObject; - -import javax.annotation.Nullable; - -@FunctionalInterface -@EverythingIsNonnullByDefault -public interface ValueExtractor { - @Nullable - T extract(JsonObject json, String key) throws Exception; -} - diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterException.java b/src/main/java/com/zepben/vertxutils/json/filter/FilterException.java deleted file mode 100644 index 1825923..0000000 --- a/src/main/java/com/zepben/vertxutils/json/filter/FilterException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter; - -import com.zepben.vertxutils.json.filter.parser.Token; -import org.apache.commons.lang3.StringUtils; - -public class FilterException extends Exception { - - public FilterException(String specification, int from, Token... expected) { - super(String.format("Error parsing [%s]. After [%s] expected one of [%s] but found [%s]", - specification, specification.substring(0, from), StringUtils.join(expected, ","), specification.substring(from))); - } - - FilterException(String message) { - super(message); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterException.kt b/src/main/java/com/zepben/vertxutils/json/filter/FilterException.kt new file mode 100644 index 0000000..4c476a6 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/filter/FilterException.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter + +import com.zepben.vertxutils.json.filter.parser.Token + +class FilterException(specification: String, from: Int, vararg expected: Token) : Exception( + "Error parsing [$specification]. After [${specification.substring(0, from)}] expected one of [${ + expected.joinToString(",") + }] but found [${specification.substring(from)}]", +) diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.java b/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.java deleted file mode 100644 index 2c6a993..0000000 --- a/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.json.filter.parser.Node; -import com.zepben.vertxutils.json.filter.parser.Parser; - -import java.util.Optional; - -/** - * FilterSpecification - *

- * Specifies json fields to filter. Can be used to specify fields to exclude or to specify fields to include (i.e., exclude everything but). - *

- * For example: a.b.c or a.b.c,x.y.x or a(b,c(d)) or -a.b.c or -a.b.c,-x.y.x or -a(b,c(d)) - *

- */ -@EverythingIsNonnullByDefault -@SuppressWarnings({"WeakerAccess"}) -public class FilterSpecification { - - private Node root; - - public FilterSpecification() { - root = new Node(); - } - - public FilterSpecification(String filter) throws FilterException { - root = Parser.parse(filter); - } - - private FilterSpecification(Node root) { - this.root = root; - } - - public Node getRoot() { - return root; - } - - public void setRoot(Node root) { - this.root = root; - } - - public String getFilter() { - return root.toString(); - } - - @Override - public String toString() { - return root.toString(); - } - - public Optional getSubfilter(String location) { - Node descendant = root.getDescendent(location); - if (descendant == null) - return Optional.empty(); - else - return Optional.of(new FilterSpecification(descendant)); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.kt b/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.kt new file mode 100644 index 0000000..3eb2cb3 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter + +import com.zepben.vertxutils.json.filter.parser.Node +import com.zepben.vertxutils.json.filter.parser.Parser.parse + +/** + * **FilterSpecification** + * + * Specifies json fields to filter. Can be used to specify fields to exclude or to specify fields to include (i.e., exclude everything but). + * + * For example: a.b.c or a.b.c,x.y.x or a(b,c(d)) or -a.b.c or -a.b.c,-x.y.x or -a(b,c(d)) + */ +class FilterSpecification( + val root: Node = Node(), +) { + + constructor(filter: String) : this(root = parse(filter)) + + val filter: String + get() = root.toString() + + override fun toString(): String = root.toString() + + fun getSubfilter(location: String): FilterSpecification? = + root.getDescendent(location)?.let { FilterSpecification(it) } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.java b/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.java deleted file mode 100644 index 646d516..0000000 --- a/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.json.filter.parser.Node; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -@EverythingIsNonnullByDefault -@SuppressWarnings("WeakerAccess") -public class JsonObjectFilter implements JsonFilter { - - static public JsonObject applyFilter(JsonObject object, FilterSpecification fs) { - return new JsonObjectFilter().apply(object, fs); - } - - /* - * Applies a filter to a JsonObject. Note that this mutates the given object and returns it. - */ - @Override - public JsonObject apply(JsonObject object, FilterSpecification fs) { - apply(fs.getRoot(), object); - return object; - } - - private void apply(Node node, Object object) { - switch (node.filterType()) { - case INCLUDE: - includeSpecified(node, object); - break; - case EXCLUDE: - excludeSpecified(node, object); - break; - case PASSTHROUGH: - // Do nothing - break; - } - } - - private void excludeSpecified(Node node, Object object) { - if (object instanceof JsonArray) { - for (Object value : ((JsonArray) object)) { - apply(node, value); - } - } else if (object instanceof JsonObject) { - JsonObject jsonObject = (JsonObject) object; - for (Node child : node.children()) { - if (child.children().isEmpty()) { - jsonObject.remove(child.content()); - } else { - apply(child, jsonObject.getValue(child.content())); - } - } - } - } - - private void includeSpecified(Node node, Object object) { - if (node.children().isEmpty()) { - return; // Nothing to do - } - if (object instanceof JsonArray) { - for (Object value : ((JsonArray) object)) { - apply(node, value); - } - } else if (object instanceof JsonObject) { - JsonObject jsonObject = (JsonObject) object; - - Set fieldsToInclude = node - .children() - .stream() - .map(Node::content) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - Set fieldsToRemove = new HashSet<>(jsonObject.fieldNames()); - fieldsToRemove.removeAll(fieldsToInclude); - - for (String fieldName : fieldsToRemove) { - jsonObject.remove(fieldName); - } - - for (Node child : node.children()) { - String fieldName = child.content(); - Object value = jsonObject.getValue(fieldName); - if (value != null) { - apply(child, value); - } - } - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.kt b/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.kt new file mode 100644 index 0000000..12a7d53 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter + +import com.zepben.vertxutils.json.filter.parser.FilterType +import com.zepben.vertxutils.json.filter.parser.Node +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject + +class JsonObjectFilter { + + /** + * Applies a filter to a JsonObject. Note that this mutates the given object and returns it. + */ + fun apply(json: JsonObject, fs: FilterSpecification): JsonObject { + apply(fs.root, json) + return json + } + + private fun apply(node: Node, obj: Any) { + when (node.filterType) { + FilterType.INCLUDE -> includeSpecified(node, obj) + FilterType.EXCLUDE -> excludeSpecified(node, obj) + FilterType.PASSTHROUGH -> {} + } + } + + private fun includeSpecified(node: Node, obj: Any) { + if (node.getChildren().isEmpty()) + return // Nothing to do + + when (obj) { + is JsonArray -> obj.forEach { value -> apply(node, value) } + + is JsonObject -> { + val fieldsToInclude = node + .getChildren() + .asSequence() + .mapNotNull(Node::content) + .toSet() + + val fieldsToRemove = obj.fieldNames() - fieldsToInclude + fieldsToRemove.forEach { obj.remove(it) } + + node.getChildren().forEach { child -> + obj.getValue(child.content)?.also { + apply(child, it) + } + } + } + } + } + + private fun excludeSpecified(node: Node, obj: Any) { + when (obj) { + is JsonArray -> obj.forEach { value -> apply(node, value) } + + is JsonObject -> { + node.getChildren().forEach { child -> + if (child.getChildren().isEmpty()) { + obj.remove(child.content) + } else { + apply(child, obj.getValue(child.content)) + } + } + } + } + } + + companion object { + + fun applyFilter(json: JsonObject, fs: FilterSpecification): JsonObject = + JsonObjectFilter().apply(json, fs) + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.kt similarity index 57% rename from src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.java rename to src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.kt index 44f9433..03e0721 100644 --- a/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.java +++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.kt @@ -1,15 +1,18 @@ /* - * Copyright 2020 Zeppelin Bend Pty Ltd + * Copyright 2026 Zeppelin Bend Pty Ltd * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +package com.zepben.vertxutils.json.filter.parser -package com.zepben.vertxutils.json.filter.parser; +enum class FilterType( + val denotedBy: String = "", +) { -public enum FilterType { INCLUDE, - EXCLUDE, + EXCLUDE(denotedBy = "-"), PASSTHROUGH + } diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.java deleted file mode 100644 index 75729d2..0000000 --- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter.parser; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.json.filter.FilterException; - -import javax.annotation.Nullable; -import java.util.regex.Matcher; - -@EverythingIsNonnullByDefault -class Lexer { - - // The string to be tokenised - private final String specification; - - // Where in the string the tokenising is up to - private int currentPosition = 0; - - // If a token has matched, this is the content of the token - @Nullable - private String currentContent; - - // If a token has matched, this is the token, otherwise NONE - private Token currentToken = Token.NONE; - - Lexer(String specification) { - this.specification = specification; - } - - void nextToken(Token... lookingFor) throws FilterException { - - skipWhitespace(); - - for (Token t : lookingFor) { - Matcher m = t.compiledPattern - .matcher(specification) - .region(currentPosition, specification.length()); - if (m.find()) { - currentContent = specification.substring(currentPosition, m.end()); - currentPosition = m.end(); - currentToken = t; - return; - } - } - throw new FilterException(specification, currentPosition, lookingFor); - } - - @Nullable - String currentContent() { - return currentContent; - } - - @Nullable - Token currentToken() { - return currentToken; - } - - private void skipWhitespace() { - while (currentPosition < specification.length() - && Character.isWhitespace(specification.charAt(currentPosition))) { - currentPosition++; - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.kt new file mode 100644 index 0000000..90cf728 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter.parser + +import com.zepben.vertxutils.json.filter.FilterException + +/** + * @param specification The string to be tokenised. + */ +internal class Lexer( + private val specification: String, +) { + + /** + * If a token has matched, this is the content of the token. + */ + var currentContent: String? = null + private set + + /** + * If a token has matched, this is the token, otherwise NONE + */ + var currentToken = Token.NONE + private set + + /** + * Where in the string the tokenising is up to + */ + private var currentPosition = 0 + + @Throws(FilterException::class) + fun nextToken(vararg lookingFor: Token) { + skipWhitespace() + + lookingFor.forEach { t -> + val m = t.compiledPattern + .matcher(specification) + .region(currentPosition, specification.length) + if (m.find()) { + currentContent = specification.substring(currentPosition, m.end()) + currentPosition = m.end() + currentToken = t + return + } + } + throw FilterException(specification, currentPosition, *lookingFor) + } + + private fun skipWhitespace() { + while ((currentPosition < specification.length) && Character.isWhitespace(specification[currentPosition])) + currentPosition++ + } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.java deleted file mode 100644 index ca0f3ce..0000000 --- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter.parser; - -import org.apache.commons.lang3.StringUtils; - -import java.util.Collection; -import java.util.TreeMap; - -import static com.zepben.vertxutils.json.filter.parser.FilterType.EXCLUDE; -import static com.zepben.vertxutils.json.filter.parser.FilterType.PASSTHROUGH; - -@SuppressWarnings({"WeakerAccess"}) -public class Node { - - // The children of this Node (might be empty) - private final TreeMap children = new TreeMap<>(); - - // The string content of this node if there is any - private String content; - - // Is this an exclude or include node? - private FilterType filterType; - - public Node() { - filterType = PASSTHROUGH; - } - - public Node(String content) { - this.content = content; - } - - public String content() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public FilterType filterType() { - return filterType; - } - - public void setFilterType(FilterType filterType) { - this.filterType = filterType; - } - - public Collection children() { - return children.values(); - } - - public Node getChild(String name) { - return children.get(name); - } - - public Node getDescendent(String name) { - Node descendant = this; - for (String n : name.split("\\.")) { - if (descendant == null) { - return null; - } - descendant = descendant.getChild(n); - } - return descendant; - } - - public Node addOrGetChild(String content) { - Node child = children.get(content); - if (child == null) { - child = new Node(content); - child.setFilterType(filterType); - children.put(content, child); - } - return child; - } - - public int countAllNodes() { - int n = 1; - for (Node child : children.values()) { - n += child.countAllNodes(); - } - return n; - } - - @Override - public String toString() { - String childrenString = StringUtils.join(children.values(), ",").replaceAll("-", ""); - if (content == null) { - return (filterType == EXCLUDE ? "-" : "") + childrenString; - } - if (children.isEmpty()) { - return content; - } else { - return String.format(children.size() == 1 ? "%s.%s" : "%s(%s)", content, childrenString); - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.kt new file mode 100644 index 0000000..ee1a6a3 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter.parser + +import java.util.* + +/** + * A Node in the filter tree. + * + * @property content The string content of this node if there is any. + * @property filterType Is this an exclude or include node? + */ +data class Node( + val content: String? = null, + val filterType: FilterType = FilterType.PASSTHROUGH, +) { + + // The children of this Node (might be empty) + private val children = TreeMap() + + fun getChildren(): Collection = children.values + + fun getChild(name: String): Node? = children[name] + + fun getDescendent(name: String): Node? { + var descendant: Node? = this + name.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().forEach { n -> + descendant ?: return null + descendant = descendant.getChild(n) + } + return descendant + } + + fun addOrGetChild(content: String): Node = + children.getOrPut(content) { Node(content, filterType) } + + fun countAllNodes(): Int = + children.values.sumOf { it.countAllNodes() } + 1 + + override fun toString(): String { + val childrenString = children.values.joinToString(separator = ",") { it.toString().replace("-", "") } + return when { + content == null -> "${filterType.denotedBy}$childrenString" + children.isEmpty() -> content + children.size == 1 -> "$content.$childrenString" + else -> "$content($childrenString)" + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.java deleted file mode 100644 index b5a2779..0000000 --- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter.parser; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.json.filter.FilterException; - -import static com.zepben.vertxutils.json.filter.parser.FilterType.EXCLUDE; -import static com.zepben.vertxutils.json.filter.parser.FilterType.INCLUDE; -import static com.zepben.vertxutils.json.filter.parser.Token.*; - -@EverythingIsNonnullByDefault -public class Parser { - - public static Node parse(String specification) - throws FilterException { - Node root = new Node(); - Lexer lexer = new Lexer(specification); - - lexer.nextToken(IDENTIFIER, DASH); - if (lexer.currentToken() == DASH) { - root.setFilterType(EXCLUDE); - lexer.nextToken(IDENTIFIER); - } else { - root.setFilterType(INCLUDE); - } - - parseNode(root, lexer, END); - - return root; - } - - private static void parseNode(Node node, Lexer lexer, Token endingToken) throws FilterException { - Node root = node; - while (lexer.currentToken() != endingToken) { - node = node.addOrGetChild(lexer.currentContent()); - lexer.nextToken(OPEN, COMMA, DOT, endingToken); - if (lexer.currentToken() == DOT) { - lexer.nextToken(IDENTIFIER); - } - if (lexer.currentToken() == OPEN) { - lexer.nextToken(IDENTIFIER); - parseNode(node, lexer, CLOSE); - lexer.nextToken(COMMA, endingToken); - } - if (lexer.currentToken() == COMMA) { - lexer.nextToken(IDENTIFIER); - parseNode(root, lexer, endingToken); - } - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.kt new file mode 100644 index 0000000..475a986 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter.parser + +import com.zepben.vertxutils.json.filter.FilterException + +object Parser { + + @Throws(FilterException::class) + fun parse(specification: String): Node { + val lexer = Lexer(specification) + + lexer.nextToken(Token.IDENTIFIER, Token.DASH) + val root = Node( + filterType = if (lexer.currentToken == Token.DASH) { + lexer.nextToken(Token.IDENTIFIER) + FilterType.EXCLUDE + } else { + FilterType.INCLUDE + }, + ) + + parseNode(root, lexer, Token.END) + + return root + } + + @Throws(FilterException::class) + private fun parseNode(node: Node, lexer: Lexer, endingToken: Token) { + var node = node + val root = node + while (lexer.currentToken != endingToken) { + node = node.addOrGetChild(lexer.currentContent!!) + lexer.nextToken(Token.OPEN, Token.COMMA, Token.DOT, endingToken) + if (lexer.currentToken == Token.DOT) { + lexer.nextToken(Token.IDENTIFIER) + } + if (lexer.currentToken == Token.OPEN) { + lexer.nextToken(Token.IDENTIFIER) + parseNode(node, lexer, Token.CLOSE) + lexer.nextToken(Token.COMMA, endingToken) + } + if (lexer.currentToken == Token.COMMA) { + lexer.nextToken(Token.IDENTIFIER) + parseNode(root, lexer, endingToken) + } + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.java deleted file mode 100644 index c4bfda9..0000000 --- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter.parser; - -import java.util.regex.Pattern; - -// The tokens that the filter specification can be tokenised into -public enum Token { - IDENTIFIER("[a-zA-Z][a-zA-Z0-9]*"), - OPEN("\\("), - CLOSE("\\)"), - COMMA(","), - DOT("\\."), - END("$"), - DASH("-"), - NONE(""); - - public final String pattern; - public final Pattern compiledPattern; - - Token(String pattern) { - this.pattern = pattern; - compiledPattern = Pattern.compile("^" + pattern); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.kt new file mode 100644 index 0000000..966c58d --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter.parser + +import java.util.regex.Pattern + +/** + * The tokens that the filter specification can be tokenised into. + */ +enum class Token(pattern: String) { + + IDENTIFIER("[a-zA-Z][a-zA-Z0-9]*"), + OPEN("\\("), + CLOSE("\\)"), + COMMA(","), + DOT("\\."), + END("$"), + DASH("-"), + NONE(""); + + val compiledPattern: Pattern = Pattern.compile("^$pattern") + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponse.java b/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponse.java deleted file mode 100644 index 136f7c5..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.ChunkedResponse; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class CaptureChunkedJsonResponse extends ChunkedJsonResponse { - - private String captured = ""; - - public CaptureChunkedJsonResponse() { - this(DEFAULT_BUFFER_SIZE); - } - - public CaptureChunkedJsonResponse(int bufferSize) { - super(bufferSize); - } - - @Override - protected void end(StringBuilder sb) { - captured = sb.toString(); - } - - @Override - protected void send(boolean force, StringBuilder sb) { - captured = sb.toString(); - } - - @Override - public String toString() { - return captured; - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponse.java b/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponse.java deleted file mode 100644 index 1809faa..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponse.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.ChunkedResponse; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -import java.util.ArrayDeque; -import java.util.Deque; - -/** - * Fluent Helper to send a JSON response in chunks. - */ -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public abstract class ChunkedJsonResponse { - - public final static int DEFAULT_BUFFER_SIZE = 1 << 21; - - public class JsonObject { - - private JsonObject() { - } - - public JsonArray beginArray(String key) { - if (isNotObject()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open object to begin an array."); - - return doAddKey(key).doBeginArray(); - } - - public JsonObject beginObject(String key) { - if (isNotObject()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open object to begin another object."); - - return doAddKey(key).doBeginObject(); - } - - public JsonObject addJson(String key, String json) { - if (isNotObject()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open object to add json."); - - return doAddKey(key).doAddJson(json); - } - - public JsonObject endObject() { - if (isNotObject()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open object to end."); - - doEndObject(JsonObjectType.OBJECT); - return object; - } - - public JsonArray endObjectInArray() { - if (isNotObject()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open object to end."); - - doEndObject(JsonObjectType.ARRAY); - return array; - } - - @SuppressWarnings("UnusedReturnValue") - public JsonObject send(boolean force) { - ChunkedJsonResponse.this.send(force, sb); - return this; - } - - private JsonArray doBeginArray() { - openItems.push(JsonObjectType.ARRAY); - isFirst = true; - - sb.append("["); - - return array; - } - - private JsonObject doBeginObject() { - openItems.push(JsonObjectType.OBJECT); - isFirst = true; - - sb.append("{"); - - return object; - } - - private JsonObject doAddKey(String key) { - if (isFirst) - isFirst = false; - else - sb.append(","); - - sb.append("\"").append(key).append("\":"); - - return this; - } - - private JsonObject doAddJson(String json) { - sb.append(json); - send(false); - - return this; - } - - private void doEndObject(JsonObjectType expectedParentType) { - openItems.pop(); - if (!openItems.isEmpty() && (openItems.peek() != expectedParentType)) { - openItems.push(JsonObjectType.OBJECT); - throw new IllegalStateException("INTERNAL ERROR: Incorrect end object method called, the parent is not of the expected type."); - } - - sb.append("}"); - isFirst = false; - - if (openItems.isEmpty()) - end(sb); - else - send(false); - } - - } - - public class JsonArray { - - private JsonArray() { - } - - public JsonArray addArrayItem(String json) { - if (isNotArray()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open array to add json."); - - if (isFirst) - isFirst = false; - else - sb.append(","); - - sb.append(json); - send(false); - - return this; - } - - public JsonObject beginObject() { - if (isNotArray()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open array to begin an object."); - - openItems.push(JsonObjectType.OBJECT); - - if (!isFirst) - sb.append(","); - - isFirst = true; - sb.append("{"); - - return object; - } - - public JsonArray beginArray() { - if (isNotArray()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open array to begin an array."); - - openItems.push(JsonObjectType.ARRAY); - - if (!isFirst) - sb.append(","); - - isFirst = true; - sb.append("["); - - return array; - } - - public JsonObject endArray() { - if (isNotArray()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open array to end."); - - doEndArray(JsonObjectType.OBJECT); - return object; - } - - public JsonArray endArrayInArray() { - if (isNotArray()) - throw new IllegalStateException("INTERNAL ERROR: You must have an open array to end."); - - doEndArray(JsonObjectType.ARRAY); - return array; - } - - @SuppressWarnings("UnusedReturnValue") - public JsonArray send(boolean force) { - ChunkedJsonResponse.this.send(force, sb); - return this; - } - - private void doEndArray(JsonObjectType expectedParentType) { - openItems.pop(); - if (!openItems.isEmpty() && (openItems.peek() != expectedParentType)) { - openItems.push(JsonObjectType.ARRAY); - throw new IllegalStateException("INTERNAL ERROR: Incorrect end array method called, the parent is not of the expected type."); - } - - sb.append("]"); - isFirst = false; - - if (openItems.isEmpty()) - end(sb); - else - send(false); - } - - } - - private enum JsonObjectType {OBJECT, ARRAY} - - - private final StringBuilder sb; - private final JsonObject object = new JsonObject(); - private final JsonArray array = new JsonArray(); - - boolean isFirst = true; - private final Deque openItems = new ArrayDeque<>(); - - public ChunkedJsonResponse(int bufferSize) { - sb = new StringBuilder(bufferSize); - } - - public JsonArray ofArray() { - if (!openItems.isEmpty()) - throw new IllegalStateException("INTERNAL ERROR: You can only start one object or array for a response."); - return object.doBeginArray(); - } - - public JsonObject ofObject() { - if (!openItems.isEmpty()) - throw new IllegalStateException("INTERNAL ERROR: You can only start one object or array for a response."); - return object.doBeginObject(); - } - - private boolean isNotArray() { - return openItems.peek() != JsonObjectType.ARRAY; - } - - private boolean isNotObject() { - return openItems.peek() != JsonObjectType.OBJECT; - } - - protected abstract void end(StringBuilder sb); - - protected abstract void send(boolean force, StringBuilder sb); - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponse.java b/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponse.java deleted file mode 100644 index a833e92..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.ChunkedResponse; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.http.HttpServerResponse; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class HttpChunkedJsonResponse extends ChunkedJsonResponse { - - private final HttpServerResponse response; - private final int bufferSize; - - public HttpChunkedJsonResponse(HttpServerResponse response) { - this(response, DEFAULT_BUFFER_SIZE); - } - - public HttpChunkedJsonResponse(HttpServerResponse response, int bufferSize) { - super(bufferSize); - - this.response = response; - this.bufferSize = bufferSize; - } - - @Override - protected void end(StringBuilder sb) { - if (!response.closed()){ - response.end(sb.toString()); - } - } - - @Override - protected void send(boolean force, StringBuilder sb) { - if ((force || sb.length() >= bufferSize) && !response.closed()) { - response.write(sb.toString()); - sb.setLength(0); - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.java b/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.java deleted file mode 100644 index 6617e47..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonObject; - -import java.util.Collections; -import java.util.List; - -/** - * A utility class to help with formatting error messages so they can be consistent across routes. - */ -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class ErrorFormatter { - - /** - * Helper method for {@link ErrorFormatter#asJson(List)} that can be called with a single error string. - * - * @param error The error message - * @return The JSON string - */ - public static String asJson(String error) { - return asJson(Collections.singletonList(error)); - } - - /** - * Method takes a list of strings and puts in in a JSON object. - * This allows route handlers to return errors in a consistent fashion. - * The JSON object is constructed as follows: {@code {"errors": ["msg1", "msg2", ...]}} - * - * @param errors The errors to be included - * @return The string representation of the JSON object. - */ - public static String asJson(List errors) { - return new JsonObject().put("errors", errors).encode(); - } - - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.kt b/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.kt new file mode 100644 index 0000000..7edccf2 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import io.vertx.core.json.JsonObject + +/** + * A utility class to help with formatting error messages so they can be consistent across routes. + */ +object ErrorFormatter { + + /** + * Helper method for [ErrorFormatter.asJson] that can be called with a single error string. + * + * @param error The error message + * @return The JSON string + */ + fun asJson(error: String?): String = asJson(listOf(error)) + + /** + * Method takes a list of strings and puts it in a JSON object. + * This allows route handlers to return errors in a consistent fashion. + * The JSON object is constructed as follows: `{"errors": ["msg1", "msg2", ...]}` + * + * @param errors The errors to be included + * @return The string representation of the JSON object. + */ + fun asJson(errors: List): String = JsonObject().put("errors", errors).encode() + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.java b/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.java deleted file mode 100644 index 81377a3..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; - -import java.util.function.BiConsumer; - - -public class ExceptionHandler implements Handler { - - private final Class tClass; - private final BiConsumer handler; - - ExceptionHandler(Class tClass, BiConsumer handler) { - this.tClass = tClass; - this.handler = handler; - } - - @Override - public void handle(RoutingContext context) { - if (!tClass.isInstance(context.failure())) { - context.next(); - return; - } - - handle(tClass.cast(context.failure()), context); - } - - private void handle(T throwable, RoutingContext handler) { - this.handler.accept(tClass.cast(throwable), handler); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.kt b/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.kt new file mode 100644 index 0000000..2b2bd7a --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import io.vertx.core.Handler +import io.vertx.ext.web.RoutingContext + +class ExceptionHandler internal constructor( + private val tClass: Class, + private val handler: (T, RoutingContext) -> Unit, +) : Handler { + + override fun handle(context: RoutingContext) { + if (!tClass.isInstance(context.failure())) { + context.next() + return + } + + handler(tClass.cast(context.failure()), context) + } + + companion object { + + internal inline fun of(noinline handler: (T, RoutingContext) -> Unit): ExceptionHandler = + ExceptionHandler(T::class.java, handler) + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.java b/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.java deleted file mode 100644 index 08a8931..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; - -@EverythingIsNonnullByDefault -public interface JsonBodyRequest { - - default T extract(JsonObject json, String key, GetValue valueSupplier) throws IllegalArgumentException { - try { - @Nullable T value = valueSupplier.get(json, key); - if (value == null) - throw new IllegalArgumentException(String.format("Required key '%s' must be specified", key)); - return value; - } catch (ClassCastException ex) { - throw new IllegalArgumentException(String.format("Error reading required key '%s'", key), ex); - } - } - - default List extractList(JsonObject json, String key, int minValues, ValueConverter valueConverter) throws IllegalArgumentException { - try { - @Nullable JsonArray values = json.getJsonArray(key); - if (values == null) - throw new IllegalArgumentException(String.format("Required key '%s' must be specified", key)); - - List result = new ArrayList<>(); - for (int i = 0; i < values.size(); ++i) - result.add(valueConverter.convert(values.getJsonObject(i))); - - if (result.size() < minValues) { - if (minValues == 1) - throw new IllegalArgumentException(String.format("Required key '%s' must have at least 1 value", key)); - else - throw new IllegalArgumentException(String.format("Required key '%s' must have at least %d values", key, minValues)); - } - - return result; - } catch (ClassCastException ex) { - throw new IllegalArgumentException(String.format("Error reading required key '%s'", key), ex); - } - } - - @FunctionalInterface - interface GetValue { - @Nullable - T get(JsonObject json, String key); - } - - @FunctionalInterface - interface ValueConverter { - @Nullable - T convert(JsonObject json); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.kt b/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.kt new file mode 100644 index 0000000..ae0e47a --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import io.vertx.core.json.JsonObject + +interface JsonBodyRequest { + + @Throws(IllegalArgumentException::class) + fun extract(json: JsonObject, key: String, valueSupplier: (json: JsonObject, key: String) -> T?): T = + try { + val value = valueSupplier(json, key) + requireNotNull(value) { "Required key '$key' must be specified" } + value + } catch (ex: ClassCastException) { + throw IllegalArgumentException("Error reading required key '$key'", ex) + } + + @Throws(IllegalArgumentException::class) + fun extractList(json: JsonObject, key: String, minValues: Int, valueConverter: (json: JsonObject) -> T): List { + try { + val values = json.getJsonArray(key) + requireNotNull(values) { "Required key '$key' must be specified" } + + val converted = (0.. + valueConverter(values.getJsonObject(i)) + } + + if (converted.size < minValues) { + require(minValues != 1) { "Required key '$key' must have at least 1 value" } + throw IllegalArgumentException("Required key '$key' must have at least $minValues values") + } + + return converted + } catch (ex: ClassCastException) { + throw IllegalArgumentException("Error reading required key '$key'", ex) + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/Respond.kt b/src/main/java/com/zepben/vertxutils/routing/Respond.kt index 9e34eb9..855b601 100644 --- a/src/main/java/com/zepben/vertxutils/routing/Respond.kt +++ b/src/main/java/com/zepben/vertxutils/routing/Respond.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Zeppelin Bend Pty Ltd + * Copyright 2026 Zeppelin Bend Pty Ltd * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -9,7 +9,6 @@ package com.zepben.vertxutils.routing import com.google.common.net.HttpHeaders import com.google.common.net.MediaType -import com.zepben.annotations.EverythingIsNonnullByDefault import com.zepben.vertxutils.json.filter.FilterSpecification import com.zepben.vertxutils.json.filter.JsonObjectFilter import io.netty.handler.codec.http.HttpResponseStatus @@ -20,50 +19,38 @@ import io.vertx.ext.web.RoutingContext /** * Class that contains a bunch of helper functions for handling HTTP responses. */ -@EverythingIsNonnullByDefault object Respond { - @JvmStatic - @JvmOverloads + fun with( context: RoutingContext, status: HttpResponseStatus, - addHeaders: Map = emptyMap() + addHeaders: Map = emptyMap(), + withEmptyContentLengthHeader: Boolean = false, ) { context .response() .setStatusCode(status.code()) - .apply { if (addHeaders.isNotEmpty()) headers().addAll(addHeaders) } + .apply { + if (addHeaders.isNotEmpty()) headers().addAll(addHeaders) + if (withEmptyContentLengthHeader) headers()[HttpHeaders.CONTENT_LENGTH] = "0" + } .end() } - @JvmStatic - fun with( - context: RoutingContext, - status: HttpResponseStatus, - withEmptyContentLengthHeader: Boolean = false - ) = with( - context, - status, - if (withEmptyContentLengthHeader) mapOf(HttpHeaders.CONTENT_LENGTH to "0") else emptyMap() - ) - - @JvmStatic fun with(context: RoutingContext, response: Response) { context .response() - .setStatusCode(response.status().code()) - .setStatusMessage(response.status().reasonPhrase()) - .apply { if (response.hasHeaders()) headers().addAll(response.headers()) } - .end(response.body()) + .setStatusCode(response.status.code()) + .setStatusMessage(response.status.reasonPhrase()) + .apply { if (response.hasHeaders()) headers().addAll(response.headers) } + .end(response.body) } - @JvmStatic - @JvmOverloads fun withJson( context: RoutingContext, status: HttpResponseStatus, json: String, - addHeaders: Map = emptyMap() + addHeaders: Map = emptyMap(), ) { context.response() .setStatusCode(status.code()) @@ -73,14 +60,12 @@ object Respond { .end(json) } - @JvmStatic - @JvmOverloads fun withJson( context: RoutingContext, status: HttpResponseStatus, json: JsonObject, filterSpecification: FilterSpecification, - addHeaders: Map = emptyMap() + addHeaders: Map = emptyMap(), ) { context.response() .setStatusCode(status.code()) @@ -92,12 +77,10 @@ object Respond { // This function breaks the pattern and doesn't actually send the response, it just returns the unsent response. // This is because EWB Network Routes needs it to behave this way and does further manipulation to it. Leave as is. - @JvmStatic - @JvmOverloads fun withJsonChunked( context: RoutingContext, status: HttpResponseStatus, - addHeaders: Map = emptyMap() + addHeaders: Map = emptyMap(), ): HttpServerResponse { return context.response() .setStatusCode(status.code()) diff --git a/src/main/java/com/zepben/vertxutils/routing/Response.java b/src/main/java/com/zepben/vertxutils/routing/Response.java deleted file mode 100644 index 8995b81..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/Response.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.google.common.net.HttpHeaders; -import com.google.common.net.MediaType; -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.buffer.Buffer; - -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; - -/** - * Class to hold things for a response to an HTTP request. - */ -@SuppressWarnings({"WeakerAccess", "UnstableApiUsage"}) -@EverythingIsNonnullByDefault -public class Response { - private HttpResponseStatus status; - private Buffer body; - @Nullable private Map headers = null; - - public Response(HttpResponseStatus httpStatus) { - this(httpStatus, Buffer.buffer()); - } - - public Response(HttpResponseStatus status, Buffer body) { - this.status = status; - this.body = body; - } - - public static Response ofJson(HttpResponseStatus status, String json) { - Response response = new Response(status, Buffer.buffer(json)); - response.headers().put(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - return response; - } - - public static Response ofText(HttpResponseStatus status, String body) { - Response response = new Response(status, Buffer.buffer(body)); - response.headers().put(HttpHeaders.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()); - return response; - } - - public HttpResponseStatus status() { - return status; - } - - public Buffer body() { - return body; - } - - public Response setStatus(HttpResponseStatus status) { - this.status = status; - return this; - } - - public Response setBody(Buffer body) { - this.body = body; - return this; - } - - public boolean hasHeaders() { - return headers != null && !headers.isEmpty(); - } - - public Map headers() { - if (headers == null) - headers = new HashMap<>(); - - return headers; - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/Response.kt b/src/main/java/com/zepben/vertxutils/routing/Response.kt new file mode 100644 index 0000000..c2b7871 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/Response.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.google.common.net.HttpHeaders +import com.google.common.net.MediaType +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.buffer.Buffer + +/** + * Class to hold things for a response to an HTTP request. + */ +class Response( + val status: HttpResponseStatus, + val body: Buffer = Buffer.buffer(), + val headers: Map = emptyMap(), +) { + + fun hasHeaders(): Boolean = !headers.isEmpty() + + companion object { + + fun ofJson(status: HttpResponseStatus, json: String): Response = + Response( + status, + Buffer.buffer(json), + mapOf(HttpHeaders.CONTENT_TYPE to MediaType.JSON_UTF_8.toString()), + ) + + fun ofText(status: HttpResponseStatus, body: String): Response = + Response( + status, + Buffer.buffer(body), + mapOf(HttpHeaders.CONTENT_TYPE to MediaType.PLAIN_TEXT_UTF_8.toString()), + ) + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/Route.java b/src/main/java/com/zepben/vertxutils/routing/Route.java deleted file mode 100644 index 6fb520a..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/Route.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler; -import com.zepben.vertxutils.routing.handlers.PathParamsHandler; -import com.zepben.vertxutils.routing.handlers.QueryParamsHandler; -import com.zepben.vertxutils.routing.handlers.params.*; -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpMethod; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; - -import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.List; -import java.util.function.BiConsumer; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class Route { - - @Nullable private final String path; - private final boolean hasRegexPath; - private final ImmutableSet methods; - private final ImmutableList handlers; - private final ImmutableList> failureHandlers; - private final boolean isPublic; - - public static Builder builder() { - return new Builder(); - } - - private Route(@Nullable String path, - boolean hasRegexPath, - ImmutableSet methods, - ImmutableList handlers, - ImmutableList> failureHandlers, - boolean isPublic) { - this.path = path; - this.hasRegexPath = hasRegexPath; - this.methods = methods; - this.handlers = handlers; - this.failureHandlers = failureHandlers; - this.isPublic = isPublic; - } - - /** - * The path of the route. - * - * @return The path of the route. - */ - @Nullable - public String path() { - return path; - } - - /** - * Set to true if the path uses regular expressions. Defaults to false. - * - * @return true is the path uses regular expressions. - */ - public boolean hasRegexPath() { - return hasRegexPath; - } - - /** - * The HTTP method for the route. - * - * @return The HTTP method for the route. - */ - public Iterable methods() { - return methods; - } - - /** - * Return a list of handlers for this route. - *

- * Remember to always call {@link RoutingContext#next()} to chain to your next handler if you have more than one. - * - * @return A list of handlers for this route. - */ - public List handlers() { - return handlers; - } - - /** - * The failure handler for the route. - * - * @return The failure handler for the route. - */ - public List> failureHandlers() { - return failureHandlers; - } - - /** - * Indicates if the route should be documented as a public route. - * - * @return true if the route is a publicly documented route. - */ - public boolean isPublic() { - return isPublic; - } - - @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) - public static class Builder { - @Nullable private String path = null; - private boolean hasRegexPath = false; - private final ImmutableSet.Builder methods = ImmutableSet.builder(); - @Nullable private PathParamsHandler pathParamsHandler = null; - @Nullable private QueryParamsHandler queryParamsHandler = null; - @Nullable private BodyHandler bodyHandler = null; - @Nullable private DecodeBodyHandler decodeBodyHandler = null; - private final ImmutableList.Builder handlers = ImmutableList.builder(); - private final ImmutableList.Builder> failureHandlers = ImmutableList.builder(); - private boolean isPublic = true; - - private Builder() { - } - - public Builder path(String path) { - if (path.isEmpty()) - throw new IllegalArgumentException("path must not be empty"); - - if (path.indexOf('%') >= 0) - throw new IllegalArgumentException("formatted path must not contain a '%'"); - - this.path = path; - return this; - } - - public Builder path(String pathFormat, PathParamRule... rules) { - int count = 0; - for (int index = pathFormat.indexOf('%'); index >= 0; index = pathFormat.indexOf('%', index + 1)) { - ++count; - if ((index == 0) - || (index >= pathFormat.length() - 1) - || (pathFormat.charAt(index - 1) != ':') - || (pathFormat.charAt(index + 1) != 's')) { - throw new IllegalArgumentException("invalid use of % in path format string"); - } - } - - if (count < rules.length) - throw new IllegalArgumentException("too many path params"); - else if (count > rules.length) - throw new IllegalArgumentException("missing path params"); - - path(String.format(pathFormat, Arrays.stream(rules).map(ParamRule::name).toArray())); - pathParamsHandler = new PathParamsHandler(rules); - return this; - } - - public Builder hasRegexPath(boolean hasRegexPath) { - this.hasRegexPath = hasRegexPath; - return this; - } - - public Builder method(HttpMethod method) { - methods.add(method); - return this; - } - - public Builder methods(HttpMethod... methods) { - for (HttpMethod method : methods) - method(method); - - return this; - } - - public Builder queryParams(QueryParamRule... rules) { - queryParamsHandler = new QueryParamsHandler(rules); - return this; - } - - public Builder bodySizeLimit(long size) { - if (bodyHandler == null) - bodyHandler(BodyHandler.create()); - - bodyHandler.setBodyLimit(size); - return this; - } - - public Builder uploadsDirectory(String path) { - if (bodyHandler == null) - bodyHandler(BodyHandler.create()); - - bodyHandler.setUploadsDirectory(path); - return this; - } - - public Builder decodeBody(RequestValueConverter bodyConverter) { - return decodeBody(bodyConverter, true); - } - - public Builder decodeBody(RequestValueConverter bodyConverter, boolean bodyRequired) { - if (bodyHandler == null) - bodyHandler(BodyHandler.create()); - - decodeBodyHandler(new DecodeBodyHandler(new BodyRule<>(bodyConverter, bodyRequired))); - return this; - } - - public Builder bodyHandler(BodyHandler handler) { - bodyHandler = handler; - return this; - } - - public Builder decodeBodyHandler(DecodeBodyHandler handler) { - decodeBodyHandler = handler; - return this; - } - - public Builder addHandler(RouteHandler handler) { - handlers.add(handler); - return this; - } - - public Builder addHandler(Handler handler) { - return addHandler(new RouteHandler(handler, false)); - } - - /** - * Registers a blocking handler. - * This makes the handler equivalent to being registered with {@link io.vertx.ext.web.Route#blockingHandler(Handler, boolean)}. - * on the {@link RouteRegister} however the boolean ordered flag is set by the argument given to the route register. - * - * @param blockingHandler The handler that contains blocking code. - * @return This builder. - */ - public final Builder addBlockingHandler(Handler blockingHandler) { - return addHandler(new RouteHandler(blockingHandler, true, null)); - } - - /** - * Registers a blocking handler. - * This makes the handler equivalent to being registered with {@link io.vertx.ext.web.Route#blockingHandler(Handler, boolean)} - * on the {@link RouteRegister}. - * - * @param blockingHandler The handler that contains blocking code. - * @return This builder. - */ - public final Builder addBlockingHandler(Handler blockingHandler, boolean ordered) { - return addHandler(new RouteHandler(blockingHandler, true, ordered)); - } - - public final Builder addFailureHandler(Handler failureHandler) { - this.failureHandlers.add(failureHandler); - return this; - } - - public final Builder addFailureHandler(Class throwableClass, BiConsumer handler) { - failureHandlers.add(new ExceptionHandler<>(throwableClass, handler)); - return this; - } - - public Builder isPublic(boolean isPublic) { - this.isPublic = isPublic; - return this; - } - - public Route build() { - if (path != null && !hasRegexPath && path.charAt(0) != '/') - throw new IllegalStateException("path must start with a /"); - - ImmutableList.Builder allHandlers = ImmutableList.builder(); - if (bodyHandler != null) { - allHandlers.add(new RouteHandler(bodyHandler, false)); - if (decodeBodyHandler != null) - allHandlers.add(new RouteHandler(decodeBodyHandler, false)); - } - - if (pathParamsHandler != null) - allHandlers.add(new RouteHandler(pathParamsHandler, false)); - - if (queryParamsHandler != null) - allHandlers.add(new RouteHandler(queryParamsHandler, false)); - - allHandlers.addAll(handlers.build()); - - return new Route( - path, - hasRegexPath, - methods.build(), - allHandlers.build(), - failureHandlers.build(), - isPublic); - } - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/Route.kt b/src/main/java/com/zepben/vertxutils/routing/Route.kt new file mode 100644 index 0000000..d5c5962 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/Route.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler +import com.zepben.vertxutils.routing.handlers.PathParamsHandler +import com.zepben.vertxutils.routing.handlers.QueryParamsHandler +import com.zepben.vertxutils.routing.handlers.params.BodyRule +import com.zepben.vertxutils.routing.handlers.params.PathParamRule +import com.zepben.vertxutils.routing.handlers.params.QueryParamRule +import com.zepben.vertxutils.routing.handlers.params.RequestValueConverter +import io.vertx.core.Handler +import io.vertx.core.http.HttpMethod +import io.vertx.ext.web.RequestBody +import io.vertx.ext.web.RoutingContext +import io.vertx.ext.web.handler.BodyHandler + +/** + * @property path The path of the route. + * @property hasRegexPath Set to true if the path uses regular expressions. Defaults to false. + * @property methods The HTTP method for the route. + * @property handlers A list of handlers for this route. Remember to always call [RoutingContext.next] to chain to your next handler if you have more than one. + * @property failureHandlers The failure handlers for the route. + * @property isPublic Indicates if the route should be documented as a public route. True if the route is a publicly documented route. + */ +class Route private constructor( + val path: String?, + val hasRegexPath: Boolean, + val methods: Set, + val handlers: List, + val failureHandlers: List>, + val isPublic: Boolean, +) { + + class Builder internal constructor() { + + private var path: String? = null + private var hasRegexPath = false + private val methods = mutableSetOf() + private var pathParamsHandler: PathParamsHandler? = null + private var queryParamsHandler: QueryParamsHandler? = null + private var bodyHandler: BodyHandler? = null + private var decodeBodyHandler: DecodeBodyHandler? = null + private val handlers = mutableListOf() + private val failureHandlers = mutableListOf>() + private var isPublic = true + + fun path(path: String): Builder = also { builder -> + require(!path.isEmpty()) { "path must not be empty" } + require(path.indexOf('%') < 0) { "formatted path must not contain a '%'" } + + builder.path = path + } + + fun path(pathFormat: String, vararg rules: PathParamRule<*>): Builder = apply { + var count = 0 + var index = pathFormat.indexOf('%') + while (index >= 0) { + ++count + require((index != 0) && (index < (pathFormat.length - 1)) && (pathFormat[index - 1] == ':') && (pathFormat[index + 1] == 's')) { + "invalid use of % in path format string" + } + index = pathFormat.indexOf('%', index + 1) + } + + require(count >= rules.size) { "too many path params" } + require(count <= rules.size) { "missing path params" } + + path(String.format(pathFormat, *rules.map { it.name }.toTypedArray())) + pathParamsHandler = PathParamsHandler(*rules) + } + + fun hasRegexPath(hasRegexPath: Boolean): Builder = also { builder -> + builder.hasRegexPath = hasRegexPath + } + + fun method(method: HttpMethod): Builder = apply { + methods.add(method) + } + + fun methods(vararg methods: HttpMethod): Builder = apply { + for (method in methods) + method(method) + } + + fun queryParams(vararg rules: QueryParamRule<*>): Builder = apply { + queryParamsHandler = QueryParamsHandler(*rules) + } + + fun bodySizeLimit(size: Long): Builder = apply { + if (bodyHandler == null) + bodyHandler(BodyHandler.create()) + + bodyHandler!!.setBodyLimit(size) + } + + fun uploadsDirectory(path: String): Builder = apply { + if (bodyHandler == null) + bodyHandler(BodyHandler.create()) + + bodyHandler!!.setUploadsDirectory(path) + } + + fun decodeBody(bodyConverter: RequestValueConverter, bodyRequired: Boolean = true): Builder = apply { + if (bodyHandler == null) + bodyHandler(BodyHandler.create()) + + decodeBodyHandler(DecodeBodyHandler(BodyRule(bodyConverter, bodyRequired))) + } + + fun bodyHandler(handler: BodyHandler): Builder = apply { + bodyHandler = handler + } + + fun decodeBodyHandler(handler: DecodeBodyHandler): Builder = apply { + decodeBodyHandler = handler + } + + fun addHandler(handler: RouteHandler): Builder = apply { + handlers.add(handler) + } + + fun addHandler(handler: Handler): Builder = + addHandler(RouteHandler(handler, false)) + + /** + * Registers a blocking handler. + * This makes the handler equivalent to being registered with [io.vertx.ext.web.Route.blockingHandler]. + * on the [RouteRegister] however the boolean ordered flag is set by the argument given to the route register. + * + * @param blockingHandler The handler that contains blocking code. + * @return This builder. + */ + fun addBlockingHandler(blockingHandler: Handler): Builder = + addHandler(RouteHandler(blockingHandler, true, null)) + + /** + * Registers a blocking handler. + * This makes the handler equivalent to being registered with [io.vertx.ext.web.Route.blockingHandler] + * on the [RouteRegister]. + * + * @param blockingHandler The handler that contains blocking code. + * @return This builder. + */ + fun addBlockingHandler(blockingHandler: Handler, ordered: Boolean): Builder = + addHandler(RouteHandler(blockingHandler, true, ordered)) + + fun addFailureHandler(failureHandler: Handler): Builder = apply { + failureHandlers.add(failureHandler) + } + + fun addFailureHandler(throwableClass: Class, handler: (T, RoutingContext?) -> Unit): Builder = apply { + failureHandlers.add(ExceptionHandler(throwableClass, handler)) + } + + fun isPublic(isPublic: Boolean): Builder = also { builder -> + builder.isPublic = isPublic + } + + fun build(): Route { + check((path == null) || hasRegexPath || (path!![0] == '/')) { "path must start with a /" } + + val allHandlers = listOfNotNull( + bodyHandler, + decodeBodyHandler.takeIf { bodyHandler != null }, + pathParamsHandler, + queryParamsHandler, + ).map { RouteHandler(it, false) } + + handlers + + return Route( + path, + hasRegexPath, + methods, + allHandlers, + failureHandlers, + isPublic, + ) + } + } + + companion object { + + fun builder(): Builder = Builder() + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteGroup.java b/src/main/java/com/zepben/vertxutils/routing/RouteGroup.java deleted file mode 100644 index c06bee6..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/RouteGroup.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -import java.util.List; - -@EverythingIsNonnullByDefault -public interface RouteGroup { - - String mountPath(); - - List routes(); - - static RouteGroup create(String mountPath, List routes) { - return new RouteGroup() { - @Override - public String mountPath() { - return mountPath; - } - - @Override - public List routes() { - return routes; - } - }; - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteGroup.kt b/src/main/java/com/zepben/vertxutils/routing/RouteGroup.kt new file mode 100644 index 0000000..6cc9d2c --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/RouteGroup.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +interface RouteGroup { + + val mountPath: String + val routes: List + + companion object { + + fun create(mountPath: String, routes: List): RouteGroup = + object : RouteGroup { + override val mountPath: String = mountPath + + override val routes: List = routes + } + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteHandler.java b/src/main/java/com/zepben/vertxutils/routing/RouteHandler.java deleted file mode 100644 index d220cc4..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/RouteHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; - -import javax.annotation.Nullable; -import java.util.Optional; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class RouteHandler { - - private final Handler handler; - private final boolean isBlocking; - @Nullable private final Boolean blockingOrdered; - - public RouteHandler(Handler handler, boolean isBlocking) { - this(handler, isBlocking, null); - } - - public RouteHandler(Handler handler, boolean isBlocking, @Nullable Boolean blockingOrdered) { - this.handler = handler; - this.isBlocking = isBlocking; - this.blockingOrdered = blockingOrdered; - } - - public Handler handler() { - return handler; - } - - public boolean isBlocking() { - return isBlocking; - } - - public Optional blockingOrdered() { - return Optional.ofNullable(blockingOrdered); - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteHandler.kt b/src/main/java/com/zepben/vertxutils/routing/RouteHandler.kt new file mode 100644 index 0000000..754326a --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/RouteHandler.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import io.vertx.core.Handler +import io.vertx.ext.web.RoutingContext + +class RouteHandler( + val handler: Handler, + val isBlocking: Boolean, + val blockingOrdered: Boolean? = null, +) diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegister.java b/src/main/java/com/zepben/vertxutils/routing/RouteRegister.java deleted file mode 100644 index 2fbf46e..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/RouteRegister.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.Handler; -import io.vertx.core.http.HttpMethod; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; - -import javax.annotation.Nullable; -import java.util.function.BiConsumer; - -@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) -@EverythingIsNonnullByDefault -public class RouteRegister { - private final Router router; - private final String rootMount; - private final boolean defaultOrderedBlockingRoutes; - private BiConsumer onAdd = (p, r) -> { - }; - - public RouteRegister(Router router, boolean defaultOrderedBlockingRoutes) { - this(router, "", defaultOrderedBlockingRoutes); - } - - public RouteRegister(Router router, String rootMount, boolean defaultOrderedBlockingRoutes) { - this.router = router; - this.rootMount = rootMount; - this.defaultOrderedBlockingRoutes = defaultOrderedBlockingRoutes; - } - - public Router router() { - return router; - } - - public RouteRegister onAdd(BiConsumer onAdd) { - this.onAdd = onAdd; - return this; - } - - public RouteRegister add(Route route, String mountPath) { - String path; - io.vertx.ext.web.Route vertxRoute; - if (route.path() == null) { - path = ""; - vertxRoute = router.route(); - } else { - path = buildPath(rootMount, mountPath, route.path()); - if (route.hasRegexPath()) - vertxRoute = router.routeWithRegex(path); - else - vertxRoute = router.route(path); - } - - for (HttpMethod method : route.methods()) - vertxRoute.method(method); - - for (RouteHandler handler : route.handlers()) { - if (handler.isBlocking()) { - vertxRoute.blockingHandler(handler.handler(), handler.blockingOrdered().orElse(defaultOrderedBlockingRoutes)); - } else { - vertxRoute.handler(handler.handler()); - } - } - - for (Handler handler : route.failureHandlers()) - vertxRoute.failureHandler(handler); - - onAdd.accept(path, route); - return this; - } - - public RouteRegister add(Route route) { - return add(route, ""); - } - - public RouteRegister add(RouteGroup group) { - group.routes().forEach(route -> add(route, group.mountPath())); - return this; - } - - public RouteRegister add(Iterable routes) { - routes.forEach(this::add); - return this; - } - - /** - * Register a collection of route groups with this RouteRegister. NOTE: This function can't be named `add` like the others due - * to type erasure making it have the same signature at the iterable for routes. - * - * @param routeGroups The collection of routes to register. - * @return This RouteRegister for fluent use. - */ - public RouteRegister addGroups(Iterable routeGroups) { - routeGroups.forEach(this::add); - return this; - } - - private String buildPath(String rootMount, String mountPath, @Nullable String routePath) { - String path = rootMount; - if (!mountPath.isEmpty()) - path += "/" + mountPath; - - if (routePath == null) { - // If the route path was null, it means match all paths, but now we are mounting it we need it to match - // all paths below the mount point. - if (!path.isEmpty()) - path += "/*"; - } else if (!routePath.isEmpty()) { - if (!path.isEmpty() && !routePath.equals("$")) - path += "/"; - - path += routePath; - } - - return path.replace("///", "/").replace("//", "/"); - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegister.kt b/src/main/java/com/zepben/vertxutils/routing/RouteRegister.kt new file mode 100644 index 0000000..f124ed5 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/RouteRegister.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import io.vertx.ext.web.Router + +class RouteRegister( + val router: Router, + val rootMount: String = "", + val defaultOrderedBlockingRoutes: Boolean, +) { + + var onAdd: (path: String, route: Route) -> Unit = { _, _ -> } + + fun add(route: Route, mountPath: String = ""): RouteRegister = apply { + val path = route.path?.let { buildPath(rootMount, mountPath, route.path) } + + val vertxRoute = when { + path == null -> router.route() + route.hasRegexPath -> router.routeWithRegex(path) + else -> router.route(path) + } + + route.methods.forEach { vertxRoute.method(it) } + + route.handlers.forEach { + when { + it.isBlocking -> vertxRoute.blockingHandler(it.handler, it.blockingOrdered ?: defaultOrderedBlockingRoutes) + else -> vertxRoute.handler(it.handler) + } + } + + route.failureHandlers.forEach { vertxRoute.failureHandler(it) } + + onAdd(path ?: "", route) + } + + fun add(group: RouteGroup): RouteRegister = apply { + group.routes.forEach { route -> add(route, group.mountPath) } + } + + fun add(routes: Iterable): RouteRegister = apply { + routes.forEach { route -> add(route) } + } + + /** + * Register a collection of route groups with this RouteRegister. NOTE: This function can't be named `add` like the others due + * to type erasure making it have the same signature at the iterable for routes. + * + * @param routeGroups The collection of routes to register. + * @return This RouteRegister for fluent use. + */ + fun addGroups(routeGroups: Iterable): RouteRegister = apply { + routeGroups.forEach { group -> add(group) } + } + + private fun buildPath(rootMount: String, mountPath: String, routePath: String?): String { + var path = rootMount + if (!mountPath.isEmpty()) path += "/$mountPath" + + if (routePath == null) { + // If the route path was null, it means match all paths, but now we are mounting it we need it to match + // all paths below the mount point. + if (!path.isEmpty()) + path += "/*" + } else if (routePath.isNotEmpty()) { + if (path.isNotEmpty() && routePath != "$") + path += "/" + + path += routePath + } + + return path.replace("///", "/").replace("//", "/") + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.java b/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.java deleted file mode 100644 index f90ab18..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import org.slf4j.Logger; - -import java.util.function.BiConsumer; - -@SuppressWarnings("WeakerAccess") -public class RouteRegisterLogger implements BiConsumer { - - private final Logger logger; - - public RouteRegisterLogger(Logger logger) { - this.logger = logger; - } - - @Override - public void accept(String path, Route route) { - route.methods().forEach(method -> logger.info(method + ": " + path)); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.kt b/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.kt new file mode 100644 index 0000000..413c72a --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import org.slf4j.Logger + +fun logRegisteredRoutes(logger: Logger): (String, Route) -> Unit = { path: String, route: Route -> + route.methods.forEach { method -> logger.info("$method: $path") } +} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersion.java b/src/main/java/com/zepben/vertxutils/routing/RouteVersion.java deleted file mode 100644 index 3a59ed4..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/RouteVersion.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.google.errorprone.annotations.Immutable; -import com.zepben.annotations.EverythingIsNonnullByDefault; - -@EverythingIsNonnullByDefault -@Immutable -@SuppressWarnings("WeakerAccess") -public class RouteVersion { - - private final int first; - private final int last; - - public static RouteVersion since(int first) { - return new RouteVersion(first, Integer.MAX_VALUE); - } - - public static RouteVersion between(int first, int last) { - return new RouteVersion(first, last); - } - - public boolean includes(int version) { - return (first <= version) && (last >= version); - } - - private RouteVersion(int first, int last) { - this.first = first; - this.last = last; - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersion.kt b/src/main/java/com/zepben/vertxutils/routing/RouteVersion.kt new file mode 100644 index 0000000..f63feeb --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/RouteVersion.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +class RouteVersion private constructor( + private val first: Int, + private val last: Int, +) { + + operator fun contains(version: Int): Boolean = (first <= version) && (last >= version) + + companion object { + + fun since(first: Int): RouteVersion = RouteVersion(first, Int.MAX_VALUE) + fun between(first: Int, last: Int): RouteVersion = RouteVersion(first, last) + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.java b/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.java deleted file mode 100644 index 863d412..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@EverythingIsNonnullByDefault -@SuppressWarnings("WeakerAccess") -public class RouteVersionUtils { - - public static List forVersion(T[] availableRoutes, int version, Function routeFactory) { - return Stream.of(availableRoutes) - .filter(rv -> rv.routeVersion().includes(version)) - .map(routeFactory) - .collect(Collectors.toList()); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.kt b/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.kt new file mode 100644 index 0000000..0ff38a2 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +object RouteVersionUtils { + + inline fun ((T) -> Route?).forVersion(version: Int): List where T : Enum, T : VersionableRoute = + enumValues() + .asSequence() + .filter { version in it.routeVersion } + .mapNotNull { this(it) } + .toList() + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.java b/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.java deleted file mode 100644 index 5218232..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler; -import com.zepben.vertxutils.routing.handlers.PathParamsHandler; -import com.zepben.vertxutils.routing.handlers.QueryParamsHandler; -import com.zepben.vertxutils.routing.handlers.params.BadParamException; -import com.zepben.vertxutils.routing.handlers.params.PathParams; -import com.zepben.vertxutils.routing.handlers.params.QueryParams; -import io.vertx.ext.web.RoutingContext; - -import java.util.Optional; - -/** - * These would ideally be extension methods for {@link io.vertx.ext.web.RoutingContext} but stupid Java doesn't have them. - */ -@EverythingIsNonnullByDefault -public class RoutingContextEx { - - public static final String PATH_PARAMS_KEY = PathParamsHandler.class.getSimpleName(); - public static final String QUERY_PARAMS_KEY = QueryParamsHandler.class.getSimpleName(); - public static final String BODY_KEY = DecodeBodyHandler.class.getSimpleName(); - - public static PathParams getPathParams(RoutingContext context) { - PathParams params = context.get(PATH_PARAMS_KEY); - if (params == null) - throw new IllegalStateException("PathParamsHandler must be called before you can use RoutingContextEx.getPathParams"); - - return params; - } - - public static void putPathParams(RoutingContext context, PathParams params) { - context.put(PATH_PARAMS_KEY, params); - } - - public static QueryParams getQueryParams(RoutingContext context) { - QueryParams params = context.get(QUERY_PARAMS_KEY); - if (params == null) - throw new IllegalStateException("QueryParamsHandler must be called before you can use RoutingContextEx.getQueryParams"); - - return params; - } - - public static void putQueryParams(RoutingContext context, QueryParams params) { - context.put(QUERY_PARAMS_KEY, params); - } - - public static T getDecodedBody(RoutingContext context) { - T body = context.get(BODY_KEY); - if (body == null) - throw BadParamException.missingBody(); - - return body; - } - - public static Optional getOptionalDecodedBody(RoutingContext context) { - return Optional.ofNullable(context.get(BODY_KEY)); - } - - public static void putRequestBody(RoutingContext context, Object body) { - context.put(BODY_KEY, body); - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.kt b/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.kt new file mode 100644 index 0000000..6ab7c7f --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler +import com.zepben.vertxutils.routing.handlers.PathParamsHandler +import com.zepben.vertxutils.routing.handlers.QueryParamsHandler +import com.zepben.vertxutils.routing.handlers.params.BadParamException +import com.zepben.vertxutils.routing.handlers.params.PathParams +import com.zepben.vertxutils.routing.handlers.params.QueryParams +import io.vertx.ext.web.RoutingContext + +/** + * These would ideally be extension methods for [RoutingContext] but stupid Java doesn't have them. + */ +object RoutingContextEx { + + val PATH_PARAMS_KEY: String = PathParamsHandler::class.java.getSimpleName() + val QUERY_PARAMS_KEY: String = QueryParamsHandler::class.java.getSimpleName() + val BODY_KEY: String = DecodeBodyHandler::class.java.getSimpleName() + + fun getPathParams(context: RoutingContext): PathParams { + val params = context.get(PATH_PARAMS_KEY) + checkNotNull(params) { "PathParamsHandler must be called before you can use RoutingContextEx.getPathParams" } + + return params + } + + fun putPathParams(context: RoutingContext, params: PathParams) { + context.put(PATH_PARAMS_KEY, params) + } + + fun getQueryParams(context: RoutingContext): QueryParams { + val params = context.get(QUERY_PARAMS_KEY) + checkNotNull(params) { "QueryParamsHandler must be called before you can use RoutingContextEx.getQueryParams" } + + return params + } + + fun putQueryParams(context: RoutingContext, params: QueryParams) { + context.put(QUERY_PARAMS_KEY, params) + } + + fun getDecodedBody(context: RoutingContext): T = + context.get(BODY_KEY) ?: throw BadParamException.missingBody() + + fun getOptionalDecodedBody(context: RoutingContext): T? = + context.get(BODY_KEY) + + fun putRequestBody(context: RoutingContext, body: Any) { + context.put(BODY_KEY, body) + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.java b/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.java deleted file mode 100644 index d79bcb9..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.handlers.FaviconHandler; -import com.zepben.vertxutils.routing.handlers.UtilHandlers; -import io.vertx.core.http.HttpMethod; -import io.vertx.ext.web.handler.StaticHandler; - -import javax.annotation.Nullable; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) -@EverythingIsNonnullByDefault -public class StaticAssetRoutes { - - private final String baseUrlPath; - @Nullable private String indexPage = null; - private final List subdirs = new ArrayList<>(); - private final String filePath; - @Nullable private String faviconUrlPath = null; - @Nullable private String faviconFilePath = null; - private boolean cachingEnabled = true; - private String defaultCharacterEncoding = StandardCharsets.UTF_8.name(); - - public StaticAssetRoutes(String baseUrlPath, String filePath, String... subdirs) { - this.baseUrlPath = addTrailingSlash(baseUrlPath); - - // Make sure we do not have a trailing slash on the base file path. This is required as vertx leaves the last - // character that matches the path (i.e. the slash) so we do not want to have a double slash. - if (filePath.endsWith("/")) - this.filePath = filePath.substring(0, filePath.length() - 1); - else - this.filePath = filePath; - - subDirs(subdirs); - } - - public StaticAssetRoutes indexPage(String indexPage) { - this.indexPage = indexPage; - return this; - } - - public StaticAssetRoutes subDirs(String... subdirs) { - this.subdirs.addAll(Arrays.asList(subdirs)); - return this; - } - - public StaticAssetRoutes favicon(String subUrlPath, String subFilePath) { - this.faviconUrlPath = String.format("%s%s", baseUrlPath, subUrlPath); - this.faviconFilePath = String.format("%s/%s", filePath, subFilePath); - return this; - } - - public StaticAssetRoutes cachingEnabled(boolean cachingEnabled) { - this.cachingEnabled = cachingEnabled; - return this; - } - - public StaticAssetRoutes defaultCharacterEncoding(String defaultCharacterEncoding) { - this.defaultCharacterEncoding = defaultCharacterEncoding; - return this; - } - - public List buildRoutes() { - List routes = new ArrayList<>(); - - indexPageRoutes(routes); - faviconRoute(routes); - subdirRoutes(routes); - - return routes; - } - - private void subdirRoutes(List routes) { - for (String subdir : subdirs) { - routes.add(Route.builder() - .path(baseUrlPath + subdir + "/*") - .method(HttpMethod.GET) - .addHandler(newStaticHandler(filePath + "/" + subdir)) - .build()); - } - } - - private void faviconRoute(List routes) { - if (faviconUrlPath != null && faviconFilePath != null) { - routes.add(Route.builder() - .path(faviconUrlPath) - .method(HttpMethod.GET) - .addHandler(new FaviconHandler(faviconFilePath, 86400)) - .build()); - } - } - - private void indexPageRoutes(List routes) { - if (indexPage != null) { - // Vert.x has a bug where it matches URLs with and without a trailing '/' as the same route. - // When no trailing / is on the URL, it causes issues with relative URLs in the returned html page. - // Workaround by providing an exact regex match for a path with no / and redirecting to path with a / - // TODO This has been raised at vertx github, but they can't decide what to do: https://github.com/vert-x3/vertx-web/issues/85 - routes.add(Route.builder() - .path(baseUrlPath.substring(0, baseUrlPath.length() - 1) + "$") - .hasRegexPath(true) - .method(HttpMethod.GET) - .addHandler(UtilHandlers.REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER) - .isPublic(false) - .build()); - - // Register the index page. - routes.add(Route.builder() - .path(baseUrlPath) - .method(HttpMethod.GET) - .addHandler(newStaticHandler(filePath).setIndexPage(indexPage)) - .build()); - } - } - - private String addTrailingSlash(String str) { - if (str.endsWith("/")) { - return str; - } else { - return str + "/"; - } - } - - private StaticHandler newStaticHandler(String path) { - return StaticHandler.create() - .setCachingEnabled(cachingEnabled) - .setAllowRootFileSystemAccess(true) - .setWebRoot(path) - .setDefaultContentEncoding(defaultCharacterEncoding); - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.kt b/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.kt new file mode 100644 index 0000000..0d36ed9 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.vertxutils.routing.handlers.FaviconHandler +import com.zepben.vertxutils.routing.handlers.UtilHandlers +import io.vertx.core.http.HttpMethod +import io.vertx.ext.web.handler.FileSystemAccess +import io.vertx.ext.web.handler.StaticHandler +import java.nio.charset.StandardCharsets + +class StaticAssetRoutes( + baseUrlPath: String, + filePath: String, + vararg subDirs: String, +) { + + private val baseUrlPath: String + private var indexPage: String? = null + private val subDirs = mutableListOf() + private val filePath: String + private var faviconUrlPath: String? = null + private var faviconFilePath: String? = null + private var cachingEnabled = true + private var defaultCharacterEncoding: String = StandardCharsets.UTF_8.name() + + init { + this.baseUrlPath = addTrailingSlash(baseUrlPath) + + // Make sure we do not have a trailing slash on the base file path. This is required as vertx leaves the last + // character that matches the path (i.e. the slash) so we do not want to have a double slash. + this.filePath = if (filePath.endsWith("/")) + filePath.substring(0, filePath.length - 1) + else + filePath + + subDirs(*subDirs) + } + + fun indexPage(indexPage: String): StaticAssetRoutes = also { + it.indexPage = indexPage + } + + fun subDirs(vararg subDirs: String): StaticAssetRoutes = also { + it.subDirs.addAll(subDirs) + } + + fun favicon(subUrlPath: String, subFilePath: String): StaticAssetRoutes = apply { + faviconUrlPath = String.format("%s%s", baseUrlPath, subUrlPath) + faviconFilePath = String.format("%s/%s", filePath, subFilePath) + } + + fun cachingEnabled(cachingEnabled: Boolean): StaticAssetRoutes = also { + it.cachingEnabled = cachingEnabled + } + + fun defaultCharacterEncoding(defaultCharacterEncoding: String): StaticAssetRoutes = also { + it.defaultCharacterEncoding = defaultCharacterEncoding + } + + fun buildRoutes(): List = buildList { + addIndexPageRoutes() + addFaviconRoute() + addSubdirRoutes() + } + + private fun MutableList.addSubdirRoutes() { + for (subdir in subDirs) { + add( + Route.builder() + .path("$baseUrlPath$subdir/*") + .method(HttpMethod.GET) + .addHandler(newStaticHandler("$filePath/$subdir")) + .build(), + ) + } + } + + private fun MutableList.addFaviconRoute() { + if (faviconUrlPath != null && faviconFilePath != null) { + add( + Route.builder() + .path(faviconUrlPath!!) + .method(HttpMethod.GET) + .addHandler(FaviconHandler(faviconFilePath!!, 86400)) + .build(), + ) + } + } + + private fun MutableList.addIndexPageRoutes() { + if (indexPage != null) { + // Vert.x has a bug where it matches URLs with and without a trailing '/' as the same route. + // When no trailing / is on the URL, it causes issues with relative URLs in the returned html page. + // Workaround by providing an exact regex match for a path with no / and redirecting to path with a / + // TODO This has been raised at vertx github, but they can't decide what to do: https://github.com/vert-x3/vertx-web/issues/85 + add( + Route.builder() + .path(baseUrlPath.substring(0, baseUrlPath.length - 1) + "$") + .hasRegexPath(true) + .method(HttpMethod.GET) + .addHandler(UtilHandlers.REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER) + .isPublic(false) + .build(), + ) + + // Register the index page. + add( + Route.builder() + .path(baseUrlPath) + .method(HttpMethod.GET) + .addHandler(newStaticHandler(filePath).setIndexPage(indexPage)) + .build(), + ) + } + } + + private fun addTrailingSlash(str: String): String = + when { + str.endsWith("/") -> str + else -> "$str/" + } + + private fun newStaticHandler(path: String): StaticHandler { + return StaticHandler.create(FileSystemAccess.ROOT, path) + .setCachingEnabled(cachingEnabled) + .setDefaultContentEncoding(defaultCharacterEncoding) + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.java b/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.java deleted file mode 100644 index 69d61a3..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -@EverythingIsNonnullByDefault -@SuppressWarnings("WeakerAccess") -public class StaticAssetsRouteConfig { - - private final String webRoot; - private final boolean isCaching; - - public static StaticAssetsRouteConfig of(String webRoot, boolean isCaching) { - return new StaticAssetsRouteConfig(webRoot, isCaching); - } - - public String webRoot() { - return webRoot; - } - - public boolean isCaching() { - return isCaching; - } - - private StaticAssetsRouteConfig(String webRoot, boolean isCaching) { - this.webRoot = webRoot; - this.isCaching = isCaching; - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.kt b/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.kt new file mode 100644 index 0000000..ef01765 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +class StaticAssetsRouteConfig private constructor( + val webRoot: String, + val isCaching: Boolean, +) { + + companion object { + + fun of(webRoot: String, isCaching: Boolean): StaticAssetsRouteConfig = + StaticAssetsRouteConfig(webRoot, isCaching) + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt b/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt index fa13dca..b9beca2 100644 --- a/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt +++ b/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Zeppelin Bend Pty Ltd + * Copyright 2026 Zeppelin Bend Pty Ltd * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,5 +12,6 @@ package com.zepben.vertxutils.routing */ interface VersionableRoute { - fun routeVersion(): RouteVersion + val routeVersion: RouteVersion + } diff --git a/src/main/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponse.kt b/src/main/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponse.kt new file mode 100644 index 0000000..e49e44e --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponse.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.chunked + +class CaptureChunkedJsonResponse( + bufferSize: Int = DEFAULT_BUFFER_SIZE, +) : ChunkedJsonResponse(bufferSize) { + + private var captured = "" + + override fun onResponseCompleted(sb: StringBuilder) { + captured = sb.toString() + } + + override fun checkWrite(sb: StringBuilder, force: Boolean) { + captured = sb.toString() + } + + override fun toString(): String { + return captured + } + + fun clear() { + captured = "" + sb.setLength(0) + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponse.kt b/src/main/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponse.kt new file mode 100644 index 0000000..e771711 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponse.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package com.zepben.vertxutils.routing.chunked + +import io.vertx.core.json.Json +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject + +/** + * The base class for building chunked JSON responses. + * + * @param bufferSize The initial capacity of the underlying string builder. + */ +abstract class ChunkedJsonResponse( + bufferSize: Int = DEFAULT_BUFFER_SIZE, +) { + + companion object { + + const val DEFAULT_BUFFER_SIZE = 1 shl 21 + + } + + protected val sb = StringBuilder(bufferSize) + private val needsSeparatorStack = ArrayDeque() + + /** + * DSL entry point for building a JSON object response. + * + * @param block The DSL block used to populate the object contents. + */ + fun ofObject(block: JsonObjectBuilder.() -> Unit) { + check(sb.isEmpty()) { "Can't reuse a non-clean response builder" } + JsonObjectBuilder().build(block) + } + + /** + * DSL entry point for building a JSON array response. + * + * @param block The DSL block used to populate the array contents. + */ + fun ofArray(block: JsonArrayBuilder.() -> Unit) { + check(sb.isEmpty()) { "Can't reuse a non-clean response builder" } + JsonArrayBuilder().build(block) + } + + /** + * A DSL builder for a JSON object. + */ + inner class JsonObjectBuilder : JsonBuilder("{", "}") { + + /** + * Add a scalar field to the JSON object being built, with appropriate escaping. + * + * NOTE: This deliberately removes support for adding JsonObject and JsonArray values directly, they should be provided via DSL builders. + * + * @param key The key for the filed in the object. + * @param value The value to associate with the [key]. + * @throws IllegalArgumentException for any unsupported value types. + */ + fun field(key: String, value: Any?) { + sb.writeKey(key).appendJsonValue(value) + checkWrite(sb) + } + + /** + * Add a nested object to the JSON object being built. + * + * @param key The key for the nested object in the object. + * @param block The DSL block used to build the nested object. + */ + fun obj(key: String, block: JsonObjectBuilder.() -> Unit) { + sb.writeKey(key) + JsonObjectBuilder().build(block) + } + + /** + * Add a nested array to the JSON object being built. + * + * @param key The key for the nested array in the object. + * @param block The DSL block used to build the nested array. + */ + fun array(key: String, block: JsonArrayBuilder.() -> Unit) { + sb.writeKey(key) + JsonArrayBuilder().build(block) + } + + private fun StringBuilder.writeKey(key: String): StringBuilder = + maybeAppendSeparator() + .append(Json.encode(key)) + .append(":") + + } + + /** + * A DSL builder for a JSON array. + */ + inner class JsonArrayBuilder : JsonBuilder("[", "]") { + + /** + * Add a scalar item to the JSON array being built, with appropriate escaping. + * + * NOTE: This deliberately removes support for adding JsonObject and JsonArray values directly, they should be provided via DSL builders. + * + * @param value The value to add to the array. + * @throws IllegalArgumentException for any unsupported value types. + */ + fun item(value: Any?) { + sb.maybeAppendSeparator().appendJsonValue(value) + checkWrite(sb) + } + + /** + * Add a nested object to the JSON array being built. + * + * @param block The DSL block used to build the nested object. + */ + fun obj(block: JsonObjectBuilder.() -> Unit) { + sb.maybeAppendSeparator() + JsonObjectBuilder().build(block) + } + + /** + * Add a nested array to the JSON array being built. + * + * @param block The DSL block used to build the nested array. + */ + fun array(block: JsonArrayBuilder.() -> Unit) { + sb.maybeAppendSeparator() + JsonArrayBuilder().build(block) + } + + } + + /** + * A base class for the DSL builders. + * + * @param openToken The token used to open the element being built by this builder. + * @param openToken The token used to close the element being built by this builder. + */ + abstract inner class JsonBuilder>( + private val openToken: String, + private val closeToken: String, + ) { + + /** + * Write the current buffer to the underlying destination if it is appropriate (e.g. exceeds minimum size requirements), or if it is forced. + * + * @param force Flag to indicate that writing should occur without performing any other checks. + */ + fun checkWrite(force: Boolean = false) = checkWrite(sb, force) + + /** + * Build the element with the given DSL block. + * + * @param block The DSL block used to populate this element. + */ + internal fun build(block: B.() -> Unit) { + sb.append(openToken) + needsSeparatorStack.addLast(false) + + @Suppress("UNCHECKED_CAST") + (this as B).block() + + sb.append(closeToken) + needsSeparatorStack.removeLast() + + if (needsSeparatorStack.isEmpty()) + onResponseCompleted(sb) + else + checkWrite(sb) + } + + /** + * Append a separator if it is required before adding the next element. + * + * The first item added to any element will suppress the separator, with any subsequent elements inserting it. + */ + protected fun StringBuilder.maybeAppendSeparator(): StringBuilder { + if (needsSeparatorStack.isNotEmpty()) { + if (needsSeparatorStack.removeLast()) + append(",") + needsSeparatorStack.addLast(true) + } + return this + } + + /** + * Append a JSON value to the element. This will provide required escaping. + * + * @param value The scalar value to add to this element. + * @throws IllegalArgumentException for any unsupported value types. + */ + protected fun StringBuilder.appendJsonValue(value: Any?): StringBuilder = + when (value) { + null -> append("null") + is String -> append(Json.encode(value)) + is Number, is Boolean -> append(value.toString()) + is JsonObject -> append(value.encode()) + is JsonArray -> append(value.encode()) + else -> throw IllegalArgumentException("Unsupported JSON value type: ${value::class}") + } + + } + + /** + * Write the current buffer to the underlying destination if it is appropriate (e.g. exceeds minimum size requirements), or if it is forced. + * + * NOTE: The [sb] buffer won't be reset by the base class, so this should be done in this function if required for the implementation. + * + * @param sb The buffer to write if required. + * @param force Flag to indicate that writing should occur without performing any other checks. + */ + protected abstract fun checkWrite(sb: StringBuilder, force: Boolean = false) + + /** + * Notification the response has been completed. + * + * This should handle the reaming buffer, then reset it if the response can be reused. + * + * @param sb The remaining buffer when the response was completed. + */ + protected abstract fun onResponseCompleted(sb: StringBuilder) + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponse.kt b/src/main/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponse.kt new file mode 100644 index 0000000..b4a69ed --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponse.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.chunked + +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.http.HttpServerResponse + +class HttpChunkedJsonResponse( + private val response: HttpServerResponse, + private val bufferSize: Int = DEFAULT_BUFFER_SIZE, +) : ChunkedJsonResponse(bufferSize) { + + private var canSetStatus = true + + override fun onResponseCompleted(sb: StringBuilder) { + canSetStatus = false + if (!response.closed()) { + response.end(sb.toString()) + } + } + + override fun checkWrite(sb: StringBuilder, force: Boolean) { + canSetStatus = false + if ((force || (sb.length >= bufferSize)) && !response.closed()) { + response.write(sb.toString()) + sb.setLength(0) + } + } + + var statusCode: HttpResponseStatus + get() = HttpResponseStatus.valueOf(response.statusCode) + set(status) { + // Once a response is committed (first write or end), it is too late to change the status. + check(canSetStatus) { "You can't set the status after the response has been committed" } + response.statusCode = status.code() + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.java b/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.java deleted file mode 100644 index 449d12c..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.ErrorFormatter; -import com.zepben.vertxutils.routing.Respond; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.BadParamException; -import com.zepben.vertxutils.routing.handlers.params.BodyRule; -import com.zepben.vertxutils.routing.handlers.params.ValueConversionException; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.RoutingContext; - -import javax.annotation.Nullable; - -@EverythingIsNonnullByDefault -public class DecodeBodyHandler implements Handler { - - private final BodyRule bodyRule; - - public DecodeBodyHandler(BodyRule bodyRule) { - this.bodyRule = bodyRule; - } - - @Override - public void handle(RoutingContext context) { - try { - Object decodedBody = handleBody(context); - if (decodedBody != null) - RoutingContextEx.putRequestBody(context, decodedBody); - - context.next(); - } catch (BadParamException ex) { - Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(ex.getMessage())); - } - } - - @SuppressWarnings("ConstantConditions") - @Nullable - private Object handleBody(RoutingContext context) { - Buffer rawBody = context.getBody(); - if (rawBody == null || rawBody.length() == 0) { - if (bodyRule.isRequired()) - throw BadParamException.missingBody(); - - return null; - } - - try { - Object body = bodyRule.converter().convert(rawBody); - if (body == null) - throw BadParamException.invalidBody(bodyRule, "value was converted into null value"); - - return body; - } catch (ValueConversionException ex) { - throw BadParamException.invalidBody(bodyRule, ex.getMessage()); - } - } - - public BodyRule bodyRule() { - return bodyRule; - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.kt new file mode 100644 index 0000000..192dfd0 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.vertxutils.routing.ErrorFormatter +import com.zepben.vertxutils.routing.Respond +import com.zepben.vertxutils.routing.RoutingContextEx +import com.zepben.vertxutils.routing.handlers.params.BadParamException +import com.zepben.vertxutils.routing.handlers.params.BodyRule +import com.zepben.vertxutils.routing.handlers.params.ValueConversionException +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.Handler +import io.vertx.ext.web.RoutingContext + +class DecodeBodyHandler( + val bodyRule: BodyRule<*>, +) : Handler { + + override fun handle(context: RoutingContext) { + try { + handleBody(context)?.also { + RoutingContextEx.putRequestBody(context, it) + } + + context.next() + } catch (ex: BadParamException) { + Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(ex.message)) + } + } + + private fun handleBody(context: RoutingContext): Any? { + val rawBody = context.body() + if (rawBody == null || rawBody.length() <= 0) { + if (bodyRule.isRequired) + throw BadParamException.missingBody() + + return null + } + + return try { + bodyRule.converter.convert(rawBody) + ?: throw BadParamException.invalidBody(bodyRule, "value was converted into null value") + } catch (ex: ValueConversionException) { + throw BadParamException.invalidBody(bodyRule, ex.message) + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.java b/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.java deleted file mode 100644 index 875610a..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.Respond; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.file.FileSystemException; -import io.vertx.ext.web.RoutingContext; - -import javax.annotation.Nullable; - -/** - * Rework on the vertx {@link io.vertx.ext.web.handler.impl.FaviconHandlerImpl}: - *

- *

    - *
  • Allows you to register a favicon at any url path, not just /favicon.ico
  • - *
  • Removes the ability to load from the classpath
  • - *
- */ -@EverythingIsNonnullByDefault -public class FaviconHandler implements Handler { - - private final String filePath; - @Nullable private Buffer icon; - private final long maxAgeSeconds; - - /** - * Create a new Favicon instance using a file in the file system and customizable cache period - *

- *

-     * Router router = Router.router(vertx);
-     * router.route().handler(FaviconHandler.create("/icons/icon.ico", 1000));
-     * 
- * - * @param filePath file path to icon - * @param maxAgeSeconds max age in http cache header - */ - @SuppressWarnings("WeakerAccess") - public FaviconHandler(String filePath, long maxAgeSeconds) { - this.filePath = filePath; - this.maxAgeSeconds = maxAgeSeconds; - if (maxAgeSeconds < 0) { - throw new IllegalArgumentException("maxAgeSeconds must be > 0"); - } - } - - @SuppressWarnings("WeakerAccess") - public String faviconPath() { - return filePath; - } - - public void handle(RoutingContext ctx) { - if (icon == null) { - icon = loadIcon(ctx); - } - - if (icon.length() > 0) { - ctx.response().putHeader("Content-Type", "image/x-icon"); - ctx.response().putHeader("Content-Length", Integer.toString(icon.length())); - ctx.response().putHeader("Cache-Control", "public, max-age=" + maxAgeSeconds); - ctx.response().end(icon); - } else { - Respond.with(ctx, HttpResponseStatus.NOT_FOUND); - } - } - - private Buffer loadIcon(RoutingContext ctx) { - try { - return ctx.vertx().fileSystem().readFileBlocking(filePath); - } catch (FileSystemException ex) { - return Buffer.buffer(); - } - } -} - diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.kt new file mode 100644 index 0000000..8c21ce2 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.vertxutils.routing.Respond +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.Handler +import io.vertx.core.buffer.Buffer +import io.vertx.core.file.FileSystemException +import io.vertx.ext.web.RoutingContext + +/** + * Rework on the vertx [io.vertx.ext.web.handler.impl.FaviconHandlerImpl]: + * + * * Allows you to register a favicon at any url path, not just /favicon.ico + * * Removes the ability to load from the classpath + * + * @param filePath file path to icon + * @param maxAgeSeconds max age in http cache header + */ +class FaviconHandler( + private val filePath: String, + private val maxAgeSeconds: Long, +) : Handler { + + private var cachedIcon: Buffer? = null + + init { + require(maxAgeSeconds >= 0) { "maxAgeSeconds must be > 0" } + } + + fun faviconPath(): String { + return filePath + } + + override fun handle(ctx: RoutingContext) { + val icon = cachedIcon ?: loadIcon(ctx).also { cachedIcon = it } + + if (icon.length() > 0) { + ctx.response().putHeader("Content-Type", "image/x-icon") + ctx.response().putHeader("Content-Length", icon.length().toString()) + ctx.response().putHeader("Cache-Control", "public, max-age=$maxAgeSeconds") + ctx.response().end(icon) + } else { + Respond.with(ctx, HttpResponseStatus.NOT_FOUND) + } + } + + private fun loadIcon(ctx: RoutingContext): Buffer = + try { + ctx.vertx().fileSystem().readFileBlocking(filePath) + } catch (_: FileSystemException) { + Buffer.buffer() + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.java b/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.java deleted file mode 100644 index 5543d69..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.ErrorFormatter; -import com.zepben.vertxutils.routing.Respond; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.*; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; - -import java.util.*; - -import static java.util.stream.Collectors.toMap; - -@EverythingIsNonnullByDefault -public class PathParamsHandler implements Handler { - - - private final Map> rules; - - @SuppressWarnings("WeakerAccess") - public PathParamsHandler(PathParamRule... rules) { - this(Arrays.asList(rules)); - } - - @SuppressWarnings("WeakerAccess") - public PathParamsHandler(Collection> rules) { - this.rules = rules.stream().collect(toMap(ParamRule::name, r -> r)); - } - - @SuppressWarnings("ConstantConditions") - @Override - public void handle(RoutingContext context) { - Map params = new HashMap<>(); - List errors = new ArrayList<>(); - for (PathParamRule rule : rules.values()) { - try { - String strValue = context.pathParam(rule.name()); - if (strValue == null) - throw BadParamException.missingParam(rule.name()); - - try { - Object value = rule.converter().convert(strValue); - if (value == null) - throw BadParamException.invalidParam(rule, strValue, "value was converted into null value"); - - params.put(rule.name(), value); - } catch (ValueConversionException ex) { - throw BadParamException.invalidParam(rule, strValue, ex.getMessage()); - } - } catch (BadParamException ex) { - errors.add(ex.getMessage()); - } - } - - if (errors.isEmpty()) { - RoutingContextEx.putPathParams(context, new PathParams(params)); - context.next(); - } else { - Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(errors)); - } - } - - public Map> rules() { - return Collections.unmodifiableMap(rules); - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.kt new file mode 100644 index 0000000..e7b6cab --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.vertxutils.routing.ErrorFormatter +import com.zepben.vertxutils.routing.Respond +import com.zepben.vertxutils.routing.RoutingContextEx +import com.zepben.vertxutils.routing.handlers.params.BadParamException +import com.zepben.vertxutils.routing.handlers.params.PathParamRule +import com.zepben.vertxutils.routing.handlers.params.PathParams +import com.zepben.vertxutils.routing.handlers.params.ValueConversionException +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.Handler +import io.vertx.ext.web.RoutingContext + +class PathParamsHandler( + rules: Collection>, +) : Handler { + + val rules: Map> = rules.associateBy { it.name } + + constructor(vararg rules: PathParamRule<*>) : this(rules.toList()) + + override fun handle(context: RoutingContext) { + val params = mutableMapOf() + val errors = rules.values.mapNotNull { rule -> + try { + val strValue = context.pathParam(rule.name) + ?: throw BadParamException.missingParam(rule.name) + + try { + params[rule.name] = rule.converter.convert(strValue) + ?: throw BadParamException.invalidParam(rule, strValue, "value was converted into null value") + null + } catch (ex: ValueConversionException) { + throw BadParamException.invalidParam(rule, strValue, ex.message) + } + } catch (ex: BadParamException) { + ex.message + } + } + + if (errors.isEmpty()) { + RoutingContextEx.putPathParams(context, PathParams(params)) + context.next() + } else { + Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(errors)) + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandler.java b/src/main/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandler.java deleted file mode 100644 index 821d98d..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandler.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.ErrorFormatter; -import com.zepben.vertxutils.routing.Respond; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.*; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; - -import java.util.*; - -import static java.util.stream.Collectors.toMap; - -@EverythingIsNonnullByDefault -public class QueryParamsHandler implements Handler { - - private final Map> rules; - - public QueryParamsHandler(QueryParamRule... rules) { - this(Arrays.asList(rules)); - } - - @SuppressWarnings("WeakerAccess") - public QueryParamsHandler(Collection> rules) { - if (rules.stream().map(ParamRule::name).distinct().count() != rules.size()) - throw new IllegalArgumentException("INTERNAL ERROR: The rules you have passed have a duplicate key."); - this.rules = rules.stream().collect(toMap(ParamRule::name, r -> r)); - } - - @SuppressWarnings("ConstantConditions") - @Override - public void handle(RoutingContext context) { - Map> params = new HashMap<>(); - List errors = new ArrayList<>(); - for (QueryParamRule rule : rules.values()) { - try { - List strValues = context.queryParam(rule.name()); - List values = new ArrayList<>(); - - if (strValues == null || strValues.isEmpty()) { - if (rule.isRequired()) - throw BadParamException.missingParam(rule.name()); - } else { - for (String strValue : strValues) { - try { - Object value = rule.converter().convert(strValue); - if (value == null) - throw BadParamException.invalidParam(rule, strValue, "value was converted into null value"); - - values.add(value); - } catch (ValueConversionException ex) { - throw BadParamException.invalidParam(rule, strValue, ex.getMessage()); - } - } - } - - if (!values.isEmpty()) - params.put(rule.name(), values); - } catch (BadParamException ex) { - errors.add(ex.getMessage()); - } - } - - if (errors.isEmpty()) { - RoutingContextEx.putQueryParams(context, new QueryParams(new HashSet<>(rules.values()), params)); - context.next(); - } else { - Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(errors)); - } - } - - public Map> rules() { - return Collections.unmodifiableMap(rules); - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandler.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandler.kt new file mode 100644 index 0000000..7c2b16a --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandler.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.vertxutils.routing.ErrorFormatter +import com.zepben.vertxutils.routing.Respond +import com.zepben.vertxutils.routing.RoutingContextEx +import com.zepben.vertxutils.routing.handlers.params.BadParamException +import com.zepben.vertxutils.routing.handlers.params.QueryParamRule +import com.zepben.vertxutils.routing.handlers.params.QueryParams +import com.zepben.vertxutils.routing.handlers.params.ValueConversionException +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.Handler +import io.vertx.ext.web.RoutingContext + +class QueryParamsHandler( + rules: Collection>, +) : Handler { + + val rules: Map> = rules.associateBy { it.name } + + private val rulesSet = rules.toSet() + + constructor(vararg rules: QueryParamRule<*>) : this(rules.toList()) + + init { + require(rules.size == this.rules.size) { "INTERNAL ERROR: The rules you have passed have a duplicate key." } + } + + override fun handle(context: RoutingContext) { + val params = mutableMapOf>() + val errors = rules.values.mapNotNull { rule -> + try { + val strValues = context.queryParam(rule.name) + val values = mutableListOf() + + if (strValues == null || strValues.isEmpty()) { + if (rule.isRequired) + throw BadParamException.missingParam(rule.name) + } else { + strValues.forEach { strValue -> + try { + val value = rule.converter.convert(strValue) + ?: throw BadParamException.invalidParam(rule, strValue, "value was converted into null value") + + values.add(value) + } catch (ex: ValueConversionException) { + throw BadParamException.invalidParam(rule, strValue, ex.message) + } + } + } + + if (!values.isEmpty()) + params[rule.name] = values + + null + } catch (ex: BadParamException) { + ex.message + } + } + + if (errors.isEmpty()) { + RoutingContextEx.putQueryParams(context, QueryParams(rulesSet, params)) + context.next() + } else { + Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(errors)) + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/UtilHandlers.java b/src/main/java/com/zepben/vertxutils/routing/handlers/UtilHandlers.java deleted file mode 100644 index 6629b41..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/UtilHandlers.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.ErrorFormatter; -import com.zepben.vertxutils.routing.Respond; -import io.vertx.core.Handler; -import io.vertx.core.VertxException; -import io.vertx.ext.web.RoutingContext; -import org.slf4j.Logger; - -import java.util.function.Function; - -import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; -import static io.netty.handler.codec.http.HttpResponseStatus.MOVED_PERMANENTLY; - -@EverythingIsNonnullByDefault -public class UtilHandlers { - - /** - * Returns the default failure handler that if the {@link RoutingContext#failure()} is not null responds with a - * 500 message with our own "standardised" errors JSON response (See {@link ErrorFormatter#asJson(String)}) - * containing the message from the failure. If a logger is specified, it will also log the stacktrace on the server side - */ - public static final Function> CATCH_ALL_API_FAILURE_HANDLER_WITH_EXCEPTION_LOGGING = logger -> (context) -> { - Throwable failure = context.failure(); - if (failure != null && !context.response().ended()) { - if (logger != null) { - logger.error("Error stack trace:", failure); - } - Respond.withJson(context, INTERNAL_SERVER_ERROR, ErrorFormatter.asJson(failure.toString())); - return; - } else if (failure instanceof VertxException && failure.getMessage().equals("Connection was closed")) { - // Don't call context.next() in this case because it logs it. We don't care. - return; - } - - context.next(); - }; - - - public static final Handler CATCH_ALL_API_FAILURE_HANDLER = context -> { - CATCH_ALL_API_FAILURE_HANDLER_WITH_EXCEPTION_LOGGING.apply(null).handle(context); - }; - - @Deprecated - public static final Handler DEFAULT_FAILURE_HANDLER = CATCH_ALL_API_FAILURE_HANDLER; - - /** - * Route handler to redirect routes with no trailing slash to one with a trailing slash. - */ - public static final Handler REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER = context -> { - String location = context.request().path() + "/"; - if (context.request().query() != null) - location += "?" + context.request().query(); - - context.response().putHeader("Location", location); - Respond.with(context, MOVED_PERMANENTLY); - }; -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/UtilHandlers.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/UtilHandlers.kt new file mode 100644 index 0000000..de4479e --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/UtilHandlers.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.vertxutils.routing.ErrorFormatter +import com.zepben.vertxutils.routing.Respond +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.Handler +import io.vertx.core.VertxException +import io.vertx.ext.web.RoutingContext +import org.slf4j.Logger + +object UtilHandlers { + + /** + * Returns the default failure handler that if the [RoutingContext.failure] is not null responds with a + * 500 message with our own "standardised" errors JSON response (See [ErrorFormatter.asJson]) + * containing the message from the failure. If a logger is specified, it will also log the stacktrace on the server side + */ + val CATCH_ALL_API_FAILURE_HANDLER_WITH_EXCEPTION_LOGGING: (Logger?) -> Handler = { logger -> + Handler { context: RoutingContext -> + val failure = context.failure() + if (failure != null && !context.response().ended()) { + logger?.error("Error stack trace:", failure) + + Respond.withJson(context, HttpResponseStatus.INTERNAL_SERVER_ERROR, ErrorFormatter.asJson(failure.toString())) + return@Handler + } else if (failure is VertxException && failure.message == "Connection was closed") { + // Don't call context.next() in this case because it logs it. We don't care. + return@Handler + } + context.next() + } + } + + val CATCH_ALL_API_FAILURE_HANDLER: Handler = { context -> + CATCH_ALL_API_FAILURE_HANDLER_WITH_EXCEPTION_LOGGING(null).handle(context) + } + + /** + * Route handler to redirect routes with no trailing slash to one with a trailing slash. + */ + val REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER: Handler = Handler { context -> + var location = "${context.request()?.path()}/${if (context.request().query() != null) "?" + context.request().query() else ""}" + + context.response().putHeader("Location", location) + Respond.with(context, HttpResponseStatus.MOVED_PERMANENTLY) + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BadParamException.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BadParamException.java deleted file mode 100644 index 7609460..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BadParamException.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -@EverythingIsNonnullByDefault -public class BadParamException extends RuntimeException { - - private BadParamException(String msg) { - super(msg); - } - - public static BadParamException missingParam(String name) { - return new BadParamException("missing required parameter " + name); - } - - public static BadParamException invalidParam(ParamRule rule, String value, String reason) { - String msg = String.format( - "Parameter '%s' with value '%s' is invalid. Expected format '%s': %s", - rule.name(), - value, - rule.converter().expectedFormat(), - reason); - - return new BadParamException(msg); - } - - public static BadParamException missingBody() { - return new BadParamException("required body is missing"); - } - - public static BadParamException invalidBody(BodyRule rule, String reason) { - String msg = String.format( - "body is invalid. Expected format '%s': %s", - rule.converter().expectedFormat(), - reason); - - return new BadParamException(msg); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BadParamException.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BadParamException.kt new file mode 100644 index 0000000..3ca812e --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BadParamException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +class BadParamException private constructor(msg: String) : RuntimeException(msg) { + + companion object { + + fun missingParam(name: String): BadParamException = + BadParamException("missing required parameter $name") + + fun invalidParam(rule: ParamRule<*, *>, value: String, reason: String?): BadParamException = + BadParamException("Parameter '${rule.name}' with value '$value' is invalid. Expected format '${rule.converter.expectedFormat}': $reason") + + fun missingBody(): BadParamException = + BadParamException("required body is missing") + + fun invalidBody(rule: BodyRule<*>, reason: String?): BadParamException = + BadParamException("body is invalid. Expected format '${rule.converter.expectedFormat}': $reason") + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyRule.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyRule.java deleted file mode 100644 index 4c3757d..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyRule.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.buffer.Buffer; - -@EverythingIsNonnullByDefault -public class BodyRule extends ParamRule { - - private final boolean isRequired; - - public BodyRule(RequestValueConverter converter, boolean isRequired) { - super("body", converter); - this.isRequired = isRequired; - } - - public boolean isRequired() { - return isRequired; - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyRule.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyRule.kt new file mode 100644 index 0000000..c7f26d3 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyRule.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import io.vertx.ext.web.RequestBody + +class BodyRule( + converter: RequestValueConverter, + val isRequired: Boolean, +) : ParamRule("body", converter) diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyType.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyType.java deleted file mode 100644 index 47f4170..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyType.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.DecodeException; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class BodyType { - - public static final RequestValueConverter JSON_OBJECT = RequestValueConverter.create( - "json object", - buffer -> { - if (buffer.length() == 0) - throw new ValueConversionException("unable to decode empty body"); - - try { - return new JsonObject(buffer.toString()); - } catch (DecodeException ex) { - throw new ValueConversionException(ex.getMessage()); - } - }); - - public static final RequestValueConverter JSON_ARRAY = RequestValueConverter.create( - "json array", - buffer -> { - if (buffer.length() == 0) - throw new ValueConversionException("unable to decode empty body"); - - try { - return new JsonArray(buffer.toString()); - } catch (DecodeException ex) { - throw new ValueConversionException(ex.getMessage()); - } - }); -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyType.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyType.kt new file mode 100644 index 0000000..dc8344c --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/BodyType.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import io.vertx.core.json.DecodeException +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import io.vertx.ext.web.RequestBody + +object BodyType { + + val JSON_OBJECT: RequestValueConverter = + RequestValueConverter.create("json object") { body -> + if (body.length() == 0) + throw ValueConversionException("unable to decode empty body") + + try { + JsonObject(body.asString()) + } catch (ex: DecodeException) { + throw ValueConversionException(ex.message) + } + } + + val JSON_ARRAY: RequestValueConverter = + RequestValueConverter.create("json array") { body -> + if (body.length() == 0) + throw ValueConversionException("unable to decode empty body") + + try { + JsonArray(body.asString()) + } catch (ex: DecodeException) { + throw ValueConversionException(ex.message) + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamRule.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamRule.java deleted file mode 100644 index 8d16e3b..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamRule.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -@EverythingIsNonnullByDefault -public abstract class ParamRule { - - private final String name; - private final RequestValueConverter converter; - - @SuppressWarnings("WeakerAccess") - public ParamRule(String name, RequestValueConverter converter) { - this.name = name; - this.converter = converter; - } - - public String name() { - return name; - } - - public RequestValueConverter converter() { - return converter; - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamRule.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamRule.kt new file mode 100644 index 0000000..95693a2 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamRule.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +abstract class ParamRule( + val name: String, + val converter: RequestValueConverter, +) diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamType.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamType.java deleted file mode 100644 index ee3a640..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamType.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.format.DateTimeParseException; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class ParamType { - - public static final RequestValueConverter STRING = RequestValueConverter.create( - "string", - param -> { - try { - return URLDecoder.decode(param, StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException | IllegalArgumentException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - public static final RequestValueConverter INT = RequestValueConverter.create( - "int", - param -> { - try { - return Integer.parseInt(param); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - public static final RequestValueConverter INT_POSITIVE = RequestValueConverter.create( - "positive int", - param -> { - try { - return Integer.parseUnsignedInt(param); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - public static final RequestValueConverter LONG = RequestValueConverter.create( - "long", - param -> { - try { - return Long.parseLong(param); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - public static final RequestValueConverter LONG_POSITIVE = RequestValueConverter.create( - "positive long", - param -> { - try { - return Long.parseUnsignedLong(param); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - public static final RequestValueConverter FLOAT = RequestValueConverter.create( - "float", - param -> { - try { - return Float.parseFloat(param); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - @SuppressWarnings("RedundantTypeArguments") - public static final RequestValueConverter FLOAT_POSITIVE = RequestValueConverter.create( - "positive float", - param -> { - try { - return Optional.of(Float.parseFloat(param)) - .filter(f -> f >= 0) - .orElseThrow(() -> new ValueConversionException("negative value")); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - public static final RequestValueConverter DOUBLE = RequestValueConverter.create( - "double", - param -> { - try { - return Double.parseDouble(param); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - @SuppressWarnings("RedundantTypeArguments") - public static final RequestValueConverter DOUBLE_POSITIVE = RequestValueConverter.create( - "positive double", - param -> { - try { - return Optional.of(Double.parseDouble(param)) - .filter(f -> f >= 0) - .orElseThrow(() -> new ValueConversionException("negative value")); - } catch (NumberFormatException e) { - throw new ValueConversionException(e.getMessage()); - } - }); - - public static final RequestValueConverter BOOL = RequestValueConverter.create( - "bool", - param -> Boolean.parseBoolean(param) || param.equals("1")); - - public static final RequestValueConverter LOCAL_DATE = RequestValueConverter.create( - "ISO standard date (e.g. yyyy-mm-dd)", - param -> { - try { - return LocalDate.parse(param); - } catch (DateTimeParseException ex) { - throw new ValueConversionException(ex.getMessage()); - } - }); - - public static final RequestValueConverter LOCAL_TIME = RequestValueConverter.create( - "ISO standard time (e.g. hh:mm)", - param -> { - // 0 pad the hours - if (param.indexOf(":") == 1) - param = "0" + param; - - try { - return LocalTime.parse(param); - } catch (DateTimeParseException ex) { - throw new ValueConversionException(ex.getMessage()); - } - }); - - public static final RequestValueConverter INSTANT = RequestValueConverter.create( - "ISO standard UTC date time (e.g. YYYY:MM:DDTHH:mm:ss.sssZ)", - param -> { - try { - return Instant.parse(param); - } catch (DateTimeParseException ex) { - throw new ValueConversionException(ex.getMessage()); - } - }); - - public static > RequestValueConverter ofEnum(Class clazz) { - T[] enumConstants = clazz.getEnumConstants(); - - return RequestValueConverter.create( - Stream.of(enumConstants).map(Enum::name).collect(Collectors.joining(", ")), - param -> { - for (T value : enumConstants) { - if (value.name().equalsIgnoreCase(param)) - return value; - } - - throw new ValueConversionException("unsupported enum value"); - }); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamType.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamType.kt new file mode 100644 index 0000000..6cab3d2 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ParamType.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import java.nio.charset.StandardCharsets +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeParseException + +object ParamType { + + val STRING: RequestValueConverter = + RequestValueConverter.create("string") { param -> + try { + URLDecoder.decode(param, StandardCharsets.UTF_8.name()) + } catch (e: UnsupportedEncodingException) { + throw ValueConversionException(e.message) + } catch (e: IllegalArgumentException) { + throw ValueConversionException(e.message) + } + } + + val INT: RequestValueConverter = + RequestValueConverter.create("int") { param -> + try { + param.toInt() + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val INT_POSITIVE: RequestValueConverter = + RequestValueConverter.create("positive int") { param -> + try { + Integer.parseUnsignedInt(param) + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val LONG: RequestValueConverter = + RequestValueConverter.create("long") { param -> + try { + param.toLong() + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val LONG_POSITIVE: RequestValueConverter = + RequestValueConverter.create("positive long") { param -> + try { + java.lang.Long.parseUnsignedLong(param) + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val FLOAT: RequestValueConverter = + RequestValueConverter.create("float") { param -> + try { + param.toFloat() + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val FLOAT_POSITIVE: RequestValueConverter = + RequestValueConverter.create("positive float") { param -> + try { + param.toFloat().takeIf { it >= 0 } ?: throw ValueConversionException("negative value") + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val DOUBLE: RequestValueConverter = + RequestValueConverter.create("double") { param -> + try { + param.toDouble() + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val DOUBLE_POSITIVE: RequestValueConverter = + RequestValueConverter.create("positive double") { param -> + try { + param.toDouble().takeIf { it >= 0 } ?: throw ValueConversionException("negative value") + } catch (e: NumberFormatException) { + throw ValueConversionException(e.message) + } + } + + val BOOL: RequestValueConverter = + RequestValueConverter.create("bool") { param -> param.toBoolean() || (param == "1") } + + val LOCAL_DATE: RequestValueConverter = + RequestValueConverter.create("ISO standard date (e.g. yyyy-mm-dd)") { param -> + try { + LocalDate.parse(param) + } catch (ex: DateTimeParseException) { + throw ValueConversionException(ex.message) + } + } + + val LOCAL_TIME: RequestValueConverter = + RequestValueConverter.create("ISO standard time (e.g. hh:mm)") { param -> + try { + // 0 pad the hours + LocalTime.parse(if (param.indexOf(":") == 1) "0$param" else param) + } catch (ex: DateTimeParseException) { + throw ValueConversionException(ex.message) + } + } + + val INSTANT: RequestValueConverter = + RequestValueConverter.create("ISO standard UTC date time (e.g. YYYY:MM:DDTHH:mm:ss.sssZ)") { param -> + try { + Instant.parse(param) + } catch (ex: DateTimeParseException) { + throw ValueConversionException(ex.message) + } + } + + inline fun > ofEnum(): RequestValueConverter = ofEnum(T::class.java) + + fun > ofEnum(clazz: Class): RequestValueConverter { + val enumConstants = clazz.getEnumConstants() + + return RequestValueConverter.create(enumConstants.joinToString { it.name }) { param -> + enumConstants.firstOrNull { it.name.equals(param, ignoreCase = true) } + ?: throw ValueConversionException("unsupported enum value") + } + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParamRule.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParamRule.java deleted file mode 100644 index 5b24f40..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParamRule.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -@EverythingIsNonnullByDefault -public class PathParamRule extends ParamRule { - - public static PathParamRule of(String name, RequestValueConverter converter) { - return new PathParamRule<>(name, converter); - } - - private PathParamRule(String name, RequestValueConverter converter) { - super(name, converter); - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParamRule.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParamRule.kt new file mode 100644 index 0000000..f700b9a --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParamRule.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +class PathParamRule private constructor( + name: String, + converter: RequestValueConverter, +) : ParamRule(name, converter) { + + companion object { + + fun of(name: String, converter: RequestValueConverter): PathParamRule = + PathParamRule(name, converter) + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParams.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParams.java deleted file mode 100644 index 691cac8..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParams.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -import java.util.Map; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class PathParams { - - private final Map params; - - public PathParams(Map params) { - this.params = params; - } - - @SuppressWarnings("unchecked") - public T get(PathParamRule rule) { - Object value = params.get(rule.name()); - if (value == null) - throw new IllegalArgumentException(String.format("INTERNAL ERROR: Path param %s was not registered with this route. Did you forget to register it?", rule.name())); - - return (T) value; - } - - public boolean exists(PathParamRule rule) { - return params.containsKey(rule.name()); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParams.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParams.kt new file mode 100644 index 0000000..9a40e4d --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/PathParams.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +class PathParams( + private val params: Map, +) { + + operator fun get(rule: PathParamRule): T { + val value = params[rule.name] + + requireNotNull(value) { "INTERNAL ERROR: Path param ${rule.name} was not registered with this route. Did you forget to register it?" } + + @Suppress("UNCHECKED_CAST") + return value as T + } + + operator fun contains(rule: PathParamRule): Boolean = rule.name in params + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRule.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRule.java deleted file mode 100644 index 3bde845..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRule.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -import javax.annotation.Nullable; - -@EverythingIsNonnullByDefault -public class QueryParamRule extends ParamRule { - - @Nullable private final T defaultValue; - private final boolean isRequired; - - public static QueryParamRule of(String name, RequestValueConverter converter) { - return new QueryParamRule<>(name, converter, null, false); - } - - public static QueryParamRule of(String name, RequestValueConverter converter, T defaultValue) { - return new QueryParamRule<>(name, converter, defaultValue, false); - } - - public static QueryParamRule ofRequired(String name, RequestValueConverter converter) { - return new QueryParamRule<>(name, converter, null, true); - } - - private QueryParamRule(String name, RequestValueConverter converter, @Nullable T defaultValue, boolean isRequired) { - super(name, converter); - this.defaultValue = defaultValue; - this.isRequired = isRequired; - } - - @Nullable - public T defaultValue() { - return defaultValue; - } - - public boolean isRequired() { - return isRequired; - } -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRule.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRule.kt new file mode 100644 index 0000000..91dfa30 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRule.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +class QueryParamRule private constructor( + name: String, + converter: RequestValueConverter, + val defaultValue: T?, + val isRequired: Boolean, +) : ParamRule(name, converter) { + + companion object { + + fun of(name: String, converter: RequestValueConverter): QueryParamRule = + QueryParamRule(name, converter, null, false) + + fun of(name: String, converter: RequestValueConverter, defaultValue: T): QueryParamRule = + QueryParamRule(name, converter, defaultValue, false) + + fun ofRequired(name: String, converter: RequestValueConverter): QueryParamRule = + QueryParamRule(name, converter, null, true) + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParams.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParams.java deleted file mode 100644 index 555e328..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParams.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import org.jetbrains.annotations.Contract; - -import javax.annotation.Nullable; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class QueryParams { - - private final Set> validRules; - private final Map> params; - - public QueryParams(Set> validRules, Map> params) { - this.validRules = validRules; - this.params = params; - } - - /** - * Get the first values passed via the query string for the given rule or the default value if no values were passed. - * - * @param rule The {@link QueryParamRule} to get the value for. - * @param The value type defined by the {@code rule}. - * @return The first values passed via the query string or the rules default value if no values were passed. - */ - public T get(QueryParamRule rule) { - return getAll(rule).get(0); - } - - /** - * Get the first values passed via the query string for the given rule or the specified value if no values were passed. - * - * @param rule The {@link QueryParamRule} to get the value for. - * @param other The value to use if no values were passed via the query string. - * @param The value type defined by the {@code rule}. - * @return The first values passed via the query string or {@code other} if no values were passed. - */ - @Nullable - @Contract("_, !null, -> !null") - public T getOrElse(QueryParamRule rule, @Nullable T other) { - return getAllOrElse(rule, other).get(0); - } - - /** - * Get all of the values passed via the query string for the given rule or the default value if no values were passed. - * - * @param rule The {@link QueryParamRule} to get the value for. - * @param The value type defined by the {@code rule}. - * @return The list of values passed via the query string or the rules default value if {@code other} is an empty list. - */ - public List getAll(QueryParamRule rule) { - List values = getAllValues(rule); - - if (values == null || values.isEmpty()) { - T defaultValue = rule.defaultValue(); - if (defaultValue == null) - throw new IllegalArgumentException(String.format("INTERNAL ERROR: Param %s has no values and no default. Either mark the param as required, provide a default or use with getOrElse or getAllOrElse.", rule.name())); - else - return Collections.singletonList(defaultValue); - } - - return values; - } - - /** - * Get all of the values passed via the query string for the given rule or the specified value if no values were passed. - * - * @param rule The {@link QueryParamRule} to get the value for. - * @param other The value to return if no values were passed via the query string. - * @param The value type defined by the {@code rule}. - * @return The list of values passed via the query string or a list containing {@code other} if no values were found. - */ - public List getAllOrElse(QueryParamRule rule, @Nullable T other) { - return getAllOrElse(rule, Collections.singletonList(other)); - } - - /** - * Get all of the values passed via the query string for the given rule or the specified value if no values were passed. - * - * @param rule The {@link QueryParamRule} to get the value for. - * @param other The list of values to return if no values were passed via the query string. - * @param The value type defined by the {@code rule}. - * @return The list of values passed via the query string or {@code other} if no values were found. - */ - public List getAllOrElse(QueryParamRule rule, List other) { - List values = getAllValues(rule); - - if (values == null || values.isEmpty()) - return other; - else - return values; - } - - /** - * @param rule The {@link QueryParamRule} to get the value for. - * @param The value type defined by the {@code rule}. - * @return True if at least one value was passed for the {@code rule} via the query string. - */ - public boolean exists(QueryParamRule rule) { - return params.containsKey(rule.name()); - } - - @SuppressWarnings("unchecked") - @Nullable - private List getAllValues(QueryParamRule rule) { - if (!validRules.contains(rule)) - throw new IllegalArgumentException(String.format("INTERNAL ERROR: Query param %s was not registered with this route. Did you forget to register it?", rule.name())); - - return (List) params.get(rule.name()); - } - -} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParams.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParams.kt new file mode 100644 index 0000000..a9ff441 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/QueryParams.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import org.jetbrains.annotations.Contract + +class QueryParams( + private val validRules: Set>, + private val params: Map>, +) { + + /** + * Get the first values passed via the query string for the given rule or the default value if no values were passed. + * + * @param rule The [QueryParamRule] to get the value for. + * @param The value type defined by the `rule`. + * @return The first values passed via the query string or the rules default value if no values were passed. + */ + operator fun get(rule: QueryParamRule): T = + getAll(rule)[0] + + /** + * Get the first values passed via the query string for the given rule or the specified value if no values were passed. + * + * @param rule The [QueryParamRule] to get the value for. + * @param other The value to use if no values were passed via the query string. + * @param The value type defined by the `rule`. + * @return The first values passed via the query string or `other` if no values were passed. + */ + @Contract("_, !null, -> !null") + fun getOrElse(rule: QueryParamRule, other: T?): T? = + getAllOrElse(rule, other)[0] + + /** + * Get all the values passed via the query string for the given rule or the default value if no values were passed. + * + * @param rule The [QueryParamRule] to get the value for. + * @param The value type defined by the `rule`. + * @return The list of values passed via the query string or the rules default value if `other` is an empty list. + */ + fun getAll(rule: QueryParamRule): List { + val values = getAllValues(rule) + + return if (values.isNullOrEmpty()) { + val defaultValue = requireNotNull(rule.defaultValue) { + "INTERNAL ERROR: Param ${rule.name} has no values and no default. Either mark the param as required, provide a default or use with getOrElse or getAllOrElse." + } + listOf(defaultValue) + } else + values + } + + /** + * Get all the values passed via the query string for the given rule or the specified value if no values were passed. + * + * @param rule The [QueryParamRule] to get the value for. + * @param other The value to return if no values were passed via the query string. + * @param The value type defined by the `rule`. + * @return The list of values passed via the query string or a list containing `other` if no values were found. + */ + fun getAllOrElse(rule: QueryParamRule, other: T?): List = + getAllOrElse(rule, listOf(other)) + + /** + * Get all the values passed via the query string for the given rule or the specified value if no values were passed. + * + * @param rule The [QueryParamRule] to get the value for. + * @param other The list of values to return if no values were passed via the query string. + * @param The value type defined by the `rule`. + * @return The list of values passed via the query string or `other` if no values were found. + */ + fun getAllOrElse(rule: QueryParamRule, other: List): List = + getAllValues(rule).takeUnless { it.isNullOrEmpty() } ?: other + + /** + * @param rule The [QueryParamRule] to get the value for. + * @param The value type defined by the `rule`. + * @return True if at least one value was passed for the `rule` via the query string. + */ + operator fun contains(rule: QueryParamRule): Boolean = params.containsKey(rule.name) + + private fun getAllValues(rule: QueryParamRule): List? { + require(validRules.contains(rule)) { + "INTERNAL ERROR: Query param ${rule.name} was not registered with this route. Did you forget to register it?" + } + + @Suppress("UNCHECKED_CAST") + return params[rule.name] as? List + } + +} diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/RequestValueConverter.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/RequestValueConverter.kt similarity index 50% rename from src/main/java/com/zepben/vertxutils/routing/handlers/params/RequestValueConverter.java rename to src/main/java/com/zepben/vertxutils/routing/handlers/params/RequestValueConverter.kt index 9ff982e..2459362 100644 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/RequestValueConverter.java +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/RequestValueConverter.kt @@ -1,64 +1,58 @@ /* - * Copyright 2020 Zeppelin Bend Pty Ltd + * Copyright 2026 Zeppelin Bend Pty Ltd * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +package com.zepben.vertxutils.routing.handlers.params -package com.zepben.vertxutils.routing.handlers.params; - -import com.zepben.annotations.EverythingIsNonnullByDefault; - -import java.util.function.Function; - -@EverythingIsNonnullByDefault -public interface RequestValueConverter { +interface RequestValueConverter { /** * Converts the given value from a raw request type to its required type. - *

+ * + * * This could be a string from a parameter or a buffer from a body. - *

- * If the given value cannot be converted, this method should throw a {@link ValueConversionException}. + * + * + * If the given value cannot be converted, this method should throw a [ValueConversionException]. * If a null value is returned it will be treated as a generic "failed conversion" error and a message like * "conversion resulted in null value" will be supplied as the reason for failure. - * + * * @param param the value from the request * @return The converted value, or null in the case the conversion fails. * @throws ValueConversionException if a value cannot be converted. */ - R convert(T param); - + fun convert(param: T): R /** * Returns a format description that the converter expects. This could be as simple as "string" or "int", but * may can be anything that describes how the value should be formatted. For example a date might be "YYYY-MM-DD". - * + * * @return A description of the expected format of values that can be converted. */ - String expectedFormat(); + val expectedFormat: String - /** - * Factory method to create an instance. - * - * @param converter Function to convert the param. - * @param expectedFormat The expected format of the param. - * @param The type of parameter to be converted. - * @param The return type of the conversion. - * @return a new RequestValueConverter instance. - */ - static RequestValueConverter create(String expectedFormat, Function converter) { - return new RequestValueConverter() { - @Override - public R convert(T param) { - return converter.apply(param); - } + companion object { + + /** + * Factory method to create an instance. + * + * @param converter Function to convert the param. + * @param expectedFormat The expected format of the param. + * @param The type of parameter to be converted. + * @param The return type of the conversion. + * @return a new RequestValueConverter instance. + */ + fun create(expectedFormat: String, converter: (T) -> R): RequestValueConverter = + object : RequestValueConverter { + + override fun convert(param: T): R = converter(param) + override val expectedFormat: String = expectedFormat - @Override - public String expectedFormat() { - return expectedFormat; } - }; + } + } diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ValueConversionException.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ValueConversionException.java deleted file mode 100644 index 12e7550..0000000 --- a/src/main/java/com/zepben/vertxutils/routing/handlers/params/ValueConversionException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -public class ValueConversionException extends RuntimeException { - - public ValueConversionException(String reason) { - super(reason); - } -} diff --git a/src/main/java/com/zepben/vertxutils/json/filter/JsonFilter.java b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ValueConversionException.kt similarity index 55% rename from src/main/java/com/zepben/vertxutils/json/filter/JsonFilter.java rename to src/main/java/com/zepben/vertxutils/routing/handlers/params/ValueConversionException.kt index 1b059bd..2590cd6 100644 --- a/src/main/java/com/zepben/vertxutils/json/filter/JsonFilter.java +++ b/src/main/java/com/zepben/vertxutils/routing/handlers/params/ValueConversionException.kt @@ -1,15 +1,10 @@ /* - * Copyright 2020 Zeppelin Bend Pty Ltd + * Copyright 2026 Zeppelin Bend Pty Ltd * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +package com.zepben.vertxutils.routing.handlers.params -package com.zepben.vertxutils.json.filter; - -public interface JsonFilter { - - R apply(T object, FilterSpecification fs); - -} +class ValueConversionException(reason: String?) : RuntimeException(reason) diff --git a/src/main/java/com/zepben/vertxutils/testing/DeployRestVerticleHelper.java b/src/main/java/com/zepben/vertxutils/testing/DeployRestVerticleHelper.java deleted file mode 100644 index 51d63da..0000000 --- a/src/main/java/com/zepben/vertxutils/testing/DeployRestVerticleHelper.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.testing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import io.vertx.core.DeploymentOptions; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; - -import java.io.IOException; -import java.net.ServerSocket; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import static com.jayway.awaitility.Awaitility.await; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class DeployRestVerticleHelper implements AutoCloseable { - - private final RequestSpecification requestSpec; - private final Vertx vertx; - - public DeployRestVerticleHelper(Class verticleClass, JsonObject config) { - try { - int port = getRandomPortNumber(); - config.put("http.port", port); - - // Start the server - Promise promise = Promise.promise(); - Future future = promise.future(); - vertx = Vertx.vertx(); - DeploymentOptions options = new DeploymentOptions().setConfig(config); - vertx.deployVerticle(verticleClass.getName(), - options, - ar -> { - if (ar.succeeded()) - promise.complete(); - else - promise.fail(ar.cause()); - }); - - await().atMost(5, TimeUnit.SECONDS).until(future::isComplete); - - if (!future.succeeded()) - throw new AssertionError(future.cause().getMessage()); - - requestSpec = new RequestSpecBuilder().setBaseUri("http://localhost").setPort(port).build(); - } catch (IOException ex) { - throw new AssertionError("Failed to start server", ex); - } - } - - @Override - public void close() { - AtomicBoolean done = new AtomicBoolean(false); - vertx.close(v -> done.set(true)); - await().until(done::get); - } - - @SuppressWarnings("UnusedReturnValue") - public RequestSpecification requestSpec() { - return requestSpec; - } - - public int getRandomPortNumber() throws IOException { - try (ServerSocket socket = new ServerSocket(0)) { - return socket.getLocalPort(); - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/testing/DeployRestVerticleHelper.kt b/src/main/java/com/zepben/vertxutils/testing/DeployRestVerticleHelper.kt new file mode 100644 index 0000000..6fddf33 --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/testing/DeployRestVerticleHelper.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.testing + +import com.jayway.awaitility.Awaitility +import io.restassured.builder.RequestSpecBuilder +import io.restassured.specification.RequestSpecification +import io.vertx.core.DeploymentOptions +import io.vertx.core.Promise +import io.vertx.core.Vertx +import io.vertx.core.json.JsonObject +import java.io.IOException +import java.lang.AutoCloseable +import java.net.ServerSocket +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +class DeployRestVerticleHelper( + verticleClass: Class<*>, + config: JsonObject, +) : AutoCloseable { + + val requestSpec: RequestSpecification + private val vertx: Vertx + + init { + try { + val port = this.randomPortNumber + config.put("http.port", port) + + // Start the server + val promise = Promise.promise() + val future = promise.future() + vertx = Vertx.vertx() + val options = DeploymentOptions().setConfig(config) + vertx.deployVerticle( + verticleClass.getName(), + options, + ) { ar -> + if (ar!!.succeeded()) promise.complete() + else promise.fail(ar.cause()) + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until { future.isComplete } + + if (!future.succeeded()) throw AssertionError(future.cause().message) + + requestSpec = RequestSpecBuilder().setBaseUri("http://localhost").setPort(port).build() + } catch (ex: IOException) { + throw AssertionError("Failed to start server", ex) + } + } + + override fun close() { + val done = AtomicBoolean(false) + vertx.close { done.set(true) } + Awaitility.await().until { done.get() } + } + + @get:Throws(IOException::class) + val randomPortNumber: Int + get() = ServerSocket(0).use { it.getLocalPort() } + +} diff --git a/src/main/java/com/zepben/vertxutils/testing/MockRoutingContext.java b/src/main/java/com/zepben/vertxutils/testing/MockRoutingContext.java deleted file mode 100644 index d8aacd7..0000000 --- a/src/main/java/com/zepben/vertxutils/testing/MockRoutingContext.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.testing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.PathParamRule; -import com.zepben.vertxutils.routing.handlers.params.PathParams; -import com.zepben.vertxutils.routing.handlers.params.QueryParamRule; -import com.zepben.vertxutils.routing.handlers.params.QueryParams; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.ext.web.RoutingContext; - -import javax.annotation.Nullable; -import java.util.*; - -import static org.mockito.Mockito.*; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class MockRoutingContext { - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - @Nullable private PathParams pathParams; - private final Map pathParamsMap = new HashMap<>(); - @Nullable private QueryParams queryParams; - private final Map> queryParamsMap = new HashMap<>(); - private final Set> queryParamRules = new HashSet<>(); - @Nullable private Object decodedBody; - - public RoutingContext build() { - RoutingContext context = mock(RoutingContext.class); - HttpServerRequest request = mock(HttpServerRequest.class, RETURNS_SELF); - HttpServerResponse response = mock(HttpServerResponse.class, RETURNS_SELF); - - doReturn(request).when(context).request(); - doReturn(response).when(context).response(); - - doReturn(Objects.requireNonNullElseGet(pathParams, () -> new PathParams(pathParamsMap))).when(context).get(RoutingContextEx.PATH_PARAMS_KEY); - - doReturn(Objects.requireNonNullElseGet(queryParams, () -> new QueryParams(queryParamRules, queryParamsMap))).when(context).get(RoutingContextEx.QUERY_PARAMS_KEY); - - doReturn(decodedBody).when(context).get(RoutingContextEx.BODY_KEY); - - return context; - } - - public Builder pathParams(PathParams params) { - pathParams = params; - return this; - } - - public Builder pathParam(PathParamRule rule, Object value) { - pathParamsMap.put(rule.name(), value); - return this; - } - - public Builder queryParams(QueryParams params) { - queryParams = params; - return this; - } - - public Builder queryParam(QueryParamRule rule) { - queryParams(rule); - return this; - } - - public Builder queryParams(QueryParamRule... rule) { - queryParamRules.addAll(Arrays.asList(rule)); - return this; - } - - public Builder queryParam(QueryParamRule rule, Object... values) { - queryParam(rule); - queryParamsMap.computeIfAbsent(rule.name(), k -> new ArrayList<>()).addAll(Arrays.asList(values)); - return this; - } - - public Builder decodedBody(Object decodedBody) { - this.decodedBody = decodedBody; - return this; - } - - private Builder() { - } - - } - - private MockRoutingContext() { - } - -} diff --git a/src/main/java/com/zepben/vertxutils/testing/MockRoutingContext.kt b/src/main/java/com/zepben/vertxutils/testing/MockRoutingContext.kt new file mode 100644 index 0000000..86b811e --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/testing/MockRoutingContext.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.testing + +import com.zepben.vertxutils.routing.RoutingContextEx +import com.zepben.vertxutils.routing.handlers.params.PathParamRule +import com.zepben.vertxutils.routing.handlers.params.PathParams +import com.zepben.vertxutils.routing.handlers.params.QueryParamRule +import com.zepben.vertxutils.routing.handlers.params.QueryParams +import io.vertx.core.http.HttpServerRequest +import io.vertx.core.http.HttpServerResponse +import io.vertx.ext.web.RoutingContext +import org.mockito.Mockito.* + +object MockRoutingContext { + + fun builder(): Builder = Builder() + + class Builder internal constructor() { + + private var pathParams: PathParams? = null + private val pathParamsMap = mutableMapOf() + private var queryParams: QueryParams? = null + private val queryParamsMap = mutableMapOf>() + private val queryParamRules = mutableSetOf>() + private var decodedBody: Any? = null + + fun build(): RoutingContext { + val context = mock(RoutingContext::class.java) + val request = mock(HttpServerRequest::class.java, RETURNS_SELF) + val response = mock(HttpServerResponse::class.java, RETURNS_SELF) + + doReturn(request).`when`(context).request() + doReturn(response).`when`(context).response() + + doReturn(pathParams ?: run { PathParams(pathParamsMap) }) + .`when`(context).get(RoutingContextEx.PATH_PARAMS_KEY) + + doReturn(queryParams ?: run { QueryParams(queryParamRules, queryParamsMap) }) + .`when`(context).get(RoutingContextEx.QUERY_PARAMS_KEY) + + doReturn(decodedBody).`when`(context).get(RoutingContextEx.BODY_KEY) + + return context + } + + fun pathParams(params: PathParams): Builder = apply { + pathParams = params + } + + fun pathParam(rule: PathParamRule<*>, value: Any): Builder = apply { + pathParamsMap[rule.name] = value + } + + fun queryParams(params: QueryParams): Builder = apply { + queryParams = params + } + + fun queryParam(rule: QueryParamRule<*>): Builder = apply { + queryParams(rule) + } + + fun queryParams(vararg rule: QueryParamRule<*>): Builder = apply { + queryParamRules.addAll(listOf(*rule)) + } + + fun queryParam(rule: QueryParamRule<*>, vararg values: Any): Builder = apply { + queryParam(rule) + queryParamsMap.getOrPut(rule.name) { mutableListOf() }.addAll(values) + } + + fun decodedBody(decodedBody: Any): Builder = also { + it.decodedBody = decodedBody + } + + } + +} diff --git a/src/main/java/com/zepben/vertxutils/testing/TestHttpServer.java b/src/main/java/com/zepben/vertxutils/testing/TestHttpServer.java deleted file mode 100644 index f387ed5..0000000 --- a/src/main/java/com/zepben/vertxutils/testing/TestHttpServer.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.testing; - -import com.zepben.vertxutils.routing.Route; -import com.zepben.vertxutils.routing.RouteRegister; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.ext.web.Router; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.ServerSocket; -import java.util.concurrent.CountDownLatch; - -@SuppressWarnings("WeakerAccess") -public class TestHttpServer implements AutoCloseable { - - private final Vertx vertx; - private final HttpServer server; - private final Router router; - private final RouteRegister routeRegister; - - public TestHttpServer() { - this(true); - } - - public TestHttpServer(boolean orderBlockingRules) { - vertx = Vertx.vertx(); - server = vertx.createHttpServer(); - router = Router.router(vertx); - routeRegister = new RouteRegister(router, orderBlockingRules); - } - - public TestHttpServer addRoute(Route route) { - routeRegister.add(route); - return this; - } - - public TestHttpServer addRoutes(Iterable routes) { - routeRegister.add(routes); - return this; - } - - public int listen() { - CountDownLatch latch = new CountDownLatch(1); - server.requestHandler(router) - .listen(getRandomPortNumber(), res -> { - if (res.failed()) - throw new RuntimeException(res.cause()); - - latch.countDown(); - }); - - try { - latch.await(); - } catch (InterruptedException ignored) { - } - - return server.actualPort(); - } - - @Override - public void close() { - CountDownLatch latch = new CountDownLatch(2); - server.close(none -> latch.countDown()); - vertx.close(none -> latch.countDown()); - - try { - latch.await(); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - } - - private int getRandomPortNumber() { - try (ServerSocket socket = new ServerSocket(0)) { - return socket.getLocalPort(); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - -} diff --git a/src/main/java/com/zepben/vertxutils/testing/TestHttpServer.kt b/src/main/java/com/zepben/vertxutils/testing/TestHttpServer.kt new file mode 100644 index 0000000..578478b --- /dev/null +++ b/src/main/java/com/zepben/vertxutils/testing/TestHttpServer.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.testing + +import com.zepben.vertxutils.routing.Route +import com.zepben.vertxutils.routing.RouteRegister +import io.vertx.core.Vertx +import io.vertx.core.http.HttpServer +import io.vertx.ext.web.Router +import java.io.IOException +import java.io.UncheckedIOException +import java.lang.AutoCloseable +import java.net.ServerSocket +import java.util.concurrent.CountDownLatch + +class TestHttpServer( + orderBlockingRules: Boolean = true, +) : AutoCloseable { + + private val vertx: Vertx = Vertx.vertx() + private val server: HttpServer = vertx.createHttpServer() + private val router: Router = Router.router(vertx) + private val routeRegister: RouteRegister = RouteRegister(router, defaultOrderedBlockingRoutes = orderBlockingRules) + + fun addRoute(route: Route): TestHttpServer = apply { + routeRegister.add(route) + } + + fun addRoutes(routes: Iterable): TestHttpServer = apply { + routeRegister.add(routes) + } + + fun listen(): Int { + val latch = CountDownLatch(1) + server.requestHandler(router) + .listen( + this.randomPortNumber, + ) { res -> + if (res.failed()) throw RuntimeException(res.cause()) + latch.countDown() + } + + try { + latch.await() + } catch (_: InterruptedException) { + } + + return server.actualPort() + } + + override fun close() { + val latch = CountDownLatch(2) + server.close { latch.countDown() } + vertx.close { latch.countDown() } + + try { + latch.await() + } catch (ex: InterruptedException) { + throw RuntimeException(ex) + } + } + + private val randomPortNumber: Int + get() = + try { + ServerSocket(0).use { it.getLocalPort() } + } catch (ex: IOException) { + throw UncheckedIOException(ex) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/TestsTest.kt b/src/test/java/com/zepben/vertxutils/TestsTest.kt new file mode 100644 index 0000000..86d86e8 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/TestsTest.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package com.zepben.vertxutils + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.testutils.junit.TestClassValidator +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class TestsTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + } + + @Test + internal fun `validate test classes`() { + TestClassValidator.validate() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/CollectorsTest.java b/src/test/java/com/zepben/vertxutils/json/CollectorsTest.java deleted file mode 100644 index 4e30a75..0000000 --- a/src/test/java/com/zepben/vertxutils/json/CollectorsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import io.vertx.core.json.JsonArray; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -public class CollectorsTest { - - @SuppressWarnings("InstantiationOfUtilityClass") - @Test - public void coverage() { - new Collectors(); - } - - @Test - public void toJsonArray() { - List list = Arrays.asList(1, 2, 3, 4, 5); - JsonArray jsonArray = list.stream().collect(Collectors.toJsonArray()); - - assertThat(list.size(), equalTo(jsonArray.size())); - for (int i = 0; i < list.size(); i++) - assertThat(list.get(i), equalTo(jsonArray.getInteger(i))); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/CollectorsTest.kt b/src/test/java/com/zepben/vertxutils/json/CollectorsTest.kt new file mode 100644 index 0000000..f5ad468 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/CollectorsTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.json.Collectors.toJsonArray +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class CollectorsTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun toJsonArray() { + val list = listOf(1, 2, 3, 4, 5) + val jsonArray = list.asSequence().toJsonArray() + + assertThat(list.size, equalTo(jsonArray.size())) + list.forEachIndexed { index, value -> assertThat(value, equalTo(jsonArray.getInteger(index))) } + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/JsonUtilsTest.java b/src/test/java/com/zepben/vertxutils/json/JsonUtilsTest.java deleted file mode 100644 index 44e3e18..0000000 --- a/src/test/java/com/zepben/vertxutils/json/JsonUtilsTest.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.testutils.junit.SystemLogExtension; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import javax.annotation.Nullable; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Stream; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static com.zepben.vertxutils.json.Collectors.toJsonArray; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.core.IsEqual.equalTo; - -@EverythingIsNonnullByDefault -public class JsonUtilsTest { - - @RegisterExtension - static SystemLogExtension systemOut = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess(); - - @RegisterExtension - static SystemLogExtension systemErr = SystemLogExtension.SYSTEM_ERR.captureLog().muteOnSuccess(); - - private static final String VALID_KEY = "key"; - private static final String MISSING_KEY = "key2"; - - private final int integerVal1 = 111; - private final int integerVal2 = 222; - private final int integerVal3 = 333; - private final double doubleVal1 = 1.11; - private final double doubleVal2 = 2.22; - private final String stringVal = "value"; - private final Path pathVal = Paths.get("valid/path"); - private final String illegalPath = "illegal\0path"; - - private final JsonObject jsonObjectVal1 = createObjectWithValue(stringVal); - private final JsonObject jsonObjectVal2 = createObjectWithValue(doubleVal2); - private final JsonObject jsonObjectVal3 = createObjectWithValue(illegalPath); - private final JsonArray jsonArrayOfJsonObjects = createArray(jsonObjectVal1, jsonObjectVal2); - private final JsonArray jsonArrayOfIntegers = createArray(integerVal1, integerVal2, integerVal3); - private final JsonArray jsonArrayOfMixed = createArray(jsonObjectVal1, integerVal2); - - @SuppressWarnings("InstantiationOfUtilityClass") - @Test - public void coverage() { - new JsonUtils(); - new JsonValueExtractors(); - } - - @Test - public void extractValue() throws Exception { - validateExtractor(JsonUtils::extractOptionalValue, JsonUtils::extractRequiredValue, jsonObjectVal1); - validateExtractor(JsonUtils::extractOptionalValue, JsonUtils::extractRequiredValue, integerVal1); - validateExtractor(JsonUtils::extractOptionalValue, JsonUtils::extractRequiredValue, stringVal); - } - - @Test - public void extractObject() throws Exception { - validateExtractor(JsonUtils::extractOptionalObject, JsonUtils::extractRequiredObject, jsonObjectVal1, stringVal, "object"); - } - - @Test - public void extractArray() throws Exception { - validateExtractor(JsonUtils::extractOptionalArray, JsonUtils::extractRequiredArray, jsonArrayOfJsonObjects, stringVal, "array"); - validateExtractor(JsonUtils::extractOptionalArray, JsonUtils::extractRequiredArray, jsonArrayOfIntegers, stringVal, "array"); - } - - @Test - public void extractString() throws Exception { - validateExtractor(JsonUtils::extractOptionalString, JsonUtils::extractRequiredString, stringVal, integerVal1, "string"); - } - - @Test - public void extractInt() throws Exception { - validateExtractor(JsonUtils::extractOptionalInt, JsonUtils::extractRequiredInt, integerVal1, stringVal, "integer"); - } - - @Test - public void extractDouble() throws Exception { - validateExtractor(JsonUtils::extractOptionalDouble, JsonUtils::extractRequiredDouble, doubleVal1, stringVal, "double"); - validateExtractor(JsonUtils::extractOptionalDouble, JsonUtils::extractRequiredDouble, Double.NaN, stringVal, "double"); - } - - @Test - public void extractPath() throws Exception { - validateExtractor(JsonUtils::extractOptionalPath, JsonUtils::extractRequiredPath, pathVal, Path::toString, doubleVal2, "path"); - validateExtractor(JsonUtils::extractOptionalPath, JsonUtils::extractRequiredPath, pathVal, Path::toString, illegalPath, "path"); - } - - @Test - public void extractObjectList() throws Exception { - List jsonObjects = Arrays.asList(jsonObjectVal1, jsonObjectVal2); - - validateExtractor(JsonUtils::extractOptionalObjectList, - JsonUtils::extractRequiredObjectList, - jsonObjects, - JsonArray::new, - Arrays.asList(integerVal1, integerVal2), - "list of objects"); - - validateExtractor(JsonUtils::extractOptionalObjectList, - JsonUtils::extractRequiredObjectList, - jsonObjects, - JsonArray::new, - stringVal, - "array"); - } - - @Test - public void extractStringList() throws Exception { - List strings = Arrays.asList(stringVal, stringVal); - - validateExtractor(JsonUtils::extractOptionalStringList, - JsonUtils::extractRequiredStringList, - strings, - JsonArray::new, - Arrays.asList(integerVal1, integerVal2), - "list of strings"); - - validateExtractor(JsonUtils::extractOptionalStringList, - JsonUtils::extractRequiredStringList, - strings, - JsonArray::new, - stringVal, - "array"); - } - - @Test - public void extractIntList() throws Exception { - List integers = Arrays.asList(integerVal1, integerVal2); - - validateExtractor(JsonUtils::extractOptionalIntList, - JsonUtils::extractRequiredIntList, - integers, - JsonArray::new, - createArray(stringVal, stringVal), - "list of integers"); - - validateExtractor(JsonUtils::extractOptionalIntList, - JsonUtils::extractRequiredIntList, - integers, - JsonArray::new, - stringVal, - "array"); - } - - @Test - public void extractDoubleList() throws Exception { - List doubles = Arrays.asList(doubleVal1, doubleVal2, Double.NaN); - - validateExtractor(JsonUtils::extractOptionalDoubleList, - JsonUtils::extractRequiredDoubleList, - doubles, - JsonArray::new, - createArray(stringVal, stringVal), - "list of doubles"); - - validateExtractor(JsonUtils::extractOptionalDoubleList, - JsonUtils::extractRequiredDoubleList, - doubles, - JsonArray::new, - stringVal, - "array"); - } - - @Test - public void extractObjectListOfList() throws Exception { - List> lists = Arrays.asList(Arrays.asList(jsonObjectVal1, jsonObjectVal2), Collections.singletonList(jsonObjectVal3)); - - Function>, Object> listsToJsonArray = l -> l - .stream() - .map(JsonArray::new) - .collect(toJsonArray()); - - validateExtractor(JsonUtils::extractOptionalObjectListOfList, - JsonUtils::extractRequiredObjectListOfList, - lists, - listsToJsonArray, - createArray(stringVal, stringVal), - "list of object lists"); - - validateExtractor(JsonUtils::extractOptionalObjectListOfList, - JsonUtils::extractRequiredObjectListOfList, - lists, - listsToJsonArray, - createArray(jsonArrayOfIntegers), - "list of object lists"); - } - - @Test - public void convertsJsonArrayToObjectList() throws Exception { - assertThat(JsonUtils.convertToObjectList(jsonArrayOfJsonObjects), contains(jsonObjectVal1, jsonObjectVal2)); - - expect(() -> JsonUtils.convertToObjectList(jsonArrayOfIntegers)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("JSON array is not a collection of expected types."); - - expect(() -> JsonUtils.convertToObjectList(jsonArrayOfMixed)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("JSON array is not a collection of expected types."); - } - - @Test - public void convertsJsonArrayToList() throws Exception { - assertThat(JsonUtils.convertToList(jsonArrayOfJsonObjects, JsonArray::getJsonObject), contains(jsonObjectVal1, jsonObjectVal2)); - assertThat(JsonUtils.convertToList(jsonArrayOfIntegers, JsonArray::getInteger), contains(integerVal1, integerVal2, integerVal3)); - assertThat(JsonUtils.convertToList(jsonArrayOfMixed, JsonArray::getValue), contains(jsonObjectVal1, integerVal2)); - - expect(() -> JsonUtils.convertToList(jsonArrayOfJsonObjects, JsonArray::getInteger)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("JSON array is not a collection of expected types."); - - expect(() -> JsonUtils.convertToList(jsonArrayOfIntegers, JsonArray::getJsonObject)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("JSON array is not a collection of expected types."); - - expect(() -> JsonUtils.convertToList(jsonArrayOfMixed, JsonArray::getDouble)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("JSON array is not a collection of expected types."); - } - - @Test - public void convertsJsonArrayToListWithExpectedCount() throws Exception { - assertThat(JsonUtils.convertToList(jsonArrayOfJsonObjects, JsonArray::getJsonObject, jsonArrayOfJsonObjects.size()), contains(jsonObjectVal1, jsonObjectVal2)); - assertThat(JsonUtils.convertToList(jsonArrayOfIntegers, JsonArray::getInteger, jsonArrayOfIntegers.size()), contains(integerVal1, integerVal2, integerVal3)); - assertThat(JsonUtils.convertToList(jsonArrayOfMixed, JsonArray::getValue, jsonArrayOfMixed.size()), contains(jsonObjectVal1, integerVal2)); - - expect(() -> JsonUtils.convertToList(jsonArrayOfJsonObjects, JsonArray::getJsonObject, 3)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("Invalid number of records in list. Expected exactly 3, found 2."); - - expect(() -> JsonUtils.convertToList(jsonArrayOfIntegers, JsonArray::getJsonObject, 2)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("Invalid number of records in list. Expected exactly 2, found 3."); - - expect(() -> JsonUtils.convertToList(jsonArrayOfMixed, JsonArray::getInteger)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("JSON array is not a collection of expected types."); - } - - private JsonObject createObjectWithValue(@Nullable Object value) { - JsonObject json = new JsonObject(); - if (value != null) - json.put(VALID_KEY, value); - else - json.putNull(VALID_KEY); - return new JsonObject(json.encode()); - } - - private JsonArray createArray(@Nullable Object... values) { - JsonArray json = new JsonArray(); - if (values != null) - Stream.of(values).forEach(json::add); - else - json.addNull(); - return new JsonArray(json.encode()); - } - - private void validateExtractor(OptionalValueExtractor optionalValueExtractor, - ValueExtractor requiredValueExtractor, - T expectedValue) throws Exception { - validateExtractor(optionalValueExtractor, requiredValueExtractor, expectedValue, value -> value); - } - - private void validateExtractor(OptionalValueExtractor optionalValueExtractor, - ValueExtractor requiredValueExtractor, - T expectedValue, - Function valueConverter) throws Exception { - JsonObject validObject = createObjectWithValue(valueConverter.apply(expectedValue)); - - assertThat(optionalValueExtractor.extract(validObject, VALID_KEY).orElseThrow(AssertionError::new), equalTo(expectedValue)); - assertThat(optionalValueExtractor.extract(validObject, MISSING_KEY).isPresent(), equalTo(false)); - - assertThat(requiredValueExtractor.extract(validObject, VALID_KEY), equalTo(expectedValue)); - - expect(() -> requiredValueExtractor.extract(validObject, MISSING_KEY)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage("No value found for required key 'key2'."); - - } - - private void validateExtractor(OptionalValueExtractor optionalValueExtractor, - ValueExtractor requiredValueExtractor, - T expectedValue, - Object invalidValue, - String description) throws Exception { - validateExtractor(optionalValueExtractor, requiredValueExtractor, expectedValue, value -> value, invalidValue, description); - } - - private void validateExtractor(OptionalValueExtractor optionalValueExtractor, - ValueExtractor requiredValueExtractor, - T expectedValue, - Function valueConverter, - Object invalidValue, - String description) throws Exception { - validateExtractor(optionalValueExtractor, requiredValueExtractor, expectedValue, valueConverter); - - JsonObject invalidObject = createObjectWithValue(invalidValue); - - expect(() -> optionalValueExtractor.extract(invalidObject, VALID_KEY)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage(String.format("Value for 'key' is not a valid %s.", description)); - - expect(() -> requiredValueExtractor.extract(invalidObject, VALID_KEY)) - .toThrow(JsonUtils.ParsingException.class) - .withMessage(String.format("Value for 'key' is not a valid %s.", description)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/JsonUtilsTest.kt b/src/test/java/com/zepben/vertxutils/json/JsonUtilsTest.kt new file mode 100644 index 0000000..f62f280 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/JsonUtilsTest.kt @@ -0,0 +1,431 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.json.JsonUtils.convertToList +import com.zepben.vertxutils.json.JsonUtils.convertToObjectList +import com.zepben.vertxutils.json.JsonUtils.extractOptionalArray +import com.zepben.vertxutils.json.JsonUtils.extractOptionalDouble +import com.zepben.vertxutils.json.JsonUtils.extractOptionalDoubleList +import com.zepben.vertxutils.json.JsonUtils.extractOptionalInt +import com.zepben.vertxutils.json.JsonUtils.extractOptionalIntList +import com.zepben.vertxutils.json.JsonUtils.extractOptionalObject +import com.zepben.vertxutils.json.JsonUtils.extractOptionalObjectList +import com.zepben.vertxutils.json.JsonUtils.extractOptionalObjectListOfList +import com.zepben.vertxutils.json.JsonUtils.extractOptionalPath +import com.zepben.vertxutils.json.JsonUtils.extractOptionalString +import com.zepben.vertxutils.json.JsonUtils.extractOptionalStringList +import com.zepben.vertxutils.json.JsonUtils.extractOptionalValue +import com.zepben.vertxutils.json.JsonUtils.extractRequiredArray +import com.zepben.vertxutils.json.JsonUtils.extractRequiredDouble +import com.zepben.vertxutils.json.JsonUtils.extractRequiredDoubleList +import com.zepben.vertxutils.json.JsonUtils.extractRequiredInt +import com.zepben.vertxutils.json.JsonUtils.extractRequiredIntList +import com.zepben.vertxutils.json.JsonUtils.extractRequiredObject +import com.zepben.vertxutils.json.JsonUtils.extractRequiredObjectList +import com.zepben.vertxutils.json.JsonUtils.extractRequiredObjectListOfList +import com.zepben.vertxutils.json.JsonUtils.extractRequiredPath +import com.zepben.vertxutils.json.JsonUtils.extractRequiredString +import com.zepben.vertxutils.json.JsonUtils.extractRequiredStringList +import com.zepben.vertxutils.json.JsonUtils.extractRequiredValue +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import io.vertx.kotlin.core.json.array +import io.vertx.kotlin.core.json.json +import io.vertx.kotlin.core.json.obj +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import java.nio.file.Path +import java.nio.file.Paths + +class JsonUtilsTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + private const val VALID_KEY = "key" + private const val MISSING_KEY = "key2" + + } + + private val integerVal1 = 111 + private val integerVal2 = 222 + private val integerVal3 = 333 + private val doubleVal1 = 1.11 + private val doubleVal2 = 2.22 + private val stringVal = "value" + private val pathVal: Path = Paths.get("valid/path") + private val illegalPath = "illegal\u0000path" + + private val jsonObjectVal1: JsonObject = createObjectWithValue(stringVal) + private val jsonObjectVal2: JsonObject = createObjectWithValue(doubleVal2) + private val jsonObjectVal3: JsonObject = createObjectWithValue(illegalPath) + private val jsonArrayOfJsonObjects: JsonArray = createArray(jsonObjectVal1, jsonObjectVal2) + private val jsonArrayOfIntegers: JsonArray = createArray(integerVal1, integerVal2, integerVal3) + private val jsonArrayOfMixed: JsonArray = createArray(jsonObjectVal1, integerVal2) + + @Test + fun extractValue() { + validateExtractor( + { obj, json -> obj.extractOptionalValue(json) }, + { obj, json -> obj.extractRequiredValue(json) }, + jsonObjectVal1, + ) + validateExtractor( + { obj, json -> obj.extractOptionalValue(json) }, + { obj, json -> obj.extractRequiredValue(json) }, + integerVal1, + ) + validateExtractor( + { obj, json -> obj.extractOptionalValue(json) }, + { obj, json -> obj.extractRequiredValue(json) }, + stringVal, + ) + } + + @Test + fun extractObject() { + validateExtractor( + { obj, json -> obj.extractOptionalObject(json) }, + { obj, json -> obj.extractRequiredObject(json) }, + jsonObjectVal1, + stringVal, + "object", + ) + } + + @Test + fun extractArray() { + validateExtractor( + { obj, json -> obj.extractOptionalArray(json) }, + { obj, json -> obj.extractRequiredArray(json) }, + jsonArrayOfJsonObjects, + stringVal, + "array", + ) + validateExtractor( + { obj, json -> obj.extractOptionalArray(json) }, + { obj, json -> obj.extractRequiredArray(json) }, + jsonArrayOfIntegers, + stringVal, + "array", + ) + } + + @Test + fun extractString() { + validateExtractor( + { obj, json -> obj.extractOptionalString(json) }, + { obj, json -> obj.extractRequiredString(json) }, + stringVal, + integerVal1, + "string", + ) + } + + @Test + fun extractInt() { + validateExtractor( + { obj, json -> obj.extractOptionalInt(json) }, + { obj, json -> obj.extractRequiredInt(json) }, + integerVal1, + stringVal, + "integer", + ) + } + + @Test + fun extractDouble() { + validateExtractor( + { obj, json -> obj.extractOptionalDouble(json) }, + { obj, json -> obj.extractRequiredDouble(json) }, + doubleVal1, + stringVal, + "double", + ) + validateExtractor( + { obj, json -> obj.extractOptionalDouble(json) }, + { obj, json -> obj.extractRequiredDouble(json) }, + Double.NaN, + stringVal, + "double", + ) + } + + @Test + fun extractPath() { + validateExtractor( + { obj, json -> obj.extractOptionalPath(json) }, + { obj, json -> obj.extractRequiredPath(json) }, + pathVal, + doubleVal2, + "path", + ) { it.toString() } + validateExtractor( + { obj, json -> obj.extractOptionalPath(json) }, + { obj, json -> obj.extractRequiredPath(json) }, + pathVal, + illegalPath, + "path", + ) { it.toString() } + } + + @Test + fun extractObjectList() { + val jsonObjects = listOf(jsonObjectVal1, jsonObjectVal2) + + validateExtractor( + { obj, json -> obj.extractOptionalObjectList(json) }, + { obj, json -> obj.extractRequiredObjectList(json) }, + jsonObjects, + listOf(integerVal1, integerVal2), + "list of objects", + ) { list -> JsonArray(list) } + + validateExtractor( + { obj, json -> obj.extractOptionalObjectList(json) }, + { obj, json -> obj.extractRequiredObjectList(json) }, + jsonObjects, + stringVal, + "array", + ) { list -> JsonArray(list) } + } + + @Test + fun extractStringList() { + val strings = listOf(stringVal, stringVal) + + validateExtractor( + { obj, json -> obj.extractOptionalStringList(json) }, + { obj, json -> obj.extractRequiredStringList(json) }, + strings, + listOf(integerVal1, integerVal2), + "list of strings", + ) { list -> JsonArray(list) } + + validateExtractor( + { obj, json -> obj.extractOptionalStringList(json) }, + { obj, json -> obj.extractRequiredStringList(json) }, + strings, + stringVal, + "array", + ) { list -> JsonArray(list) } + } + + @Test + fun extractIntList() { + val integers = listOf(integerVal1, integerVal2) + + validateExtractor( + { obj, json -> obj.extractOptionalIntList(json) }, + { obj, json -> obj.extractRequiredIntList(json) }, + integers, + createArray(stringVal, stringVal), + "list of integers", + ) { list -> JsonArray(list) } + + validateExtractor( + { obj, json -> obj.extractOptionalIntList(json) }, + { obj, json -> obj.extractRequiredIntList(json) }, + integers, + stringVal, + "array", + ) { list -> JsonArray(list) } + } + + @Test + fun extractDoubleList() { + val doubles = listOf(doubleVal1, doubleVal2, Double.NaN) + + validateExtractor( + { obj, json -> obj.extractOptionalDoubleList(json) }, + { obj, json -> obj.extractRequiredDoubleList(json) }, + doubles, + createArray(stringVal, stringVal), + "list of doubles", + ) { list -> JsonArray(list) } + + validateExtractor( + { obj, json -> obj.extractOptionalDoubleList(json) }, + { obj, json -> obj.extractRequiredDoubleList(json) }, + doubles, + stringVal, + "array", + ) { list -> JsonArray(list) } + } + + @Test + fun extractObjectListOfList() { + val lists = listOf(listOf(jsonObjectVal1, jsonObjectVal2), listOf(jsonObjectVal3)) + + validateExtractor( + { obj, json -> obj.extractOptionalObjectListOfList(json) }, + { obj, json -> obj.extractRequiredObjectListOfList(json) }, + lists, + createArray(stringVal, stringVal), + "list of object lists", + ) { list -> JsonArray(list!!.map { JsonArray(it) }) } + + validateExtractor( + { obj, json -> obj.extractOptionalObjectListOfList(json) }, + { obj, json -> obj.extractRequiredObjectListOfList(json) }, + lists, + createArray(jsonArrayOfIntegers), + "list of object lists", + ) { list -> JsonArray(list!!.map { JsonArray(it) }) } + } + + @Test + fun convertsJsonArrayToObjectList() { + assertThat( + jsonArrayOfJsonObjects.convertToObjectList(), + contains(jsonObjectVal1, jsonObjectVal2), + ) + + expect { jsonArrayOfIntegers.convertToObjectList() } + .toThrow() + .withMessage("JSON array is not a collection of expected types.") + + expect { jsonArrayOfMixed.convertToObjectList() } + .toThrow() + .withMessage("JSON array is not a collection of expected types.") + } + + @Test + fun convertsJsonArrayToList() { + assertThat( + jsonArrayOfJsonObjects.convertToList { obj, pos -> obj.getJsonObject(pos) }, + contains(jsonObjectVal1, jsonObjectVal2), + ) + assertThat( + jsonArrayOfIntegers.convertToList { obj, pos -> obj.getInteger(pos) }, + contains(integerVal1, integerVal2, integerVal3), + ) + assertThat( + jsonArrayOfMixed.convertToList { obj, pos -> obj.getValue(pos) }, + contains(jsonObjectVal1, integerVal2), + ) + + expect { jsonArrayOfJsonObjects.convertToList { obj, pos -> obj.getInteger(pos) } } + .toThrow() + .withMessage("JSON array is not a collection of expected types.") + + expect { jsonArrayOfIntegers.convertToList { obj, pos -> obj.getJsonObject(pos) } } + .toThrow() + .withMessage("JSON array is not a collection of expected types.") + + expect { jsonArrayOfMixed.convertToList { obj, pos -> obj.getDouble(pos) } } + .toThrow() + .withMessage("JSON array is not a collection of expected types.") + } + + @Test + fun convertsJsonArrayToListWithExpectedCount() { + assertThat( + jsonArrayOfJsonObjects.convertToList( + jsonArrayOfJsonObjects.size(), + ) { arr, pos -> arr.getJsonObject(pos) }, + contains(jsonObjectVal1, jsonObjectVal2), + ) + assertThat( + jsonArrayOfIntegers.convertToList(jsonArrayOfIntegers.size()) { obj, pos -> obj.getInteger(pos) }, + contains(integerVal1, integerVal2, integerVal3), + ) + assertThat( + jsonArrayOfMixed.convertToList(jsonArrayOfMixed.size()) { obj, pos -> obj.getValue(pos) }, + contains(jsonObjectVal1, integerVal2), + ) + + expect { jsonArrayOfJsonObjects.convertToList(3) { obj, pos -> obj.getJsonObject(pos) } } + .toThrow() + .withMessage("Invalid number of records in list. Expected exactly 3, found 2.") + + expect { jsonArrayOfIntegers.convertToList(2) { obj, pos -> obj.getJsonObject(pos) } } + .toThrow() + .withMessage("Invalid number of records in list. Expected exactly 2, found 3.") + + expect { jsonArrayOfMixed.convertToList { obj, pos -> obj.getInteger(pos) } } + .toThrow() + .withMessage("JSON array is not a collection of expected types.") + } + + private fun createObjectWithValue(value: Any?): JsonObject = + json { + obj(VALID_KEY to value) + } + + private fun createArray(vararg values: Any?): JsonArray = + json { + array(*values) + } + + private fun validateExtractor( + optionalValueExtractor: (JsonObject, String) -> T?, + requiredValueExtractor: (JsonObject, String) -> T, + expectedValue: T?, + ) { + validateExtractor(optionalValueExtractor, requiredValueExtractor, expectedValue) { it } + } + + private fun validateExtractor( + optionalValueExtractor: (JsonObject, String) -> T?, + requiredValueExtractor: (JsonObject, String) -> T, + expectedValue: T?, + valueConverter: (T?) -> Any?, + ) { + val validObject = createObjectWithValue(valueConverter(expectedValue)) + + assertThat(optionalValueExtractor(validObject, VALID_KEY), equalTo(expectedValue)) + assertThat(optionalValueExtractor(validObject, MISSING_KEY), nullValue()) + + assertThat(requiredValueExtractor(validObject, VALID_KEY), equalTo(expectedValue)) + + expect { requiredValueExtractor(validObject, MISSING_KEY) } + .toThrow() + .withMessage("No value found for required key 'key2'.") + } + + private fun validateExtractor( + optionalValueExtractor: (JsonObject, String) -> T?, + requiredValueExtractor: (JsonObject, String) -> T, + expectedValue: T?, + invalidValue: Any, + description: String, + ) { + validateExtractor(optionalValueExtractor, requiredValueExtractor, expectedValue, invalidValue, description) { it } + } + + private fun validateExtractor( + optionalValueExtractor: (JsonObject, String) -> T?, + requiredValueExtractor: (JsonObject, String) -> T, + expectedValue: T?, + invalidValue: Any, + description: String, + valueConverter: (T?) -> Any?, + ) { + validateExtractor(optionalValueExtractor, requiredValueExtractor, expectedValue, valueConverter) + + val invalidObject = createObjectWithValue(invalidValue) + + expect { optionalValueExtractor(invalidObject, VALID_KEY) } + .toThrow() + .withMessage("Value for 'key' is not a valid $description.") + + expect { requiredValueExtractor(invalidObject, VALID_KEY) } + .toThrow() + .withMessage("Value for 'key' is not a valid $description.") + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/LazyJsonArrayTest.java b/src/test/java/com/zepben/vertxutils/json/LazyJsonArrayTest.java deleted file mode 100644 index 956e051..0000000 --- a/src/test/java/com/zepben/vertxutils/json/LazyJsonArrayTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import io.vertx.core.json.JsonArray; -import org.junit.jupiter.api.Test; - -import java.util.stream.IntStream; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; - -public class LazyJsonArrayTest { - - @Test - public void smokeTest() { - LazyJsonArray lja = new LazyJsonArray(() -> { - JsonArray jsonArray = new JsonArray(); - IntStream.range(0, 100).forEach(jsonArray::add); - return jsonArray; - }); - - assertThat(lja.size(), equalTo(100)); - assertThat(lja.getInteger(99), equalTo(99)); - - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/LazyJsonArrayTest.kt b/src/test/java/com/zepben/vertxutils/json/LazyJsonArrayTest.kt new file mode 100644 index 0000000..30c32ba --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/LazyJsonArrayTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.core.json.JsonArray +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class LazyJsonArrayTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun smokeTest() { + val lja = LazyJsonArray { + JsonArray(listOf(*(0..<100).toList().toTypedArray())) + } + + assertThat(lja.size(), equalTo(100)) + assertThat(lja.getInteger(99), equalTo(99)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/LazyJsonObjectTest.java b/src/test/java/com/zepben/vertxutils/json/LazyJsonObjectTest.java deleted file mode 100644 index 8ed697a..0000000 --- a/src/test/java/com/zepben/vertxutils/json/LazyJsonObjectTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json; - -import io.vertx.core.json.JsonObject; -import org.apache.commons.lang3.mutable.MutableInt; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; - -public class LazyJsonObjectTest { - - private static final String KEY1 = "pi"; - private static final String KEY2 = "zepben"; - private static final double VALUE1 = Math.PI; - private static final String VALUE2 = "Zeppelin Bend"; - - @Test - public void lazyObjectTest() { - LazyJsonObject ljo = new LazyJsonObject(() -> { - JsonObject jsonObject = new JsonObject(); - jsonObject.put(KEY1, VALUE1); - jsonObject.put(KEY2, VALUE2); - return jsonObject; - }); - - String encoded = ljo.encode(); - JsonObject decoded = new JsonObject(encoded); - - assertThat(decoded.getDouble(KEY1), equalTo(VALUE1)); - assertThat(decoded.getString(KEY2), equalTo(VALUE2)); - } - - @Test - public void lazyIncludedMemberTest() { - final MutableInt callCount = new MutableInt(0); - - LazyJsonObject jsonObject = new LazyJsonObject(); - jsonObject.put(KEY1, () -> { - callCount.increment(); - return VALUE1; - }); - jsonObject.put(KEY2, VALUE2); - - String encoded = jsonObject.encode(); - JsonObject decoded = new JsonObject(encoded); - - assertThat(decoded.getDouble(KEY1), equalTo(VALUE1)); - assertThat(decoded.getString(KEY2), equalTo(VALUE2)); - assertThat(callCount.getValue(), equalTo(1)); - } - - @Test - public void lazyExcludedMemberTest() { - final MutableInt callCount = new MutableInt(0); - - LazyJsonObject jsonObject = new LazyJsonObject(); - jsonObject.put(KEY1, () -> { - callCount.increment(); - return VALUE1; - }); - jsonObject.put(KEY2, VALUE2); - - jsonObject.remove("pi"); - - String encoded = jsonObject.encode(); - JsonObject decoded = new JsonObject(encoded); - - assertThat(decoded.getDouble(KEY1), is(nullValue())); - assertThat(decoded.getString(KEY2), equalTo(VALUE2)); - assertThat(callCount.getValue(), equalTo(0)); - } - - @Test - public void lazyObjectWithIncludedLazyMemberTest() { - final MutableInt callCount = new MutableInt(0); - - LazyJsonObject ljo = new LazyJsonObject(() -> { - LazyJsonObject jsonObject = new LazyJsonObject(); - jsonObject.put(KEY1, () -> { - callCount.increment(); - return VALUE1; - }); - jsonObject.put(KEY2, VALUE2); - return jsonObject; - }); - - String encoded = ljo.encode(); - JsonObject decoded = new JsonObject(encoded); - - assertThat(decoded.getDouble(KEY1), equalTo(VALUE1)); - assertThat(decoded.getString(KEY2), equalTo(VALUE2)); - - assertThat(callCount.getValue(), equalTo(1)); - } - - @Test - public void lazyObjectWithExcludedLazyMemberTest() { - final MutableInt callCount = new MutableInt(0); - - LazyJsonObject ljo = new LazyJsonObject(() -> { - LazyJsonObject jsonObject = new LazyJsonObject(); - jsonObject.put(KEY1, () -> { - callCount.increment(); - return VALUE1; - }); - jsonObject.put(KEY2, VALUE2); - return jsonObject; - }); - - ljo.remove("pi"); - - String encoded = ljo.encode(); - - JsonObject decoded = new JsonObject(encoded); - - assertThat(decoded.getDouble(KEY1), is(nullValue())); - assertThat(decoded.getString(KEY2), equalTo(VALUE2)); - - assertThat(callCount.getValue(), equalTo(0)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/LazyJsonObjectTest.kt b/src/test/java/com/zepben/vertxutils/json/LazyJsonObjectTest.kt new file mode 100644 index 0000000..0893bc7 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/LazyJsonObjectTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json + +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.core.json.JsonObject +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class LazyJsonObjectTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + private const val KEY1 = "pi" + private const val KEY2 = "zepben" + private const val VALUE1 = Math.PI + private const val VALUE2 = "Zeppelin Bend" + } + + @Test + fun lazyObjectTest() { + val ljo = LazyJsonObject { + val jsonObject = JsonObject() + jsonObject.put(KEY1, VALUE1) + jsonObject.put(KEY2, VALUE2) + jsonObject + } + + val encoded = ljo.encode() + val decoded = JsonObject(encoded) + + assertThat(decoded.getDouble(KEY1), equalTo(VALUE1)) + assertThat(decoded.getString(KEY2), equalTo(VALUE2)) + } + + @Test + fun lazyIncludedMemberTest() { + var callCount = 0 + + val jsonObject = LazyJsonObject() + jsonObject.put(KEY1) { + ++callCount + VALUE1 + } + jsonObject.put(KEY2, VALUE2) + + val encoded = jsonObject.encode() + val decoded = JsonObject(encoded) + + assertThat(decoded.getDouble(KEY1), equalTo(VALUE1)) + assertThat(decoded.getString(KEY2), equalTo(VALUE2)) + assertThat(callCount, equalTo(1)) + } + + @Test + fun lazyExcludedMemberTest() { + var callCount = 0 + + val jsonObject = LazyJsonObject() + jsonObject.put(KEY1) { + ++callCount + VALUE1 + } + jsonObject.put(KEY2, VALUE2) + + jsonObject.remove(KEY1) + + val encoded = jsonObject.encode() + val decoded = JsonObject(encoded) + + assertThat(decoded.getDouble(KEY1), nullValue()) + assertThat(decoded.getString(KEY2), equalTo(VALUE2)) + assertThat(callCount, equalTo(0)) + } + + @Test + fun lazyObjectWithIncludedLazyMemberTest() { + var callCount = 0 + + val ljo = LazyJsonObject { + val jsonObject = LazyJsonObject() + jsonObject.put(KEY1) { + ++callCount + VALUE1 + } + jsonObject.put(KEY2, VALUE2) + jsonObject + } + + val encoded = ljo.encode() + val decoded = JsonObject(encoded) + + assertThat(decoded.getDouble(KEY1), equalTo(VALUE1)) + assertThat(decoded.getString(KEY2), equalTo(VALUE2)) + + assertThat(callCount, equalTo(1)) + } + + @Test + fun lazyObjectWithExcludedLazyMemberTest() { + var callCount = 0 + + val ljo = LazyJsonObject { + val jsonObject = LazyJsonObject() + jsonObject.put(KEY1) { + ++callCount + VALUE1 + } + jsonObject.put(KEY2, VALUE2) + jsonObject + } + + ljo.remove(KEY1) + + val encoded = ljo.encode() + + val decoded = JsonObject(encoded) + + assertThat(decoded.getDouble(KEY1), nullValue()) + assertThat(decoded.getString(KEY2), equalTo(VALUE2)) + + assertThat(callCount, equalTo(0)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/FilterSpecificationTest.java b/src/test/java/com/zepben/vertxutils/json/filter/FilterSpecificationTest.java deleted file mode 100644 index 1985fcc..0000000 --- a/src/test/java/com/zepben/vertxutils/json/filter/FilterSpecificationTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter; - -import com.zepben.testutils.junit.SystemLogExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -public class FilterSpecificationTest { - - @RegisterExtension static SystemLogExtension systemOut = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess(); - - @Test - public void parsesCorrectly() throws Exception { - String filter = "key1(key11.key111,key12.key121)"; - validateFilter(filter, filter); - - validateFilter("key1.key11,key1.key12", "key1(key11,key12)"); - } - - private void validateFilter(String filter, String expected) throws FilterException { - FilterSpecification filterSpecification = new FilterSpecification(filter); - assertThat(filterSpecification.toString(), equalTo(expected)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/FilterSpecificationTest.kt b/src/test/java/com/zepben/vertxutils/json/filter/FilterSpecificationTest.kt new file mode 100644 index 0000000..ccdd4bc --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/filter/FilterSpecificationTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter + +import com.zepben.testutils.junit.SystemLogExtension +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class FilterSpecificationTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun parsesCorrectly() { + val filter = "key1(key11.key111,key12.key121)" + validateFilter(filter, filter) + + validateFilter("key1.key11,key1.key12", "key1(key11,key12)") + } + + private fun validateFilter(filter: String, expected: String?) { + val filterSpecification = FilterSpecification(filter) + assertThat(filterSpecification.toString(), equalTo(expected)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/JsonFilterTest.java b/src/test/java/com/zepben/vertxutils/json/filter/JsonFilterTest.java deleted file mode 100644 index f2c148f..0000000 --- a/src/test/java/com/zepben/vertxutils/json/filter/JsonFilterTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.json.JsonObject; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.Optional; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; - -@EverythingIsNonnullByDefault -public class JsonFilterTest { - - @Test - public void loadKvnetTest() throws FilterException, IOException { - String testDataFile = "load/load-complete.json"; - String pattern = "-results(series(energy(maximums(kwIn,kwOut,kwNet,pf),readings(values(kwIn,kwOut,kwNet,pf)))))"; - String expectedOutputFileName = "load/load-kvnet.json"; - testFilterSpecification(testDataFile, pattern, expectedOutputFileName); - } - - @Test - public void networkNoConnectivityTest() throws FilterException, IOException { - String testDataFile = "network/network-complete.json"; - String pattern = "-feeders(assets(connections))"; - String expectedOutputFileName = "network/network-no-connectivity.json"; - testFilterSpecification(testDataFile, pattern, expectedOutputFileName); - } - - @Test - public void networkSimpleConnectivityTest() throws FilterException, IOException { - String testDataFile = "network/network-complete.json"; - String pattern = "-feeders(assets(connections(numCores,normalDirections,currentDirections,currentPhases,normalPhases),siteId,loadId,feeder,lngLat,lngLats))"; - String expectedOutputFileName = "network/network-simple-connectivity.json"; - testFilterSpecification(testDataFile, pattern, expectedOutputFileName); - } - - @Test - public void networkMinimalDetailsTest() throws FilterException, IOException { - String testDataFile = "network/network-complete.json"; - String pattern = "-feeders(assets(type,siteId,loadId,voltage,connections,feeder,length))"; - String expectedOutputFileName = "network/network-minimal-details.json"; - testFilterSpecification(testDataFile, pattern, expectedOutputFileName); - } - - @Test - public void includeFilterField3Field4Test() throws FilterException, IOException { - String testDataFile = "abcdefg/abcdefg-complete.json"; - String pattern = "b(e(field3,field4))"; - String expectedOutputFileName = "abcdefg/abcdefg-field3-field4.json"; - testFilterSpecification(testDataFile, pattern, expectedOutputFileName); - } - - @Test - public void includeFilterFieldXFieldYTest() throws FilterException, IOException { - String testDataFile = "abcdefg/abcdefg-complete.json"; - String pattern = "c(g(fieldX,fieldY))"; - String expectedOutputFileName = "abcdefg/abcdefg-fieldX-fieldY.json"; - testFilterSpecification(testDataFile, pattern, expectedOutputFileName); - } - - @Test - public void testSubfilter() throws FilterException { - String pattern = "-feeders(assets(connections(numCores,normalDirections,currentDirections,currentPhases,normalPhases),siteId,loadId,feeder,lngLat,lngLats))"; - FilterSpecification fs = new FilterSpecification(pattern); - FilterSpecification subfs = fs.getSubfilter("feeders.assets.connections").orElseThrow(AssertionError::new); - String subFilterPattern = subfs.toString(); - assertThat(subFilterPattern, equalTo("connections(currentDirections,currentPhases,normalDirections,normalPhases,numCores)")); - - assertThat(fs.getSubfilter("feeders.blah"), equalTo(Optional.empty())); - } - - - private void testFilterSpecification(String testDataFile, - String pattern, - String expectedOutputFileName) throws IOException, FilterException { - - // Load the test data. - JsonObject testData = new JsonObject(IOUtils.toString(getClass().getResourceAsStream(testDataFile), UTF_8)); - - // Create the specification - FilterSpecification filterSpecification = new FilterSpecification(pattern); - - // Filter it - JsonObjectFilter.applyFilter(testData, filterSpecification); - - // Load the expected outcome. - JsonObject expected = new JsonObject(IOUtils.toString(getClass().getResourceAsStream(expectedOutputFileName), UTF_8)); - - // Assert that they're the same - assertThat(testData, equalTo(expected)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/JsonFilterTest.kt b/src/test/java/com/zepben/vertxutils/json/filter/JsonFilterTest.kt new file mode 100644 index 0000000..43a5c9a --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/filter/JsonFilterTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.json.filter.JsonObjectFilter.Companion.applyFilter +import io.vertx.core.json.JsonObject +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class JsonFilterTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun loadKvnetTest() { + testFilterSpecification( + testDataFile = "load/load-complete.json", + pattern = "-results(series(energy(maximums(kwIn,kwOut,kwNet,pf),readings(values(kwIn,kwOut,kwNet,pf)))))", + expectedOutputFileName = "load/load-kvnet.json", + ) + } + + @Test + fun networkNoConnectivityTest() { + testFilterSpecification( + testDataFile = "network/network-complete.json", + pattern = "-feeders(assets(connections))", + expectedOutputFileName = "network/network-no-connectivity.json", + ) + } + + @Test + fun networkSimpleConnectivityTest() { + testFilterSpecification( + testDataFile = "network/network-complete.json", + pattern = "-feeders(assets(connections(numCores,normalDirections,currentDirections,currentPhases,normalPhases),siteId,loadId,feeder,lngLat,lngLats))", + expectedOutputFileName = "network/network-simple-connectivity.json", + ) + } + + @Test + fun networkMinimalDetailsTest() { + testFilterSpecification( + testDataFile = "network/network-complete.json", + pattern = "-feeders(assets(type,siteId,loadId,voltage,connections,feeder,length))", + expectedOutputFileName = "network/network-minimal-details.json", + ) + } + + @Test + fun includeFilterField3Field4Test() { + testFilterSpecification( + testDataFile = "abcdefg/abcdefg-complete.json", + pattern = "b(e(field3,field4))", + expectedOutputFileName = "abcdefg/abcdefg-field3-field4.json", + ) + } + + @Test + fun includeFilterFieldXFieldYTest() { + testFilterSpecification( + testDataFile = "abcdefg/abcdefg-complete.json", + pattern = "c(g(fieldX,fieldY))", + expectedOutputFileName = "abcdefg/abcdefg-fieldX-fieldY.json", + ) + } + + @Test + fun testSubfilter() { + val fs = FilterSpecification( + "-feeders(assets(connections(numCores,normalDirections,currentDirections,currentPhases,normalPhases),siteId,loadId,feeder,lngLat,lngLats))", + ) + + assertThat( + fs.getSubfilter("feeders.assets.connections").toString(), + equalTo("connections(currentDirections,currentPhases,normalDirections,normalPhases,numCores)"), + ) + + assertThat(fs.getSubfilter("feeders.blah"), nullValue()) + } + + private fun testFilterSpecification( + testDataFile: String, + pattern: String, + expectedOutputFileName: String, + ) { + // Load the test data. + val testData = JsonObject(javaClass.getResource(testDataFile)?.readText()) + + // Create the specification + val filterSpecification = FilterSpecification(pattern) + + // Filter it + applyFilter(testData, filterSpecification) + + // Load the expected outcome. + val expected = JsonObject(javaClass.getResource(expectedOutputFileName)?.readText()) + + // Assert that they're the same + assertThat(testData, equalTo(expected)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/JsonObjectFilterTest.java b/src/test/java/com/zepben/vertxutils/json/filter/JsonObjectFilterTest.java deleted file mode 100644 index 0f6d7d5..0000000 --- a/src/test/java/com/zepben/vertxutils/json/filter/JsonObjectFilterTest.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter; - -import com.zepben.testutils.junit.SystemLogExtension; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -public class JsonObjectFilterTest { - - @RegisterExtension - static SystemLogExtension systemOut = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess(); - - @Test - public void filtersObject() throws Exception { - JsonObject jsonObject = filteredObject("key1,key3(key31,key32)"); - - validateTopLevelKeys(jsonObject, true, false, true, false); - JsonObject key3 = jsonObject.getJsonObject("key3"); - - assertThat(key3.containsKey("key31"), equalTo(true)); - assertThat(key3.containsKey("key32"), equalTo(true)); - assertThat(key3.containsKey("key33"), equalTo(false)); - - jsonObject = filteredObject("key2,key4(key41,key43(key431,key433))"); - - validateTopLevelKeys(jsonObject, false, true, false, true); - validateKey4(jsonObject, true, true, false); - - jsonObject = filteredObject("key4(key43.key431,key44.key442)"); - - validateTopLevelKeys(jsonObject, false, false, false, true); - validateKey4(jsonObject, false, false, true); - - jsonObject = filteredObject("key1.value"); - validateTopLevelKeys(jsonObject, true, false, false, false); - - jsonObject = filteredObject("-key1.value"); - validateTopLevelKeys(jsonObject, true, true, true, true); - } - - private void validateKey4(JsonObject jsonObject, boolean expect41, boolean expect433, boolean expect44) { - JsonArray key4 = jsonObject.getJsonArray("key4"); - - assertThat(key4.size(), equalTo(2)); - validateArrayObjects(key4.getJsonArray(0), 3, 1, expect41, expect433, expect44); - validateArrayObjects(key4.getJsonArray(1), 1, 4, expect41, expect433, expect44); - } - - @Test - public void filtersDoNotRemoveEmptyArrays() throws Exception { - JsonObject jsonObject = createObject(); - jsonObject.getJsonArray("key4").clear(); - - JsonObjectFilter.applyFilter(jsonObject, new FilterSpecification("key4.key43.key431")); - - validateTopLevelKeys(jsonObject, false, false, false, true); - JsonArray key4 = jsonObject.getJsonArray("key4"); - - assertThat(key4.size(), equalTo(0)); - } - - @Test - public void isFluent() throws Exception { - JsonObject jsonObject = new JsonObject(); - assertThat(JsonObjectFilter.applyFilter(jsonObject, new FilterSpecification("test")), equalTo(jsonObject)); - } - - private void validateTopLevelKeys(JsonObject jsonObject, boolean expect1, boolean expect2, boolean expect3, boolean expect4) { - assertThat(jsonObject.containsKey("key1"), equalTo(expect1)); - assertThat(jsonObject.containsKey("key2"), equalTo(expect2)); - assertThat(jsonObject.containsKey("key3"), equalTo(expect3)); - assertThat(jsonObject.containsKey("key4"), equalTo(expect4)); - } - - private void validateArrayObjects(JsonArray jsonArray, int expectedSize, int startCount, boolean expect41, boolean expect433, boolean expect44) { - assertThat(jsonArray.size(), equalTo(expectedSize)); - - for (int i = 0; i < expectedSize; ++i) - validateArrayObject(jsonArray.getJsonObject(i), startCount + i, expect41, expect433, expect44); - } - - private void validateArrayObject(JsonObject jsonObject, double count, boolean expect41, boolean expect433, boolean expect44) { - assertThat(jsonObject.containsKey("key41"), equalTo(expect41)); - assertThat(jsonObject.containsKey("key42"), equalTo(false)); - assertThat(jsonObject.containsKey("key43"), equalTo(true)); - assertThat(jsonObject.containsKey("key44"), equalTo(expect44)); - - if (expect41) - assertThat(jsonObject.getDouble("key41"), equalTo(41 + (count / 10))); - - JsonObject key43 = jsonObject.getJsonObject("key43"); - - assertThat(key43.containsKey("key431"), equalTo(true)); - assertThat(key43.containsKey("key432"), equalTo(false)); - assertThat(key43.containsKey("key433"), equalTo(expect433)); - - assertThat(key43.getDouble("key431"), equalTo(431 + (count / 10))); - if (expect433) - assertThat(key43.getDouble("key433"), equalTo(433 + (count / 10))); - - if (expect44) { - JsonObject key44 = jsonObject.getJsonObject("key44"); - assertThat(key44.containsKey("key441"), equalTo(false)); - assertThat(key44.containsKey("key442"), equalTo(true)); - assertThat(key44.containsKey("key443"), equalTo(false)); - - assertThat(key44.getDouble("key442"), equalTo(442 + (count / 10))); - } - } - - private JsonObject filteredObject(String filter) throws Exception { - JsonObject jsonObject = createObject(); - JsonObjectFilter.applyFilter(jsonObject, new FilterSpecification(filter)); - return jsonObject; - } - - private JsonObject createObject() { - return new JsonObject() - .put("key1", 1) - .put("key2", "2") - .put("key3", createObject(3, 1)) - .put("key4", new JsonArray() - .add(new JsonArray() - .add(createObject(4, 1)) - .add(createObject(4, 2)) - .add(createObject(4, 3)) - ) - .add(new JsonArray() - .add(createObject(4, 4)) - ) - ); - } - - private JsonObject createObject(int key, double count) { - return new JsonObject() - .put("key" + key + "1", (key * 10) + 1 + (count / 10)) - .put("key" + key + "2", (key * 10) + 2 + (count / 10)) - .put("key" + key + "3", new JsonObject() - .put("key" + key + "31", (key * 100) + 31 + (count / 10)) - .put("key" + key + "32", (key * 100) + 32 + (count / 10)) - .put("key" + key + "33", (key * 100) + 33 + (count / 10)) - ) - .put("key" + key + "4", new JsonObject() - .put("key" + key + "41", (key * 100) + 41 + (count / 10)) - .put("key" + key + "42", (key * 100) + 42 + (count / 10)) - .put("key" + key + "43", (key * 100) + 43 + (count / 10)) - ); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/JsonObjectFilterTest.kt b/src/test/java/com/zepben/vertxutils/json/filter/JsonObjectFilterTest.kt new file mode 100644 index 0000000..5c51abe --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/filter/JsonObjectFilterTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.json.filter.JsonObjectFilter.Companion.applyFilter +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import io.vertx.kotlin.core.json.array +import io.vertx.kotlin.core.json.json +import io.vertx.kotlin.core.json.obj +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class JsonObjectFilterTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun filtersObject() { + var jsonObject = filteredObject("key1,key3(key31,key32)") + + validateTopLevelKeys(jsonObject, expect1 = true, expect2 = false, expect3 = true, expect4 = false) + val key3 = jsonObject.getJsonObject("key3") + + assertThat(key3.containsKey("key31"), equalTo(true)) + assertThat(key3.containsKey("key32"), equalTo(true)) + assertThat(key3.containsKey("key33"), equalTo(false)) + + jsonObject = filteredObject("key2,key4(key41,key43(key431,key433))") + + validateTopLevelKeys(jsonObject, expect1 = false, expect2 = true, expect3 = false, expect4 = true) + validateKey4(jsonObject, expect41 = true, expect433 = true, expect44 = false) + + jsonObject = filteredObject("key4(key43.key431,key44.key442)") + + validateTopLevelKeys(jsonObject, expect1 = false, expect2 = false, expect3 = false, expect4 = true) + validateKey4(jsonObject, expect41 = false, expect433 = false, expect44 = true) + + jsonObject = filteredObject("key1.value") + validateTopLevelKeys(jsonObject, expect1 = true, expect2 = false, expect3 = false, expect4 = false) + + jsonObject = filteredObject("-key1.value") + validateTopLevelKeys(jsonObject, expect1 = true, expect2 = true, expect3 = true, expect4 = true) + } + + private fun validateKey4(jsonObject: JsonObject, expect41: Boolean, expect433: Boolean, expect44: Boolean) { + val key4 = jsonObject.getJsonArray("key4") + + assertThat(key4.size(), equalTo(2)) + validateArrayObjects(key4.getJsonArray(0), 3, 1, expect41, expect433, expect44) + validateArrayObjects(key4.getJsonArray(1), 1, 4, expect41, expect433, expect44) + } + + @Test + fun filtersDoNotRemoveEmptyArrays() { + val jsonObject = createObject() + jsonObject.getJsonArray("key4").clear() + + applyFilter(jsonObject, FilterSpecification("key4.key43.key431")) + + validateTopLevelKeys(jsonObject, expect1 = false, expect2 = false, expect3 = false, expect4 = true) + val key4 = jsonObject.getJsonArray("key4") + + assertThat(key4.size(), equalTo(0)) + } + + @Test + fun isFluent() { + val jsonObject = JsonObject() + assertThat(applyFilter(jsonObject, FilterSpecification("test")), equalTo(jsonObject)) + } + + private fun validateTopLevelKeys(jsonObject: JsonObject, expect1: Boolean, expect2: Boolean, expect3: Boolean, expect4: Boolean) { + assertThat(jsonObject.containsKey("key1"), equalTo(expect1)) + assertThat(jsonObject.containsKey("key2"), equalTo(expect2)) + assertThat(jsonObject.containsKey("key3"), equalTo(expect3)) + assertThat(jsonObject.containsKey("key4"), equalTo(expect4)) + } + + private fun validateArrayObjects(jsonArray: JsonArray, expectedSize: Int, startCount: Int, expect41: Boolean, expect433: Boolean, expect44: Boolean) { + assertThat(jsonArray.size(), equalTo(expectedSize)) + + (0.. + validateArrayObject(jsonArray.getJsonObject(i), (startCount + i).toDouble(), expect41, expect433, expect44) + } + } + + private fun validateArrayObject(jsonObject: JsonObject, count: Double, expect41: Boolean, expect433: Boolean, expect44: Boolean) { + assertThat(jsonObject.containsKey("key41"), equalTo(expect41)) + assertThat(jsonObject.containsKey("key42"), equalTo(false)) + assertThat(jsonObject.containsKey("key43"), equalTo(true)) + assertThat(jsonObject.containsKey("key44"), equalTo(expect44)) + + if (expect41) + assertThat(jsonObject.getDouble("key41"), equalTo(41 + (count / 10))) + + val key43 = jsonObject.getJsonObject("key43") + + assertThat(key43.containsKey("key431"), equalTo(true)) + assertThat(key43.containsKey("key432"), equalTo(false)) + assertThat(key43.containsKey("key433"), equalTo(expect433)) + + assertThat(key43.getDouble("key431"), equalTo(431 + (count / 10))) + if (expect433) + assertThat(key43.getDouble("key433"), equalTo(433 + (count / 10))) + + if (expect44) { + val key44 = jsonObject.getJsonObject("key44") + assertThat(key44.containsKey("key441"), equalTo(false)) + assertThat(key44.containsKey("key442"), equalTo(true)) + assertThat(key44.containsKey("key443"), equalTo(false)) + + assertThat(key44.getDouble("key442"), equalTo(442 + (count / 10))) + } + } + + private fun filteredObject(filter: String): JsonObject = + createObject().also { + applyFilter(it, FilterSpecification(filter)) + } + + private fun createObject(): JsonObject { + return json { + obj( + "key1" to 1, + "key2" to "2", + "key3" to createObject(3, 1.0), + "key4" to array( + array( + createObject(4, 1.0), + createObject(4, 2.0), + createObject(4, 3.0), + ), + array( + createObject(4, 4.0), + ), + ), + ) + } + } + + private fun createObject(key: Int, count: Double): JsonObject { + return json { + obj( + "key" + key + "1" to (key * 10) + 1 + (count / 10), + "key" + key + "2" to (key * 10) + 2 + (count / 10), + "key" + key + "3" to obj( + "key" + key + "31" to (key * 100) + 31 + (count / 10), + "key" + key + "32" to (key * 100) + 32 + (count / 10), + "key" + key + "33" to (key * 100) + 33 + (count / 10), + ), + "key" + key + "4" to obj( + "key" + key + "41" to (key * 100) + 41 + (count / 10), + "key" + key + "42" to (key * 100) + 42 + (count / 10), + "key" + key + "43" to (key * 100) + 43 + (count / 10), + ), + ) + } + } + +} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/parser/ParserTest.java b/src/test/java/com/zepben/vertxutils/json/filter/parser/ParserTest.java deleted file mode 100644 index d626ad5..0000000 --- a/src/test/java/com/zepben/vertxutils/json/filter/parser/ParserTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.json.filter.parser; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.json.filter.FilterException; -import org.junit.jupiter.api.Test; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@EverythingIsNonnullByDefault -public class ParserTest { - - @Test - public void exceptions() { - expect(() -> Parser.parse("")) - .toThrow(FilterException.class) - .withMessage("Error parsing []. After [] expected one of [IDENTIFIER,DASH] but found []"); - - expect(() -> Parser.parse("asd(")) - .toThrow(FilterException.class) - .withMessage("Error parsing [asd(]. After [asd(] expected one of [IDENTIFIER] but found []"); - } - - @Test - public void simple1() throws FilterException { - Node node = Parser.parse("feeders"); - assertEquals("feeders", node.toString()); - assertEquals(2, node.countAllNodes()); - } - - @Test - public void simple2() throws FilterException { - Node node = Parser.parse("a.b"); - assertEquals("a.b", node.toString()); - assertEquals(3, node.countAllNodes()); - } - - @Test - public void exclude() throws FilterException { - Node node = Parser.parse("-a.b"); - assertEquals("-a.b", node.toString()); - assertEquals(3, node.countAllNodes()); - } - - @Test - public void medium1() throws FilterException { - Node node = Parser.parse("feeders(assets,feeder)"); - assertEquals("feeders(assets,feeder)", node.toString()); - assertEquals(4, node.countAllNodes()); - } - - @Test - public void medium2() throws FilterException { - Node node = Parser.parse("a(b,c),a.d"); - assertEquals("a(b,c,d)", node.toString()); - assertEquals(5, node.countAllNodes()); - } - - @Test - public void complex() throws FilterException { - Node node = Parser.parse("feeders(assets(id,isOpen,lngLat,name,symbol),feeder(id,name,state))"); - assertEquals("feeders(assets(id,isOpen,lngLat,name,symbol),feeder(id,name,state))", node.toString()); - assertEquals(12, node.countAllNodes()); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/json/filter/parser/ParserTest.kt b/src/test/java/com/zepben/vertxutils/json/filter/parser/ParserTest.kt new file mode 100644 index 0000000..d5f56f1 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/json/filter/parser/ParserTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.json.filter.parser + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.json.filter.FilterException +import com.zepben.vertxutils.json.filter.parser.Parser.parse +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class ParserTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun exceptions() { + expect { parse("") } + .toThrow() + .withMessage("Error parsing []. After [] expected one of [IDENTIFIER,DASH] but found []") + + expect { parse("asd(") } + .toThrow() + .withMessage("Error parsing [asd(]. After [asd(] expected one of [IDENTIFIER] but found []") + } + + @Test + fun simple1() { + val node = parse("feeders") + assertThat(node.toString(), equalTo("feeders")) + assertThat(node.countAllNodes(), equalTo(2)) + } + + @Test + fun simple2() { + val node = parse("a.b") + assertThat(node.toString(), equalTo("a.b")) + assertThat(node.countAllNodes(), equalTo(3)) + } + + @Test + fun exclude() { + val node = parse("-a.b") + assertThat(node.toString(), equalTo("-a.b")) + assertThat(node.countAllNodes(), equalTo(3)) + } + + @Test + fun medium1() { + val node = parse("feeders(assets,feeder)") + assertThat(node.toString(), equalTo("feeders(assets,feeder)")) + assertThat(node.countAllNodes(), equalTo(4)) + } + + @Test + fun medium2() { + val node = parse("a(b,c),a.d") + assertThat(node.toString(), equalTo("a(b,c,d)")) + assertThat(node.countAllNodes(), equalTo(5)) + } + + @Test + fun complex() { + val node = parse("feeders(assets(id,isOpen,lngLat,name,symbol),feeder(id,name,state))") + assertThat(node.toString(), equalTo("feeders(assets(id,isOpen,lngLat,name,symbol),feeder(id,name,state))")) + assertThat(node.countAllNodes(), equalTo(12)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponseTest.java b/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponseTest.java deleted file mode 100644 index 781eabc..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponseTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.ChunkedResponse; - -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; - -public class CaptureChunkedJsonResponseTest { - - @Test - public void captured() { - CaptureChunkedJsonResponse response = new CaptureChunkedJsonResponse(); - - ChunkedJsonResponse.JsonArray jsonArray = response.ofArray(); - jsonArray.addArrayItem("this is").send(false); - - assertThat(response.toString(), equalTo("[this is")); - - jsonArray.addArrayItem("my").send(false); - - assertThat(response.toString(), equalTo("[this is,my")); - - jsonArray.addArrayItem("test data").endArray(); - - assertThat(response.toString(), equalTo("[this is,my,test data]")); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponseTest.java b/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponseTest.java deleted file mode 100644 index 04ecc5a..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponseTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.ChunkedResponse; - -import org.junit.jupiter.api.Test; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; - -/** - * All tests in this suite call end on the response in order to capture the internal buffer for checking. - */ -public class ChunkedJsonResponseTest { - - private final ChunkedJsonResponse response = new CaptureChunkedJsonResponse(); - - @Test - public void beginResponseObject() { - response.ofObject().send(true); - assertThat(response.toString(), equalTo("{")); - - expect(response::ofObject).toThrow(IllegalStateException.class); - expect(response::ofArray).toThrow(IllegalStateException.class); - } - - @Test - public void beginResponseArray() { - response.ofArray().send(true); - assertThat(response.toString(), equalTo("[")); - - expect(response::ofObject).toThrow(IllegalStateException.class); - expect(response::ofArray).toThrow(IllegalStateException.class); - } - - @Test - public void endResponseObject() { - ChunkedJsonResponse.JsonObject jsonObject = response.ofObject().endObject(); - assertThat(response.toString(), equalTo("{}")); - - expect(jsonObject::endObject).toThrow(IllegalStateException.class); - } - - @Test - public void endResponseArray() { - ChunkedJsonResponse.JsonObject jsonObject = response.ofArray().endArray(); - assertThat(response.toString(), equalTo("[]")); - - expect(jsonObject::endObject).toThrow(IllegalStateException.class); - } - - @Test - public void generatesExpectedJson() { - response - .ofObject() - .beginObject("o1") - .addJson("o1k1", "0") - .addJson("o1k2", "\"text\"") - .beginArray("o1k3") - .beginArray() - .beginObject() - .endObjectInArray() - .endArrayInArray() - .endArray() - .beginArray("o1k4") - .endArray() - .endObject() - .beginObject("o2") - .beginObject("o3") - .endObject() - .beginArray("o2k1") - .addArrayItem("0") - .addArrayItem("1") - .addArrayItem("2") - .endArray() - .endObject() - .endObject(); - - String expected = "{\"o1\":{\"o1k1\":0,\"o1k2\":\"text\",\"o1k3\":[[{}]],\"o1k4\":[]},\"o2\":{\"o3\":{},\"o2k1\":[0,1,2]}}"; - - assertThat(response.toString(), equalTo(expected)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponseTest.java b/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponseTest.java deleted file mode 100644 index 1dabea0..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponseTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.ChunkedResponse; - -import io.vertx.core.http.HttpServerResponse; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.openMocks; - -public class HttpChunkedJsonResponseTest { - - @Mock - private HttpServerResponse httpServerResponse; - private AutoCloseable mockitoSession; - - @BeforeEach - public void setUp() { - mockitoSession = openMocks(this); - } - - @AfterEach - public void tearDown() throws Exception { - mockitoSession.close(); - } - - @Test - public void doEnd() { - doReturn(null).when(httpServerResponse).end(any(String.class)); - - // Check that ending the response sends the remaining buffer. - new HttpChunkedJsonResponse(httpServerResponse).ofArray().endArray(); - Mockito.verify(httpServerResponse).end("[]"); - } - - @Test - public void doSend() { - doReturn(null).when(httpServerResponse).write(any(String.class)); - - ChunkedJsonResponse.JsonArray jsonArray = new HttpChunkedJsonResponse(httpServerResponse, 10).ofArray(); - - // Check that a non forced send does nothing if the buffer is under sized. - jsonArray.addArrayItem("this").send(false); - Mockito.verify(httpServerResponse, never()).write("[this"); - - // Check that a forced send works even if the buffer is under sized. - jsonArray.addArrayItem("is").send(true); - Mockito.verify(httpServerResponse).write("[this,is"); - - // Check that the buffer has been reset and is again under sized. - jsonArray.addArrayItem("my").send(false); - Mockito.verify(httpServerResponse, never()).write(",my"); - - // Check that a non forced sends works once the buffer size is exceeded. - jsonArray.addArrayItem("test data").send(false); - Mockito.verify(httpServerResponse).write(",my,test data"); - } - - @Test - public void responseCheck() { - doReturn(true).when(httpServerResponse).closed(); - new HttpChunkedJsonResponse(httpServerResponse).ofArray().endArray(); - Mockito.verify(httpServerResponse, never()).end(any(String.class)); - - doReturn(false).when(httpServerResponse).closed(); - new HttpChunkedJsonResponse(httpServerResponse).ofArray().endArray(); - Mockito.verify(httpServerResponse).end("[]"); - - doReturn(true).when(httpServerResponse).closed(); - doReturn(null).when(httpServerResponse).write(any(String.class)); - ChunkedJsonResponse.JsonArray jsonArray = new HttpChunkedJsonResponse(httpServerResponse, 10).ofArray(); - - jsonArray.addArrayItem("this").send(false); - Mockito.verify(httpServerResponse, never()).write(any(String.class)); - - jsonArray.addArrayItem("is").send(true); - Mockito.verify(httpServerResponse, never()).write(any(String.class)); - - jsonArray.addArrayItem("my").send(false); - Mockito.verify(httpServerResponse, never()).write(any(String.class)); - - jsonArray.addArrayItem("test data").send(false); - Mockito.verify(httpServerResponse, never()).write(any(String.class)); - - doReturn(false).when(httpServerResponse).closed(); - jsonArray.endArray().send(true); - Mockito.verify(httpServerResponse).write("[this,is,my,test data]"); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/ErrorFormatterTest.java b/src/test/java/com/zepben/vertxutils/routing/ErrorFormatterTest.java deleted file mode 100644 index 9362f46..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/ErrorFormatterTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.google.common.net.HttpHeaders; -import com.google.common.net.MediaType; -import com.zepben.vertxutils.routing.handlers.UtilHandlers; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.*; - -@SuppressWarnings("UnstableApiUsage") -public class ErrorFormatterTest { - - @SuppressWarnings("ThrowableNotThrown") - @Test - public void defaultFailureHandler() { - RoutingContext context = mock(RoutingContext.class); - HttpServerResponse response = mock(HttpServerResponse.class, RETURNS_SELF); - doReturn(response).when(context).response(); - - Throwable failure = new RuntimeException("test"); - doReturn(failure).when(context).failure(); - UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER.handle(context); - - verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - verify(response).end(ErrorFormatter.asJson(failure.toString())); - } - - @Test - public void redirectNoTrailingToTrailing() { - RoutingContext context = mock(RoutingContext.class); - HttpServerRequest request = mock(HttpServerRequest.class); - HttpServerResponse response = mock(HttpServerResponse.class, RETURNS_SELF); - doReturn(request).when(context).request(); - doReturn(response).when(context).response(); - doReturn("/some/path/without/slash").when(request).path(); - doReturn("test=true").when(request).query(); - - UtilHandlers.REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER.handle(context); - - verify(response).putHeader("Location", "/some/path/without/slash/?test=true"); - verify(response).setStatusCode(301); - verify(response).end(); - } - - @Test - public void errorToJson() { - String err = "err"; - String actual = ErrorFormatter.asJson(err); - String expected = new JsonObject().put("errors", Collections.singletonList(err)).encode(); - assertThat(actual, equalTo(expected)); - } - - @Test - public void errorsToJson() { - List errs = Arrays.asList("err1", "err2"); - String actual = ErrorFormatter.asJson(errs); - String expected = new JsonObject().put("errors", errs).encode(); - assertThat(actual, equalTo(expected)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/ErrorFormatterTest.kt b/src/test/java/com/zepben/vertxutils/routing/ErrorFormatterTest.kt new file mode 100644 index 0000000..31c4a91 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/ErrorFormatterTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.google.common.net.HttpHeaders +import com.google.common.net.MediaType +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.ErrorFormatter.asJson +import com.zepben.vertxutils.routing.handlers.UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER +import com.zepben.vertxutils.routing.handlers.UtilHandlers.REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER +import io.vertx.core.http.HttpServerRequest +import io.vertx.core.http.HttpServerResponse +import io.vertx.core.json.JsonObject +import io.vertx.ext.web.RoutingContext +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.* + +class ErrorFormatterTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun defaultFailureHandler() { + val context = mock() + val response = mock(RETURNS_SELF) + doReturn(response).`when`(context).response() + + val failure: Throwable = RuntimeException("test") + doReturn(failure).`when`(context).failure() + CATCH_ALL_API_FAILURE_HANDLER.handle(context) + + verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + verify(response).end(asJson(failure.toString())) + } + + @Test + fun redirectNoTrailingToTrailing() { + val context = mock(RoutingContext::class.java) + val request = mock() + val response = mock(RETURNS_SELF) + doReturn(request).`when`(context).request() + doReturn(response).`when`(context).response() + doReturn("/some/path/without/slash").`when`(request).path() + doReturn("test=true").`when`(request).query() + + REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER.handle(context) + + verify(response).putHeader("Location", "/some/path/without/slash/?test=true") + verify(response).statusCode = 301 + verify(response).end() + } + + @Test + fun errorToJson() { + val err = "err" + val actual = asJson(err) + val expected = JsonObject().put("errors", listOf(err)).encode() + assertThat(actual, equalTo(expected)) + } + + @Test + fun errorsToJson() { + val errs = listOf("err1", "err2") + val actual = asJson(errs) + val expected = JsonObject().put("errors", errs).encode() + assertThat(actual, equalTo(expected)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/ExceptionHandlerTest.java b/src/test/java/com/zepben/vertxutils/routing/ExceptionHandlerTest.java deleted file mode 100644 index 2d02303..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/ExceptionHandlerTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.function.BiConsumer; - -import static org.mockito.Mockito.*; - -@SuppressWarnings({"unchecked", "ThrowableNotThrown"}) -public class ExceptionHandlerTest { - - private final RoutingContext context = mock(RoutingContext.class); - - @Test - public void handlesExceptions() { - IOException ioEx = new IOException("test"); - BiConsumer ioExHandler = mock(BiConsumer.class); - doReturn(ioEx).when(context).failure(); - - ExceptionHandler handler = new ExceptionHandler<>(IOException.class, ioExHandler); - handler.handle(context); - - verify(ioExHandler).accept(ioEx, context); - verify(context, never()).next(); - } - - @Test - public void handlesNoFailure() { - new ExceptionHandler<>(RuntimeException.class, (t, c) -> {}).handle(context); - verify(context).next(); - } - - @Test - public void handlesNoMatch() { - doReturn(new RuntimeException()).when(context).failure(); - BiConsumer ioExHandler = mock(BiConsumer.class); - - ExceptionHandler handler = new ExceptionHandler<>(IOException.class, ioExHandler); - handler.handle(context); - - verify(ioExHandler, never()).accept(any(), any()); - verify(context).next(); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/ExceptionHandlerTest.kt b/src/test/java/com/zepben/vertxutils/routing/ExceptionHandlerTest.kt new file mode 100644 index 0000000..dafcd72 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/ExceptionHandlerTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.ext.web.RoutingContext +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.* +import org.mockito.kotlin.any +import java.io.IOException + +class ExceptionHandlerTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val context = mock() + + @Test + fun handlesExceptions() { + val ioEx = IOException("test") + val ioExHandler = mock<(IOException, RoutingContext?) -> Unit>() + doReturn(ioEx).`when`(context).failure() + + val handler = ExceptionHandler.of(ioExHandler) + handler.handle(context) + + verify(ioExHandler).invoke(ioEx, context) + verify(context, never()).next() + } + + @Test + fun handlesNoFailure() { + ExceptionHandler.of { _, _ -> }.handle(context) + verify(context).next() + } + + @Test + fun handlesNoMatch() { + doReturn(RuntimeException()).`when`(context).failure() + val ioExHandler = mock<(IOException, RoutingContext?) -> Unit>() + + val handler = ExceptionHandler.of(ioExHandler) + handler.handle(context) + + verify(ioExHandler, never()).invoke(any(), any()) + verify(context).next() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/JsonBodyRequestTest.java b/src/test/java/com/zepben/vertxutils/routing/JsonBodyRequestTest.java deleted file mode 100644 index ac63e2a..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/JsonBodyRequestTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import org.junit.jupiter.api.Test; - -import java.util.Objects; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; - -public class JsonBodyRequestTest { - - private final BlankJsonBodyRequest request = new BlankJsonBodyRequest(); - - @Test - public void extract() { - JsonObject jsonObject = new JsonObject() - .put("string", "value") - .put("int", 1) - .put("double", 2.2); - - assertThat(request.extract(jsonObject, "string", JsonObject::getString), equalTo("value")); - assertThat(request.extract(jsonObject, "int", JsonObject::getInteger), equalTo(1)); - assertThat(request.extract(jsonObject, "double", JsonObject::getDouble), equalTo(2.2)); - - expect(() -> request.extract(jsonObject, "string", JsonObject::getInteger)) - .toThrow(IllegalArgumentException.class) - .withMessage("Error reading required key 'string'"); - - expect(() -> request.extract(jsonObject, "fake", JsonObject::getValue)) - .toThrow(IllegalArgumentException.class) - .withMessage("Required key 'fake' must be specified"); - } - - @Test - public void extractList() { - JsonObject jsonObject = new JsonObject() - .put("objArray", new JsonArray() - .add(new JsonObject() - .put("id", 12) - .put("value", "test1")) - .add(new JsonObject() - .put("id", 34) - .put("value", "test2"))) - .put("emptyArray", new JsonArray()) - .put("double", 2.2); - - assertThat(request.extractList(jsonObject, "objArray", 2, this::fromJson), - contains(new TestDataPair(12, "test1"), new TestDataPair(34, "test2"))); - - expect(() -> request.extractList(jsonObject, "objArray", 3, this::fromJson)) - .toThrow(IllegalArgumentException.class) - .withMessage("Required key 'objArray' must have at least 3 values"); - - expect(() -> request.extractList(jsonObject, "emptyArray", 1, this::fromJson)) - .toThrow(IllegalArgumentException.class) - .withMessage("Required key 'emptyArray' must have at least 1 value"); - - expect(() -> request.extractList(jsonObject, "double", 3, this::fromJson)) - .toThrow(IllegalArgumentException.class) - .withMessage("Error reading required key 'double'"); - - expect(() -> request.extractList(jsonObject, "fake", 3, this::fromJson)) - .toThrow(IllegalArgumentException.class) - .withMessage("Required key 'fake' must be specified"); - } - - private TestDataPair fromJson(JsonObject jsonObject) { - return new TestDataPair(jsonObject.getInteger("id"), jsonObject.getString("value")); - } - - private static class BlankJsonBodyRequest implements JsonBodyRequest { - } - - private static class TestDataPair { - final int id; - final String value; - - TestDataPair(int id, String value) { - this.id = id; - this.value = value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof TestDataPair)) return false; - TestDataPair that = (TestDataPair) o; - return id == that.id && - Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(id, value); - } - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/JsonBodyRequestTest.kt b/src/test/java/com/zepben/vertxutils/routing/JsonBodyRequestTest.kt new file mode 100644 index 0000000..c009a65 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/JsonBodyRequestTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.core.json.JsonObject +import io.vertx.kotlin.core.json.array +import io.vertx.kotlin.core.json.json +import io.vertx.kotlin.core.json.obj +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class JsonBodyRequestTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val request = BlankJsonBodyRequest() + + @Test + fun extract() { + val jsonObject = json { + obj( + "string" to "value", + "int" to 1, + "double" to 2.2, + ) + } + + assertThat( + request.extract(jsonObject, "string") { obj, key -> obj.getString(key) }, + equalTo("value"), + ) + assertThat( + request.extract(jsonObject, "int") { obj, key -> obj.getInteger(key) }, + equalTo(1), + ) + assertThat( + request.extract(jsonObject, "double") { obj, key -> obj.getDouble(key) }, + equalTo(2.2), + ) + + expect { request.extract(jsonObject, "string") { obj, key -> obj.getInteger(key) } } + .toThrow() + .withMessage("Error reading required key 'string'") + + expect { request.extract(jsonObject, "fake") { obj, key -> obj.getValue(key) } } + .toThrow() + .withMessage("Required key 'fake' must be specified") + } + + @Test + fun extractList() { + val jsonObject = json { + obj( + "objArray" to array( + obj( + "id" to 12, + "value" to "test1", + ), + obj( + "id" to 34, + "value" to "test2", + ), + ), + "emptyArray" to array(), + "double" to 2.2, + ) + } + + assertThat( + request.extractList(jsonObject, "objArray", 2) { fromJson(it) }, + contains(TestDataPair(12, "test1"), TestDataPair(34, "test2")), + ) + + expect { request.extractList(jsonObject, "objArray", 3) { fromJson(it) } } + .toThrow() + .withMessage("Required key 'objArray' must have at least 3 values") + + expect { request.extractList(jsonObject, "emptyArray", 1) { fromJson(it) } } + .toThrow() + .withMessage("Required key 'emptyArray' must have at least 1 value") + + expect { request.extractList(jsonObject, "double", 3) { fromJson(it) } } + .toThrow() + .withMessage("Error reading required key 'double'") + + expect { request.extractList(jsonObject, "fake", 3) { fromJson(it) } } + .toThrow() + .withMessage("Required key 'fake' must be specified") + } + + private fun fromJson(jsonObject: JsonObject): TestDataPair { + return TestDataPair(jsonObject.getInteger("id"), jsonObject.getString("value")) + } + + private class BlankJsonBodyRequest : JsonBodyRequest + + private data class TestDataPair(val id: Int, val value: String) + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/RespondTest.java b/src/test/java/com/zepben/vertxutils/routing/RespondTest.java deleted file mode 100644 index f3c825a..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/RespondTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2025 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.google.common.net.HttpHeaders; -import com.google.common.net.MediaType; -import com.zepben.testutils.junit.SystemLogExtension; -import com.zepben.vertxutils.json.filter.FilterSpecification; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.util.Map; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.*; - -public class RespondTest { - - @RegisterExtension - static SystemLogExtension systemErr = SystemLogExtension.SYSTEM_ERR.captureLog().muteOnSuccess(); - - private final RoutingContext context = mock(RoutingContext.class); - private final HttpServerResponse response = mock(HttpServerResponse.class, RETURNS_SELF); - - @BeforeEach - public void setUp() { - doReturn(response).when(context).response(); - } - - @Test - public void withStatus() { - Respond.with(context, HttpResponseStatus.OK); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(response).end(); - } - - @Test - public void withStatusPlusHeaders() { - MultiMap existingHeaders = mock(MultiMap.class); - doReturn(existingHeaders).when(this.response).headers(); - Map addHeaders = Map.of("X-Test-Header", "value", "X-Test-Header-2", "value2"); - Respond.with(context, HttpResponseStatus.OK, addHeaders); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(existingHeaders).addAll(addHeaders); - verify(response).end(); - } - - @Test - public void withStatusPlusEmptyContentLengthHeader() { - MultiMap existingHeaders = mock(MultiMap.class); - doReturn(existingHeaders).when(this.response).headers(); - Map addHeaders = Map.of(HttpHeaders.CONTENT_LENGTH, "0"); - Respond.with(context, HttpResponseStatus.OK, true); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(existingHeaders).addAll(addHeaders); - verify(response).end(); - } - - @Test - public void withResponse() { - Response response = Response.ofText(HttpResponseStatus.OK, "test"); - MultiMap headers = mock(MultiMap.class); - doReturn(headers).when(this.response).headers(); - Respond.with(context, response); - verify(this.response).setStatusCode(HttpResponseStatus.OK.code()); - verify(headers).addAll(response.headers()); - verify(this.response).end(response.body()); - } - - @Test - public void withJson() { - Respond.withJson(context, HttpResponseStatus.OK, "json"); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - verify(response).end("json"); - } - - @Test - public void withJsonPlusHeaders() { - MultiMap existingHeaders = mock(MultiMap.class); - doReturn(existingHeaders).when(this.response).headers(); - Map addHeaders = Map.of("X-Test-Header", "value", "X-Test-Header-2", "value2"); - - Respond.withJson(context, HttpResponseStatus.OK, "json", addHeaders); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - verify(existingHeaders).addAll(addHeaders); - verify(response).end("json"); - } - - @Test - public void withJsonFilter() throws Exception { - FilterSpecification filterSpecification = new FilterSpecification("a.b"); - - JsonObject jsonObject = new JsonObject() - .put("a", new JsonObject() - .put("b", 1) - .put("c", 2)) - .put("d", 3); - - Respond.withJson(context, HttpResponseStatus.OK, jsonObject, filterSpecification); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - verify(response).end("{\"a\":{\"b\":1}}"); - } - - @Test - public void withJsonFilterPlusHeaders() throws Exception { - FilterSpecification filterSpecification = new FilterSpecification("a.b"); - - JsonObject jsonObject = new JsonObject() - .put("a", new JsonObject() - .put("b", 1) - .put("c", 2)) - .put("d", 3); - - MultiMap existingHeaders = mock(MultiMap.class); - doReturn(existingHeaders).when(this.response).headers(); - Map addHeaders = Map.of("X-Test-Header", "value", "X-Test-Header-2", "value2"); - - Respond.withJson(context, HttpResponseStatus.OK, jsonObject, filterSpecification, addHeaders); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - verify(existingHeaders).addAll(addHeaders); - verify(response).end("{\"a\":{\"b\":1}}"); - } - - @Test - public void withJsonChunked() { - HttpServerResponse returnedResponse = Respond.withJsonChunked(context, HttpResponseStatus.OK); - assertThat(returnedResponse, is(response)); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - verify(response).setChunked(true); - } - - @Test - public void withJsonChunkedPlusHeaders() { - MultiMap existingHeaders = mock(MultiMap.class); - doReturn(existingHeaders).when(this.response).headers(); - Map addHeaders = Map.of("X-Test-Header", "value", "X-Test-Header-2", "value2"); - - HttpServerResponse returnedResponse = Respond.withJsonChunked(context, HttpResponseStatus.OK, addHeaders); - assertThat(returnedResponse, is(response)); - verify(response).setStatusCode(HttpResponseStatus.OK.code()); - verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()); - verify(existingHeaders).addAll(addHeaders); - verify(response).setChunked(true); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/RespondTest.kt b/src/test/java/com/zepben/vertxutils/routing/RespondTest.kt new file mode 100644 index 0000000..56a1fce --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/RespondTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.google.common.net.HttpHeaders +import com.google.common.net.MediaType +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.json.filter.FilterSpecification +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.MultiMap +import io.vertx.core.http.HttpServerResponse +import io.vertx.core.json.JsonObject +import io.vertx.ext.web.RoutingContext +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.* + +class RespondTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val response = mock(RETURNS_SELF) + private val context = mock().also { + doReturn(response).`when`(it).response() + } + + @Test + fun withStatus() { + Respond.with(context, HttpResponseStatus.OK) + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(response).end() + } + + @Test + fun withStatusPlusHeaders() { + val existingHeaders = mock(MultiMap::class.java) + val addHeaders = mapOf("X-Test-Header" to "value", "X-Test-Header-2" to "value2") + doReturn(existingHeaders).`when`(this.response).headers() + + Respond.with(context, HttpResponseStatus.OK, addHeaders) + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(existingHeaders).addAll(addHeaders) + verify(response).end() + } + + @Test + fun withStatusPlusEmptyContentLengthHeader() { + val existingHeaders = mock(MultiMap::class.java) + doReturn(existingHeaders).`when`(this.response).headers() + + Respond.with(context, HttpResponseStatus.OK, withEmptyContentLengthHeader = true) + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(existingHeaders).set(HttpHeaders.CONTENT_LENGTH, "0") + verify(response).end() + } + + @Test + fun withResponse() { + val response = Response.ofText(HttpResponseStatus.OK, "test") + val headers = mock(MultiMap::class.java) + doReturn(headers).`when`(this.response).headers() + + Respond.with(context, response) + + verify(this.response).statusCode = HttpResponseStatus.OK.code() + verify(headers).addAll(response.headers) + verify(this.response).end(response.body) + } + + @Test + fun withJson() { + Respond.withJson(context, HttpResponseStatus.OK, "json") + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + verify(response).end("json") + } + + @Test + fun withJsonPlusHeaders() { + val existingHeaders = mock(MultiMap::class.java) + doReturn(existingHeaders).`when`(this.response).headers() + val addHeaders = mapOf("X-Test-Header" to "value", "X-Test-Header-2" to "value2") + + Respond.withJson(context, HttpResponseStatus.OK, "json", addHeaders) + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + verify(existingHeaders).addAll(addHeaders) + verify(response).end("json") + } + + @Test + fun withJsonFilter() { + val filterSpecification = FilterSpecification("a.b") + + val jsonObject = JsonObject() + .put( + "a", + JsonObject() + .put("b", 1) + .put("c", 2), + ) + .put("d", 3) + + Respond.withJson(context, HttpResponseStatus.OK, jsonObject, filterSpecification) + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + verify(response).end("{\"a\":{\"b\":1}}") + } + + @Test + fun withJsonFilterPlusHeaders() { + val filterSpecification = FilterSpecification("a.b") + + val jsonObject = JsonObject() + .put( + "a", + JsonObject() + .put("b", 1) + .put("c", 2), + ) + .put("d", 3) + + val existingHeaders = mock(MultiMap::class.java) + doReturn(existingHeaders).`when`(this.response).headers() + val addHeaders = mapOf("X-Test-Header" to "value", "X-Test-Header-2" to "value2") + + Respond.withJson(context, HttpResponseStatus.OK, jsonObject, filterSpecification, addHeaders) + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + verify(existingHeaders).addAll(addHeaders) + verify(response).end("{\"a\":{\"b\":1}}") + } + + @Test + fun withJsonChunked() { + val returnedResponse = Respond.withJsonChunked(context, HttpResponseStatus.OK) + + assertThat(returnedResponse, equalTo(response)) + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + verify(response).isChunked = true + } + + @Test + fun withJsonChunkedPlusHeaders() { + val existingHeaders = mock(MultiMap::class.java) + doReturn(existingHeaders).`when`(this.response).headers() + val addHeaders = mapOf("X-Test-Header" to "value", "X-Test-Header-2" to "value2") + + val returnedResponse = Respond.withJsonChunked(context, HttpResponseStatus.OK, addHeaders) + + assertThat(returnedResponse, equalTo(response)) + + verify(response).statusCode = HttpResponseStatus.OK.code() + verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + verify(existingHeaders).addAll(addHeaders) + verify(response).isChunked = true + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/ResponseTest.java b/src/test/java/com/zepben/vertxutils/routing/ResponseTest.java deleted file mode 100644 index c742887..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/ResponseTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.google.common.net.HttpHeaders; -import com.google.common.net.MediaType; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.buffer.Buffer; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -@SuppressWarnings("UnstableApiUsage") -public class ResponseTest { - - @Test - public void statusConstructor() { - Response response = new Response(HttpResponseStatus.OK); - assertThat(response.status(), is(HttpResponseStatus.OK)); - assertThat(response.body(), is(Buffer.buffer())); - } - - @Test - public void statusBodyConstructor() { - Buffer buffer = Buffer.buffer("test"); - Response response = new Response(HttpResponseStatus.OK, buffer); - assertThat(response.status(), is(HttpResponseStatus.OK)); - assertThat(response.body(), is(buffer)); - } - - @Test - public void setters() { - Buffer buffer = Buffer.buffer("test"); - Response response = new Response(HttpResponseStatus.OK); - response.setStatus(HttpResponseStatus.BAD_REQUEST) - .setBody(buffer); - - assertThat(response.status(), is(HttpResponseStatus.BAD_REQUEST)); - assertThat(response.body(), is(buffer)); - } - - @Test - public void ofJson() { - Response response = Response.ofJson(HttpResponseStatus.OK, "json"); - assertThat(response.status(), is(HttpResponseStatus.OK)); - assertThat(response.body(), is(Buffer.buffer("json"))); - assertThat(response.headers().get(HttpHeaders.CONTENT_TYPE), is(MediaType.JSON_UTF_8.toString())); - } - - @Test - public void ofText() { - Response response = Response.ofText(HttpResponseStatus.OK, "text"); - assertThat(response.status(), is(HttpResponseStatus.OK)); - assertThat(response.body(), is(Buffer.buffer("text"))); - assertThat(response.headers().get(HttpHeaders.CONTENT_TYPE), is(MediaType.PLAIN_TEXT_UTF_8.toString())); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/ResponseTest.kt b/src/test/java/com/zepben/vertxutils/routing/ResponseTest.kt new file mode 100644 index 0000000..85bd055 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/ResponseTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.google.common.net.HttpHeaders +import com.google.common.net.MediaType +import com.zepben.testutils.junit.SystemLogExtension +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.buffer.Buffer +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class ResponseTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun statusConstructor() { + val response = Response(HttpResponseStatus.OK, Buffer.buffer()) + + assertThat(response.status, equalTo(HttpResponseStatus.OK)) + assertThat(response.body, equalTo(Buffer.buffer())) + } + + @Test + fun statusBodyConstructor() { + val buffer = Buffer.buffer("test") + val response = Response(HttpResponseStatus.OK, buffer) + + assertThat(response.status, equalTo(HttpResponseStatus.OK)) + assertThat(response.body, equalTo(buffer)) + } + + @Test + fun ofJson() { + val response = Response.ofJson(HttpResponseStatus.OK, "json") + + assertThat(response.status, equalTo(HttpResponseStatus.OK)) + assertThat(response.body, equalTo(Buffer.buffer("json"))) + assertThat(response.headers[HttpHeaders.CONTENT_TYPE], equalTo(MediaType.JSON_UTF_8.toString())) + } + + @Test + fun ofText() { + val response = Response.ofText(HttpResponseStatus.OK, "text") + + assertThat(response.status, equalTo(HttpResponseStatus.OK)) + assertThat(response.body, equalTo(Buffer.buffer("text"))) + assertThat(response.headers[HttpHeaders.CONTENT_TYPE], equalTo(MediaType.PLAIN_TEXT_UTF_8.toString())) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteRegisterLoggerTest.java b/src/test/java/com/zepben/vertxutils/routing/RouteRegisterLoggerTest.java deleted file mode 100644 index 08bc298..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/RouteRegisterLoggerTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import io.vertx.core.http.HttpMethod; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; - -import static io.vertx.core.http.HttpMethod.GET; -import static io.vertx.core.http.HttpMethod.PUT; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.*; - -public class RouteRegisterLoggerTest { - - @Test - public void logsRouteDetails() { - Logger logger = mock(Logger.class); - - String[] paths = {"/my/path/1", "/my/path/2", "/my/other/3"}; - HttpMethod[][] methods = {{GET}, {PUT}, {GET, PUT}}; - assertThat(paths.length, equalTo(methods.length)); - - RouteRegisterLogger routeRegisterLogger = new RouteRegisterLogger(logger); - for (int i = 0; i < paths.length; ++i) { - routeRegisterLogger.accept("/mount" + paths[i], Route.builder().path(paths[i]).methods(methods[i]).build()); - - for (int j = 0; j < methods[i].length; ++j) - verify(logger, times(1)).info(methods[i][j] + ": /mount" + paths[i]); - } - - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteRegisterLoggerTest.kt b/src/test/java/com/zepben/vertxutils/routing/RouteRegisterLoggerTest.kt new file mode 100644 index 0000000..b7980dd --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/RouteRegisterLoggerTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.Route.Companion.builder +import io.vertx.core.http.HttpMethod +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.* +import org.slf4j.Logger + +class RouteRegisterLoggerTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun logsRouteDetails() { + val logger = mock() + + val paths = arrayOf("/my/path/1", "/my/path/2", "/my/other/3") + val methods = arrayOf( + arrayOf(HttpMethod.GET), + arrayOf(HttpMethod.PUT), + arrayOf(HttpMethod.GET, HttpMethod.PUT), + ) + assertThat(paths.size, equalTo(methods.size)) + + val routeRegisterLogger = logRegisteredRoutes(logger) + paths.forEachIndexed { i, path -> + routeRegisterLogger.invoke("/mount$path", builder().path(path).methods(*methods[i]).build()) + + methods[i].forEach { method -> + verify(logger, times(1)).info("$method: /mount$path") + } + } + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteRegisterTest.java b/src/test/java/com/zepben/vertxutils/routing/RouteRegisterTest.java deleted file mode 100644 index 2b78b8f..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/RouteRegisterTest.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import io.vertx.core.Handler; -import io.vertx.core.http.HttpMethod; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; - -import java.util.Arrays; -import java.util.List; -import java.util.function.BiConsumer; - -import static org.mockito.Mockito.*; - -public class RouteRegisterTest { - - private final Router router = mock(Router.class); - private final io.vertx.ext.web.Route vertxRoute = mock(io.vertx.ext.web.Route.class); - - private RouteRegister register = new RouteRegister(router, true); - - @BeforeEach - public void setUp() { - doReturn(vertxRoute).when(router).route(any()); - doReturn(vertxRoute).when(router).routeWithRegex(any()); - } - - @Test - public void addRoutes() { - Handler handler = c -> { - }; - Handler blockingHandler = c -> { - }; - Handler failureHandler = c -> { - }; - - Route route = Route.builder() - .path("/some/path") - .methods(HttpMethod.GET, HttpMethod.PUT) - .addHandler(handler) - .addBlockingHandler(blockingHandler) - .addFailureHandler(failureHandler) - .build(); - - register.add(route); - - verify(router).route("/some/path"); - verify(vertxRoute).method(HttpMethod.GET); - verify(vertxRoute).method(HttpMethod.PUT); - - InOrder inOrder = inOrder(vertxRoute, vertxRoute); - inOrder.verify(vertxRoute).handler(handler); - inOrder.verify(vertxRoute).blockingHandler(blockingHandler, true); - - verify(vertxRoute).failureHandler(failureHandler); - } - - @Test - public void addRoutesDefaultOrderedBlockingTrue() { - register = new RouteRegister(router, true); - - Handler blockingHandler = c -> { - }; - - Route route = Route.builder() - .path("/some/path") - .addBlockingHandler(blockingHandler) - .build(); - - register.add(route); - - verify(router).route("/some/path"); - verify(vertxRoute).blockingHandler(blockingHandler, true); - } - - @Test - public void addRoutesDefaultOrderedBlockingFalse() { - register = new RouteRegister(router, false); - - Handler blockingHandler = c -> { - }; - - Route route = Route.builder() - .path("/some/path") - .addBlockingHandler(blockingHandler) - .build(); - - register.add(route); - - verify(router).route("/some/path"); - verify(vertxRoute).blockingHandler(blockingHandler, false); - } - - @Test - public void addRoutesOrderedBlockingTrue() { - register = new RouteRegister(router, false); - - Handler blockingHandler = c -> { - }; - - Route route = Route.builder() - .path("/some/path") - .addBlockingHandler(blockingHandler, true) - .build(); - - register.add(route); - - verify(router).route("/some/path"); - verify(vertxRoute).blockingHandler(blockingHandler, true); - } - - @Test - public void addRoutesOrderedBlockingFalse() { - register = new RouteRegister(router, true); - - Handler blockingHandler = c -> { - }; - - Route route = Route.builder() - .path("/some/path") - .addBlockingHandler(blockingHandler, false) - .build(); - - register.add(route); - - verify(router).route("/some/path"); - verify(vertxRoute).blockingHandler(blockingHandler, false); - } - - @Test - public void usesMountPaths() { - register = new RouteRegister(router, "/rootMount/", true); - Route route = Route.builder() - .path("/some/path") - .build(); - - register.add(route, "/mount/"); - - verify(router).route("/rootMount/mount/some/path"); - } - - @Test - public void addRouteGroup() { - Route route1 = Route.builder().path("/route/1").build(); - Route route2 = Route.builder().path("/route/2").build(); - RouteGroup group = RouteGroup.create("/group", Arrays.asList(route1, route2)); - - register.add(group); - - verify(router).route("/group/route/1"); - verify(router).route("/group/route/2"); - } - - @Test - public void addRouteGroups() { - Route route1 = Route.builder().path("/route/1").build(); - Route route2 = Route.builder().path("/route/2").build(); - RouteGroup group1 = RouteGroup.create("/group1", List.of(route1)); - RouteGroup group2 = RouteGroup.create("/group2", List.of(route2)); - - register.addGroups(List.of(group1, group2)); - - verify(router).route("/group1/route/1"); - verify(router).route("/group2/route/2"); - } - - @Test - public void addsRegex() { - Route route = Route.builder() - .path("/some/regex/path") - .hasRegexPath(true) - .methods(HttpMethod.GET) - .build(); - - register.add(route); - - verify(router).routeWithRegex("/some/regex/path"); - } - - @Test - public void usesMountPathRegex() { - Route route = Route.builder() - .path("/some/regex/path") - .hasRegexPath(true) - .build(); - - register.add(route, "/mount"); - - verify(router).routeWithRegex("/mount/some/regex/path"); - } - - @Test - public void mountWithDollarRegexDoesNotAddSlash() { - Route route = Route.builder() - .path("$") - .hasRegexPath(true) - .build(); - - register.add(route, "/mount"); - - verify(router).routeWithRegex("/mount$"); - } - - @SuppressWarnings("unchecked") - @Test - public void onAddCallback() { - String path = "/some/regex/path"; - Route route = Route.builder() - .path(path).build(); - - BiConsumer callback = mock(BiConsumer.class); - register.onAdd(callback) - .add(route); - verify(callback).accept(path, route); - - String mount = "/my/mount"; - register.onAdd(callback) - .add(route, mount); - verify(callback).accept(mount + path, route); - - String rootMount = "/our/root"; - RouteRegister registerWithPath = new RouteRegister(router, rootMount, true); - - registerWithPath.onAdd(callback) - .add(route); - verify(callback).accept(rootMount + path, route); - - registerWithPath.onAdd(callback) - .add(route, mount); - verify(callback).accept(rootMount + mount + path, route); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteRegisterTest.kt b/src/test/java/com/zepben/vertxutils/routing/RouteRegisterTest.kt new file mode 100644 index 0000000..2cefa78 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/RouteRegisterTest.kt @@ -0,0 +1,229 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.Route.Companion.builder +import com.zepben.vertxutils.routing.RouteGroup.Companion.create +import io.vertx.core.Handler +import io.vertx.core.http.HttpMethod +import io.vertx.ext.web.Route +import io.vertx.ext.web.Router +import io.vertx.ext.web.RoutingContext +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.* + +class RouteRegisterTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val vertxRoute = mock() + private val router = mock().also { + doReturn(vertxRoute).`when`(it).route(anyString()) + doReturn(vertxRoute).`when`(it).routeWithRegex(anyString()) + } + + private val handler = Handler {} + private val blockingHandler = Handler {} + private val failureHandler = Handler {} + + private var register = RouteRegister(router, "", true) + + @Test + fun addRoutes() { + val route = builder() + .path("/some/path") + .methods(HttpMethod.GET, HttpMethod.PUT) + .addHandler(handler) + .addBlockingHandler(blockingHandler) + .addFailureHandler(failureHandler) + .build() + + register.add(route, "") + + verify(router).route("/some/path") + verify(vertxRoute).method(HttpMethod.GET) + verify(vertxRoute).method(HttpMethod.PUT) + + val inOrder = inOrder(vertxRoute, vertxRoute) + inOrder.verify(vertxRoute).handler(handler) + inOrder.verify(vertxRoute).blockingHandler(blockingHandler, true) + + verify(vertxRoute).failureHandler(failureHandler) + } + + @Test + fun addRoutesDefaultOrderedBlockingTrue() { + register = RouteRegister(router, "", true) + + val route = builder() + .path("/some/path") + .addBlockingHandler(blockingHandler) + .build() + + register.add(route, "") + + verify(router).route("/some/path") + verify(vertxRoute).blockingHandler(blockingHandler, true) + } + + @Test + fun addRoutesDefaultOrderedBlockingFalse() { + register = RouteRegister(router, "", false) + + val route = builder() + .path("/some/path") + .addBlockingHandler(blockingHandler) + .build() + + register.add(route, "") + + verify(router).route("/some/path") + verify(vertxRoute).blockingHandler(blockingHandler, false) + } + + @Test + fun addRoutesOrderedBlockingTrue() { + register = RouteRegister(router, "", false) + + val route = builder() + .path("/some/path") + .addBlockingHandler(blockingHandler, true) + .build() + + register.add(route, "") + + verify(router).route("/some/path") + verify(vertxRoute).blockingHandler(blockingHandler, true) + } + + @Test + fun addRoutesOrderedBlockingFalse() { + register = RouteRegister(router, "", true) + + val route = builder() + .path("/some/path") + .addBlockingHandler(blockingHandler, false) + .build() + + register.add(route, "") + + verify(router).route("/some/path") + verify(vertxRoute).blockingHandler(blockingHandler, false) + } + + @Test + fun usesMountPaths() { + register = RouteRegister(router, "/rootMount/", true) + val route = builder() + .path("/some/path") + .build() + + register.add(route, "/mount/") + + verify(router).route("/rootMount/mount/some/path") + } + + @Test + fun addRouteGroup() { + val route1 = builder().path("/route/1").build() + val route2 = builder().path("/route/2").build() + val group = create("/group", listOf(route1, route2)) + + register.add(group) + + verify(router).route("/group/route/1") + verify(router).route("/group/route/2") + } + + @Test + fun addRouteGroups() { + val route1 = builder().path("/route/1").build() + val route2 = builder().path("/route/2").build() + val group1 = create("/group1", listOf(route1)) + val group2 = create("/group2", listOf(route2)) + + register.addGroups(listOf(group1, group2)) + + verify(router).route("/group1/route/1") + verify(router).route("/group2/route/2") + } + + @Test + fun addsRegex() { + val route = builder() + .path("/some/regex/path") + .hasRegexPath(true) + .methods(HttpMethod.GET) + .build() + + register.add(route, "") + + verify(router).routeWithRegex("/some/regex/path") + } + + @Test + fun usesMountPathRegex() { + val route = builder() + .path("/some/regex/path") + .hasRegexPath(true) + .build() + + register.add(route, "/mount") + + verify(router).routeWithRegex("/mount/some/regex/path") + } + + @Test + fun mountWithDollarRegexDoesNotAddSlash() { + val route = builder() + .path("$") + .hasRegexPath(true) + .build() + + register.add(route, "/mount") + + verify(router).routeWithRegex("/mount$") + } + + @Test + fun onAddCallback() { + val path = "/some/regex/path" + val route = builder() + .path(path).build() + + val callback = mock<(String, com.zepben.vertxutils.routing.Route) -> Unit>() + register.onAdd = callback + register.add(route, "") + verify(callback).invoke(path, route) + + val mount = "/my/mount" + register.onAdd = callback + register.add(route, mount) + verify(callback).invoke(mount + path, route) + + val rootMount = "/our/root" + val registerWithPath = RouteRegister(router, rootMount, true) + + registerWithPath.onAdd = callback + registerWithPath.add(route, "") + verify(callback).invoke(rootMount + path, route) + + registerWithPath.onAdd = callback + registerWithPath.add(route, mount) + verify(callback).invoke(rootMount + mount + path, route) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteTest.java b/src/test/java/com/zepben/vertxutils/routing/RouteTest.java deleted file mode 100644 index 43d5192..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/RouteTest.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler; -import com.zepben.vertxutils.routing.handlers.PathParamsHandler; -import com.zepben.vertxutils.routing.handlers.QueryParamsHandler; -import com.zepben.vertxutils.routing.handlers.UtilHandlers; -import com.zepben.vertxutils.routing.handlers.params.BodyType; -import com.zepben.vertxutils.routing.handlers.params.ParamType; -import com.zepben.vertxutils.routing.handlers.params.PathParamRule; -import com.zepben.vertxutils.routing.handlers.params.QueryParamRule; -import io.vertx.core.Handler; -import io.vertx.core.http.HttpMethod; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.function.BiConsumer; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.hamcrest.core.Is.is; -import static org.mockito.Mockito.mock; - -public class RouteTest { - - @Test - public void path() { - Route route = Route.builder().path("/a/path").build(); - assertThat(route.path(), is("/a/path")); - } - - @Test - public void formatPath() { - PathParamRule rule = PathParamRule.of("test", ParamType.STRING); - Route route = Route.builder().path("/a/path/:%s", rule).build(); - assertThat(route.path(), is("/a/path/:test")); - } - - @Test - public void pathMustNotBeEmpty() { - expect(() -> Route.builder().path("")).toThrow(IllegalArgumentException.class).withMessage("path must not be empty"); - } - - @Test - public void validatePath() { - expect(() -> Route.builder().path("woop").build()).toThrow(IllegalStateException.class).withMessage("path must start with a /"); - - // This should not throw because of regex flag - Route.builder().path("woop").hasRegexPath(true).build(); - } - - @Test - public void validatePathParams() { - PathParamRule rule = PathParamRule.of("test", ParamType.STRING); - - expect(() -> Route.builder().path("/some/path", rule)).toThrow(IllegalArgumentException.class).withMessage("too many path params"); - expect(() -> Route.builder().path("/some/path/:%s/:%s", rule)).toThrow(IllegalArgumentException.class).withMessage("missing path params"); - expect(() -> Route.builder().path("/some/path/:%d", rule)).toThrow(IllegalArgumentException.class).withMessage("invalid use of % in path format string"); - expect(() -> Route.builder().path("/some/path/:%%", rule)).toThrow(IllegalArgumentException.class).withMessage("invalid use of % in path format string"); - expect(() -> Route.builder().path("/some/path/%s", rule)).toThrow(IllegalArgumentException.class).withMessage("invalid use of % in path format string"); - expect(() -> Route.builder().path("/some/path/:%s")).toThrow(IllegalArgumentException.class).withMessage("formatted path must not contain a '%'"); - } - - @Test - public void pathParamsHandler() { - PathParamRule rule = PathParamRule.of("test", ParamType.STRING); - Route route = Route.builder() - .addHandler(r -> { - }) - .path("/a/path/:%s", rule).build(); - - // Make sure the params handler is before any other registered handler. - assertThat(route.handlers().get(0).handler(), instanceOf(PathParamsHandler.class)); - - PathParamsHandler handler = (PathParamsHandler) route.handlers().get(0).handler(); - assertThat(handler.rules().values(), containsInAnyOrder(rule)); - } - - @Test - public void defaultRegexPath() { - assertThat(Route.builder().build().hasRegexPath(), is(false)); - } - - @Test - public void setRegexPath() { - assertThat(Route.builder().hasRegexPath(true).build().hasRegexPath(), is(true)); - } - - @Test - public void method() { - assertThat(Route.builder().method(HttpMethod.GET).build().methods(), contains(HttpMethod.GET)); - } - - @Test - public void methods() { - assertThat(Route.builder().methods(HttpMethod.GET, HttpMethod.POST).build().methods(), contains(HttpMethod.GET, HttpMethod.POST)); - } - - @Test - public void queryParams() { - QueryParamRule rule1 = QueryParamRule.of("p1", ParamType.STRING); - QueryParamRule rule2 = QueryParamRule.of("p2", ParamType.INT); - - Route route = Route.builder() - .addHandler(rc -> { - }) - .queryParams(rule1, rule2) - .build(); - - - // Make sure the params handler is before any other registered handler. - assertThat(route.handlers().get(0).handler(), instanceOf(QueryParamsHandler.class)); - - QueryParamsHandler handler = (QueryParamsHandler) route.handlers().get(0).handler(); - assertThat(handler.rules().values(), containsInAnyOrder(rule1, rule2)); - } - - @Test - public void bodySizeLimit() { - Route route = Route.builder() - .addHandler(rc -> { - }) - .bodySizeLimit(1) - .build(); - - // Make sure the body handler is before any other registered handler. - assertThat(route.handlers().get(0).handler(), instanceOf(BodyHandler.class)); - - // Unfortunately there is no easy way to test the size was set correctly on the BodyHandler as it does not expose getters. - } - - @Test - public void uploadsDirectory() { - Route route = Route.builder() - .addHandler(rc -> { - }) - .uploadsDirectory("/some/path") - .build(); - - // Make sure the body handler is before any other registered handler. - assertThat(route.handlers().get(0).handler(), instanceOf(BodyHandler.class)); - - // Unfortunately there is no easy way to test the directory was set correctly on the BodyHandler as it does not expose getters. - } - - @Test - public void decodeBody() { - Route route = Route.builder() - .addHandler(rc -> { - }) - .decodeBody(BodyType.JSON_OBJECT) - .build(); - - // Make sure the body handler is before any other registered handler. - assertThat(route.handlers().get(0).handler(), instanceOf(BodyHandler.class)); - assertThat(route.handlers().get(1).handler(), instanceOf(DecodeBodyHandler.class)); - - DecodeBodyHandler handler = (DecodeBodyHandler) route.handlers().get(1).handler(); - assertThat(handler.bodyRule().converter(), is(BodyType.JSON_OBJECT)); - assertThat(handler.bodyRule().isRequired(), is(true)); - } - - @Test - public void decodeBodyOptional() { - Route route = Route.builder() - .addHandler(rc -> { - }) - .decodeBody(BodyType.JSON_OBJECT, false) - .build(); - - // Make sure the body handler is before any other registered handler. - assertThat(route.handlers().get(0).handler(), instanceOf(BodyHandler.class)); - assertThat(route.handlers().get(1).handler(), instanceOf(DecodeBodyHandler.class)); - - DecodeBodyHandler handler = (DecodeBodyHandler) route.handlers().get(1).handler(); - assertThat(handler.bodyRule().converter(), is(BodyType.JSON_OBJECT)); - assertThat(handler.bodyRule().isRequired(), is(false)); - } - - @Test - public void failureHandler() { - List> handlers = Route.builder() - .addFailureHandler(UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER) - .build() - .failureHandlers(); - - assertThat(handlers.get(0), is(UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER)); - } - - @SuppressWarnings("unchecked") - @Test - public void exceptionFailureHandler() { - BiConsumer handler = mock(BiConsumer.class); - List> handlers = Route.builder() - .addFailureHandler(RuntimeException.class, handler) - .build() - .failureHandlers(); - - assertThat(handlers.get(0), instanceOf(ExceptionHandler.class)); - } - - @Test - public void nonBlockingHandler() { - Route route = Route.builder().addHandler(c -> { - }).build(); - assertThat(route.handlers().get(0).isBlocking(), is(false)); - } - - @Test - public void flagsBlockingHandler() { - Route route = Route.builder().addBlockingHandler(c -> { - }).build(); - assertThat(route.handlers().get(0).isBlocking(), is(true)); - } - - @Test - public void defaultIsPublic() { - assertThat(Route.builder().build().isPublic(), is(true)); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteTest.kt b/src/test/java/com/zepben/vertxutils/routing/RouteTest.kt new file mode 100644 index 0000000..bffeec4 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/RouteTest.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.Route.Companion.builder +import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler +import com.zepben.vertxutils.routing.handlers.PathParamsHandler +import com.zepben.vertxutils.routing.handlers.QueryParamsHandler +import com.zepben.vertxutils.routing.handlers.UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER +import com.zepben.vertxutils.routing.handlers.params.BodyType +import com.zepben.vertxutils.routing.handlers.params.ParamType +import com.zepben.vertxutils.routing.handlers.params.PathParamRule +import com.zepben.vertxutils.routing.handlers.params.QueryParamRule +import io.vertx.core.http.HttpMethod +import io.vertx.ext.web.RoutingContext +import io.vertx.ext.web.handler.BodyHandler +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.mock + +class RouteTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun path() { + val route = builder().path("/a/path").build() + assertThat(route.path, equalTo("/a/path")) + } + + @Test + fun formatPath() { + val rule = PathParamRule.of("test", ParamType.STRING) + val route = builder().path("/a/path/:%s", rule).build() + assertThat(route.path, equalTo("/a/path/:test")) + } + + @Test + fun pathMustNotBeEmpty() { + expect { builder().path("") }.toThrow() + .withMessage("path must not be empty") + } + + @Test + fun validatePath() { + expect { builder().path("woop").build() }.toThrow() + .withMessage("path must start with a /") + + // This should not throw because of regex flag + builder().path("woop").hasRegexPath(true).build() + } + + @Test + fun validatePathParams() { + val rule = PathParamRule.of("test", ParamType.STRING) + + expect { builder().path("/some/path", rule) }.toThrow() + .withMessage("too many path params") + expect { builder().path("/some/path/:%s/:%s", rule) }.toThrow() + .withMessage("missing path params") + expect { builder().path("/some/path/:%d", rule) }.toThrow() + .withMessage("invalid use of % in path format string") + expect { builder().path("/some/path/:%%", rule) }.toThrow() + .withMessage("invalid use of % in path format string") + expect { builder().path("/some/path/%s", rule) }.toThrow() + .withMessage("invalid use of % in path format string") + expect { builder().path("/some/path/:%s") }.toThrow() + .withMessage("formatted path must not contain a '%'") + } + + @Test + fun pathParamsHandler() { + val rule = PathParamRule.of("test", ParamType.STRING) + val route = builder() + .addHandler { } + .path("/a/path/:%s", rule).build() + + // Make sure the params handler is before any other registered handler. + assertThat( + route.handlers[0].handler, + instanceOf(PathParamsHandler::class.java), + ) + + val handler = route.handlers[0].handler as PathParamsHandler + assertThat(handler.rules.values, containsInAnyOrder(rule)) + } + + @Test + fun defaultRegexPath() { + assertThat(builder().build().hasRegexPath, equalTo(false)) + } + + @Test + fun setRegexPath() { + assertThat(builder().hasRegexPath(true).build().hasRegexPath, equalTo(true)) + } + + @Test + fun method() { + assertThat(builder().method(HttpMethod.GET).build().methods, contains(HttpMethod.GET)) + } + + @Test + fun methods() { + assertThat(builder().methods(HttpMethod.GET, HttpMethod.POST).build().methods, contains(HttpMethod.GET, HttpMethod.POST)) + } + + @Test + fun queryParams() { + val rule1 = QueryParamRule.of("p1", ParamType.STRING) + val rule2 = QueryParamRule.of("p2", ParamType.INT) + + val route = builder() + .addHandler { } + .queryParams(rule1, rule2) + .build() + + // Make sure the params handler is before any other registered handler. + assertThat( + route.handlers[0].handler, + instanceOf(QueryParamsHandler::class.java), + ) + + val handler = route.handlers[0].handler as QueryParamsHandler + assertThat(handler.rules.values, containsInAnyOrder(rule1, rule2)) + } + + @Test + fun bodySizeLimit() { + val route = builder() + .addHandler { } + .bodySizeLimit(1) + .build() + + // Make sure the body handler is before any other registered handler. + assertThat( + route.handlers[0].handler, + instanceOf(BodyHandler::class.java), + ) + + // Unfortunately there is no easy way to test the size was set correctly on the BodyHandler as it does not expose getters. + } + + @Test + fun uploadsDirectory() { + val route = builder() + .addHandler { } + .uploadsDirectory("/some/path") + .build() + + // Make sure the body handler is before any other registered handler. + assertThat( + route.handlers[0].handler, + instanceOf(BodyHandler::class.java), + ) + + // Unfortunately there is no easy way to test the directory was set correctly on the BodyHandler as it does not expose getters. + } + + @Test + fun decodeBody() { + val route = builder() + .addHandler { } + .decodeBody(BodyType.JSON_OBJECT, true) + .build() + + // Make sure the body handler is before any other registered handler. + assertThat( + route.handlers[0].handler, + instanceOf(BodyHandler::class.java), + ) + assertThat( + route.handlers[1].handler, + instanceOf(DecodeBodyHandler::class.java), + ) + + val handler = route.handlers[1].handler as DecodeBodyHandler + assertThat(handler.bodyRule.converter, equalTo(BodyType.JSON_OBJECT)) + assertThat(handler.bodyRule.isRequired, equalTo(true)) + } + + @Test + fun decodeBodyOptional() { + val route = builder() + .addHandler { } + .decodeBody(BodyType.JSON_OBJECT, false) + .build() + + // Make sure the body handler is before any other registered handler. + assertThat( + route.handlers[0].handler, + instanceOf(BodyHandler::class.java), + ) + assertThat( + route.handlers[1].handler, + instanceOf(DecodeBodyHandler::class.java), + ) + + val handler = route.handlers[1].handler as DecodeBodyHandler + assertThat(handler.bodyRule.converter, equalTo(BodyType.JSON_OBJECT)) + assertThat(handler.bodyRule.isRequired, equalTo(false)) + } + + @Test + fun failureHandler() { + val handlers = builder() + .addFailureHandler(CATCH_ALL_API_FAILURE_HANDLER) + .build() + .failureHandlers + + assertThat(handlers[0], equalTo(CATCH_ALL_API_FAILURE_HANDLER)) + } + + @Test + fun exceptionFailureHandler() { + val handler = mock<(RuntimeException, RoutingContext?) -> Unit>() + val handlers = builder() + .addFailureHandler(RuntimeException::class.java, handler) + .build() + .failureHandlers + + assertThat(handlers[0], instanceOf(ExceptionHandler::class.java)) + } + + @Test + fun nonBlockingHandler() { + val route = builder().addHandler { }.build() + assertThat(route.handlers[0].isBlocking, equalTo(false)) + } + + @Test + fun flagsBlockingHandler() { + val route = builder().addBlockingHandler { }.build() + assertThat(route.handlers[0].isBlocking, equalTo(true)) + } + + @Test + fun defaultIsPublic() { + assertThat(builder().build().isPublic, equalTo(true)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteVersionTest.java b/src/test/java/com/zepben/vertxutils/routing/RouteVersionTest.java deleted file mode 100644 index d96039a..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/RouteVersionTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.testutils.junit.SystemLogExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -public class RouteVersionTest { - - @RegisterExtension static SystemLogExtension systemOut = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess(); - - @Test - public void canCheckForVersion() { - validateAvailability(RouteVersion.since(1), true, true, true, true, true, true); - validateAvailability(RouteVersion.since(2), false, true, true, true, true, true); - validateAvailability(RouteVersion.since(3), false, false, true, true, true, true); - validateAvailability(RouteVersion.since(20), false, false, true, true, true, true); - validateAvailability(RouteVersion.since(21), false, false, false, true, true, true); - validateAvailability(RouteVersion.since(22), false, false, false, false, true, true); - validateAvailability(RouteVersion.since(23), false, false, false, false, false, true); - - validateAvailability(RouteVersion.between(1, 1), true, false, false, false, false, false); - validateAvailability(RouteVersion.between(1, 2), true, true, false, false, false, false); - validateAvailability(RouteVersion.between(1, 21), true, true, true, true, false, false); - validateAvailability(RouteVersion.between(21, 21), false, false, false, true, false, false); - validateAvailability(RouteVersion.between(21, 22), false, false, false, true, true, false); - } - - private void validateAvailability(RouteVersion routeVersion, boolean isInV1, boolean isInV2, boolean isInV20, boolean isInV21, boolean isInV22, boolean isInVMax) { - assertThat(routeVersion.includes(0), equalTo(false)); - assertThat(routeVersion.includes(1), equalTo(isInV1)); - assertThat(routeVersion.includes(2), equalTo(isInV2)); - assertThat(routeVersion.includes(20), equalTo(isInV20)); - assertThat(routeVersion.includes(21), equalTo(isInV21)); - assertThat(routeVersion.includes(22), equalTo(isInV22)); - assertThat(routeVersion.includes(Integer.MAX_VALUE), equalTo(isInVMax)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteVersionTest.kt b/src/test/java/com/zepben/vertxutils/routing/RouteVersionTest.kt new file mode 100644 index 0000000..8e09537 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/RouteVersionTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.RouteVersion.Companion.between +import com.zepben.vertxutils.routing.RouteVersion.Companion.since +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class RouteVersionTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun canCheckForVersion() { + validateAvailability(since(1), isInV1 = true, isInV2 = true, isInV20 = true, isInV21 = true, isInV22 = true, isInVMax = true) + validateAvailability(since(2), isInV2 = true, isInV20 = true, isInV21 = true, isInV22 = true, isInVMax = true) + validateAvailability(since(3), isInV20 = true, isInV21 = true, isInV22 = true, isInVMax = true) + validateAvailability(since(20), isInV20 = true, isInV21 = true, isInV22 = true, isInVMax = true) + validateAvailability(since(21), isInV21 = true, isInV22 = true, isInVMax = true) + validateAvailability(since(22), isInV22 = true, isInVMax = true) + validateAvailability(since(23), isInVMax = true) + + validateAvailability(between(1, 1), isInV1 = true) + validateAvailability(between(1, 2), isInV1 = true, isInV2 = true) + validateAvailability(between(1, 21), isInV1 = true, isInV2 = true, isInV20 = true, isInV21 = true) + validateAvailability(between(21, 21), isInV21 = true) + validateAvailability(between(21, 22), isInV21 = true, isInV22 = true) + } + + private fun validateAvailability( + routeVersion: RouteVersion, + isInV1: Boolean = false, + isInV2: Boolean = false, + isInV20: Boolean = false, + isInV21: Boolean = false, + isInV22: Boolean = false, + isInVMax: Boolean = false, + ) { + assertThat(routeVersion.contains(0), equalTo(false)) + assertThat(routeVersion.contains(1), equalTo(isInV1)) + assertThat(routeVersion.contains(2), equalTo(isInV2)) + assertThat(routeVersion.contains(20), equalTo(isInV20)) + assertThat(routeVersion.contains(21), equalTo(isInV21)) + assertThat(routeVersion.contains(22), equalTo(isInV22)) + assertThat(routeVersion.contains(Int.MAX_VALUE), equalTo(isInVMax)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteVersionUtilsTest.java b/src/test/java/com/zepben/vertxutils/routing/RouteVersionUtilsTest.java deleted file mode 100644 index f8c0fdd..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/RouteVersionUtilsTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.testutils.junit.SystemLogExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.util.function.Function; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.empty; -import static org.mockito.Mockito.mock; - -public class RouteVersionUtilsTest { - - @RegisterExtension - static SystemLogExtension systemOut = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess(); - - private final Route route1 = mock(Route.class); - private final Route route2V2 = mock(Route.class); - private final Route route2 = mock(Route.class); - private final Route route3V1 = mock(Route.class); - private final Route route3V2 = mock(Route.class); - private final Route route3 = mock(Route.class); - - private final Function routeFactory = availableRoute -> { - switch (availableRoute) { - case ROUTE_1: - return route1; - case ROUTE_2_V2: - return route2V2; - case ROUTE_2: - return route2; - case ROUTE_3_V1: - return route3V1; - case ROUTE_3_V2: - return route3V2; - case ROUTE_3: - return route3; - default: - return mock(Route.class); - } - }; - - @Test - public void createsRoutesForVersion() { - validateVersionRoutes(0); - validateVersionRoutes(1, route1, route2V2, route3V1); - validateVersionRoutes(2, route1, route2V2, route3V2); - validateVersionRoutes(3, route1, route2, route3); - validateVersionRoutes(4, route1, route2, route3); - } - - private void validateVersionRoutes(int version, Route... expectedRoutes) { - if (expectedRoutes.length > 0) - assertThat(RouteVersionUtils.forVersion(AvailableRoute.values(), version, routeFactory), contains(expectedRoutes)); - else - assertThat(RouteVersionUtils.forVersion(AvailableRoute.values(), version, routeFactory), empty()); - } - - @EverythingIsNonnullByDefault - private enum AvailableRoute implements VersionableRoute { - ROUTE_1(RouteVersion.since(1)), - ROUTE_2_V2(RouteVersion.between(1, 2)), - ROUTE_2(RouteVersion.since(3)), - ROUTE_3_V1(RouteVersion.between(1, 1)), - ROUTE_3_V2(RouteVersion.between(2, 2)), - ROUTE_3(RouteVersion.since(3)); - - private final RouteVersion rv; - - AvailableRoute(RouteVersion rv) { - this.rv = rv; - } - - @Override - public RouteVersion routeVersion() { - return rv; - } - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/RouteVersionUtilsTest.kt b/src/test/java/com/zepben/vertxutils/routing/RouteVersionUtilsTest.kt new file mode 100644 index 0000000..de56718 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/RouteVersionUtilsTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.RouteVersion.Companion.between +import com.zepben.vertxutils.routing.RouteVersion.Companion.since +import com.zepben.vertxutils.routing.RouteVersionUtils.forVersion +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.Matchers.empty +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.mock + +class RouteVersionUtilsTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val route1 = mock() + private val route2V2 = mock() + private val route2 = mock() + private val route3V1 = mock() + private val route3V2 = mock() + private val route3 = mock() + + private val routeFactory = { availableRoute: AvailableRoute -> + when (availableRoute) { + AvailableRoute.ROUTE_1 -> route1 + AvailableRoute.ROUTE_2_V2 -> route2V2 + AvailableRoute.ROUTE_2 -> route2 + AvailableRoute.ROUTE_3_V1 -> route3V1 + AvailableRoute.ROUTE_3_V2 -> route3V2 + AvailableRoute.ROUTE_3 -> route3 + } + } + + @Test + fun createsRoutesForVersion() { + validateVersionRoutes(0) + validateVersionRoutes(1, route1, route2V2, route3V1) + validateVersionRoutes(2, route1, route2V2, route3V2) + validateVersionRoutes(3, route1, route2, route3) + validateVersionRoutes(4, route1, route2, route3) + } + + private fun validateVersionRoutes(version: Int, vararg expectedRoutes: Route) { + val routes = routeFactory.forVersion(version) + if (expectedRoutes.isNotEmpty()) + assertThat(routes, Matchers.contains(*expectedRoutes)) + else + assertThat(routes, empty()) + } + + private enum class AvailableRoute( + override val routeVersion: RouteVersion, + ) : VersionableRoute { + + ROUTE_1(since(1)), + ROUTE_2_V2(between(1, 2)), + ROUTE_2(since(3)), + ROUTE_3_V1(between(1, 1)), + ROUTE_3_V2(between(2, 2)), + ROUTE_3(since(3)) + + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/RoutingContextExTest.java b/src/test/java/com/zepben/vertxutils/routing/RoutingContextExTest.java deleted file mode 100644 index cef7bab..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/RoutingContextExTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import com.zepben.vertxutils.routing.handlers.params.BadParamException; -import com.zepben.vertxutils.routing.handlers.params.PathParams; -import com.zepben.vertxutils.routing.handlers.params.QueryParams; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.Optional; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.mockito.Mockito.*; - -public class RoutingContextExTest { - - private final RoutingContext context = mock(RoutingContext.class); - - @Test - public void keysAreDifferent() { - assertThat(RoutingContextEx.PATH_PARAMS_KEY, not(equalTo(RoutingContextEx.QUERY_PARAMS_KEY))); - assertThat(RoutingContextEx.PATH_PARAMS_KEY, not(equalTo(RoutingContextEx.BODY_KEY))); - } - - @Test - public void getPathParams() { - PathParams params = new PathParams(Collections.emptyMap()); - doReturn(params).when(context).get(RoutingContextEx.PATH_PARAMS_KEY); - assertThat(RoutingContextEx.getPathParams(context), is(params)); - } - - @Test - public void getPathParamsWhenNoParams() { - expect(() -> RoutingContextEx.getPathParams(context)).toThrow(IllegalStateException.class); - } - - @Test - public void putPathParams() { - PathParams params = new PathParams(Collections.emptyMap()); - RoutingContextEx.putPathParams(context, params); - verify(context).put(RoutingContextEx.PATH_PARAMS_KEY, params); - } - - @Test - public void getQueryParams() { - QueryParams params = mock(QueryParams.class); - doReturn(params).when(context).get(RoutingContextEx.QUERY_PARAMS_KEY); - assertThat(RoutingContextEx.getQueryParams(context), is(params)); - } - - @Test - public void getQueryParamsWhenNoParams() { - expect(() -> RoutingContextEx.getQueryParams(context)).toThrow(IllegalStateException.class); - } - - @Test - public void putQueryParams() { - QueryParams params = mock(QueryParams.class); - RoutingContextEx.putQueryParams(context, params); - verify(context).put(RoutingContextEx.QUERY_PARAMS_KEY, params); - } - - @Test - public void getDecodedBody() { - doReturn("expected").when(context).get(RoutingContextEx.BODY_KEY); - assertThat(RoutingContextEx.getDecodedBody(context), is("expected")); - } - - @Test - public void getOptionalDecodedBody() { - doReturn("expected").when(context).get(RoutingContextEx.BODY_KEY); - assertThat(RoutingContextEx.getOptionalDecodedBody(context), is(Optional.of("expected"))); - } - - @Test - public void getMissingOptionalDecodedBody() { - assertThat(RoutingContextEx.getOptionalDecodedBody(context), is(Optional.empty())); - } - - @Test - public void geRequestBodyWhenNoBody() { - expect(() -> RoutingContextEx.getDecodedBody(context)).toThrow(BadParamException.class); - } - - @Test - public void putRequestBody() { - Object body = new Object(); - RoutingContextEx.putRequestBody(context, body); - verify(context).put(RoutingContextEx.BODY_KEY, body); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/RoutingContextExTest.kt b/src/test/java/com/zepben/vertxutils/routing/RoutingContextExTest.kt new file mode 100644 index 0000000..8079bc3 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/RoutingContextExTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.RoutingContextEx.BODY_KEY +import com.zepben.vertxutils.routing.RoutingContextEx.PATH_PARAMS_KEY +import com.zepben.vertxutils.routing.RoutingContextEx.QUERY_PARAMS_KEY +import com.zepben.vertxutils.routing.RoutingContextEx.getDecodedBody +import com.zepben.vertxutils.routing.RoutingContextEx.getOptionalDecodedBody +import com.zepben.vertxutils.routing.RoutingContextEx.getPathParams +import com.zepben.vertxutils.routing.RoutingContextEx.getQueryParams +import com.zepben.vertxutils.routing.RoutingContextEx.putPathParams +import com.zepben.vertxutils.routing.RoutingContextEx.putQueryParams +import com.zepben.vertxutils.routing.RoutingContextEx.putRequestBody +import com.zepben.vertxutils.routing.handlers.params.BadParamException +import com.zepben.vertxutils.routing.handlers.params.PathParams +import com.zepben.vertxutils.routing.handlers.params.QueryParams +import io.vertx.ext.web.RoutingContext +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.* + +class RoutingContextExTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val context = mock() + + @Test + fun keysAreDifferent() { + assertThat(PATH_PARAMS_KEY, not(equalTo(QUERY_PARAMS_KEY))) + assertThat(PATH_PARAMS_KEY, not(equalTo(BODY_KEY))) + } + + @Test + fun getPathParams() { + val params = PathParams(emptyMap()) + doReturn(params).`when`(context).get(PATH_PARAMS_KEY) + assertThat(getPathParams(context), equalTo(params)) + } + + @Test + fun getPathParamsWhenNoParams() { + expect { getPathParams(context) }.toThrow() + } + + @Test + fun putPathParams() { + val params = PathParams(emptyMap()) + putPathParams(context, params) + verify(context).put(PATH_PARAMS_KEY, params) + } + + @Test + fun getQueryParams() { + val params = mock() + doReturn(params).`when`(context).get(QUERY_PARAMS_KEY) + assertThat(getQueryParams(context), equalTo(params)) + } + + @Test + fun getQueryParamsWhenNoParams() { + expect { getQueryParams(context) }.toThrow() + } + + @Test + fun putQueryParams() { + val params = mock() + putQueryParams(context, params) + verify(context).put(QUERY_PARAMS_KEY, params) + } + + @Test + fun getDecodedBody() { + doReturn("expected").`when`(context).get(BODY_KEY) + assertThat(getDecodedBody(context), equalTo("expected")) + } + + @Test + fun getOptionalDecodedBody() { + doReturn("expected").`when`(context).get(BODY_KEY) + assertThat(getOptionalDecodedBody(context), equalTo("expected")) + } + + @Test + fun getMissingOptionalDecodedBody() { + assertThat(getOptionalDecodedBody(context), nullValue()) + } + + @Test + fun geRequestBodyWhenNoBody() { + expect { getDecodedBody(context) }.toThrow() + } + + @Test + fun putRequestBody() { + val body = Any() + putRequestBody(context, body) + verify(context).put(BODY_KEY, body) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/StaticAssetRoutesTest.java b/src/test/java/com/zepben/vertxutils/routing/StaticAssetRoutesTest.java deleted file mode 100644 index 05385b4..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/StaticAssetRoutesTest.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import io.restassured.RestAssured; -import io.vertx.core.Vertx; -import io.vertx.ext.web.Router; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.concurrent.CountDownLatch; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.core.Is.is; - -public class StaticAssetRoutesTest { - - @TempDir - public File temporaryFolder; - - private String filePath; - private final String baseUrl = "/app"; - private final int port = 34251; - private StaticAssetRoutes staticRoutes; - private final Vertx vertx = Vertx.vertx(); - - @BeforeEach - public void setUp() throws Exception { - RestAssured.port = port; - filePath = Files.createDirectory(Paths.get(temporaryFolder.toString(), "webroot")).toString(); - staticRoutes = new StaticAssetRoutes(baseUrl, filePath); - } - - @AfterEach - public void tearDown() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - vertx.close(r -> latch.countDown()); - latch.await(); - } - - @Test - public void indexPage() throws Exception { - String html = "The page"; - Files.write(Paths.get(filePath, "index.html"), html.getBytes(StandardCharsets.UTF_8)); - - staticRoutes.indexPage("index.html"); - List routes = staticRoutes.buildRoutes(); - assertThat(routes.size(), is(2)); - startServer(routes); - - given() - .when() - .redirects() - .follow(false) - .get(baseUrl) - .then() - .statusCode(301) - .header("Location", baseUrl + "/"); - - given() - .when() - .get(baseUrl + "/") - .then() - .statusCode(200) - .header("Content-Type", "text/html;charset=UTF-8") - .body(equalTo(html)); - } - - @Test - public void favicon() throws Exception { - String ico = "This should be an image!"; - Files.write(Paths.get(filePath, "favicon.ico"), ico.getBytes(StandardCharsets.UTF_8)); - - staticRoutes.favicon("favicon.ico", "favicon.ico"); - List routes = staticRoutes.buildRoutes(); - assertThat(routes.size(), is(1)); - startServer(routes); - - given() - .when() - .get(baseUrl + "/favicon.ico") - .then() - .statusCode(200) - .body(equalTo(ico)); - } - - @Test - public void subdirs() throws Exception { - String js = "const aVar = 1"; - Path jsDir = Paths.get(filePath, "js"); - Files.createDirectory(jsDir); - Files.write(jsDir.resolve("somejs.js"), js.getBytes(StandardCharsets.UTF_8)); - - staticRoutes.subDirs("js"); - List routes = staticRoutes.buildRoutes(); - assertThat(routes.size(), is(1)); - startServer(routes); - - given() - .when() - .get(baseUrl + "/js/somejs.js") - .then() - .statusCode(200) - .body(equalTo(js)); - } - - private void startServer(Iterable routes) throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - Router router = Router.router(vertx); - vertx.createHttpServer() - .requestHandler(new RouteRegister(router, true).add(routes).router()) - .listen(port, r -> { - latch.countDown(); - if (r.failed()) - throw new RuntimeException("Failed to start server"); - }); - - latch.await(); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/StaticAssetRoutesTest.kt b/src/test/java/com/zepben/vertxutils/routing/StaticAssetRoutesTest.kt new file mode 100644 index 0000000..ab955be --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/StaticAssetRoutesTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.junit.SystemLogExtension +import io.restassured.RestAssured +import io.vertx.core.Vertx +import io.vertx.ext.web.Router +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.CountDownLatch + +class StaticAssetRoutesTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @TempDir + lateinit var temporaryFolder: File + + private val baseUrl = "/app" + private val port = 34251.also { RestAssured.port = it } + private val vertx: Vertx = Vertx.vertx() + + private val filePath: String by lazy { Files.createDirectory(Paths.get(temporaryFolder.toString(), "webroot")).toString() } + private val staticRoutes: StaticAssetRoutes by lazy { StaticAssetRoutes(baseUrl, filePath) } + + @AfterEach + fun tearDown() { + val latch = CountDownLatch(1) + vertx.close { latch.countDown() } + latch.await() + } + + @Test + fun indexPage() { + val html = "The page" + Files.write(Paths.get(filePath, "index.html"), html.toByteArray(StandardCharsets.UTF_8)) + + staticRoutes.indexPage("index.html") + val routes = staticRoutes.buildRoutes() + assertThat(routes.size, equalTo(2)) + startServer(routes) + + RestAssured.given() + .`when`() + .redirects() + .follow(false) + .get(baseUrl) + .then() + .statusCode(301) + .header("Location", "$baseUrl/") + + RestAssured.given() + .`when`() + .get("$baseUrl/") + .then() + .statusCode(200) + .header("Content-Type", "text/html;charset=UTF-8") + .body(equalTo(html)) + } + + @Test + fun favicon() { + val ico = "This should be an image!" + Files.write(Paths.get(filePath, "favicon.ico"), ico.toByteArray(StandardCharsets.UTF_8)) + + staticRoutes.favicon("favicon.ico", "favicon.ico") + val routes = staticRoutes.buildRoutes() + assertThat(routes.size, equalTo(1)) + startServer(routes) + + RestAssured.given() + .`when`() + .get("$baseUrl/favicon.ico") + .then() + .statusCode(200) + .body(equalTo(ico)) + } + + @Test + fun subdirs() { + val js = "const aVar = 1" + val jsDir = Paths.get(filePath, "js") + Files.createDirectory(jsDir) + Files.write(jsDir.resolve("somejs.js"), js.toByteArray(StandardCharsets.UTF_8)) + + staticRoutes.subDirs("js") + val routes = staticRoutes.buildRoutes() + assertThat(routes.size, equalTo(1)) + startServer(routes) + + RestAssured.given() + .`when`() + .get("$baseUrl/js/somejs.js") + .then() + .statusCode(200) + .body(equalTo(js)) + } + + private fun startServer(routes: Iterable) { + val latch = CountDownLatch(1) + val router = Router.router(vertx) + vertx.createHttpServer() + .requestHandler(RouteRegister(router, "", true).add(routes).router) + .listen(port) { + latch.countDown() + if (it.failed()) throw RuntimeException("Failed to start server") + } + + latch.await() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfigTest.java b/src/test/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfigTest.java deleted file mode 100644 index 4db06c4..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfigTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing; - -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -public class StaticAssetsRouteConfigTest { - - @Test - public void accessors() { - StaticAssetsRouteConfig config = StaticAssetsRouteConfig.of("root1", true); - assertThat(config.webRoot(), equalTo("root1")); - assertThat(config.isCaching(), equalTo(true)); - - config = StaticAssetsRouteConfig.of("root2", false); - assertThat(config.webRoot(), equalTo("root2")); - assertThat(config.isCaching(), equalTo(false)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfigTest.kt b/src/test/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfigTest.kt new file mode 100644 index 0000000..c13e36b --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfigTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.StaticAssetsRouteConfig.Companion.of +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class StaticAssetsRouteConfigTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun accessors() { + var config = of("root1", true) + assertThat(config.webRoot, equalTo("root1")) + assertThat(config.isCaching, equalTo(true)) + + config = of("root2", false) + assertThat(config.webRoot, equalTo("root2")) + assertThat(config.isCaching, equalTo(false)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponseTest.kt b/src/test/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponseTest.kt new file mode 100644 index 0000000..a2d370f --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponseTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.chunked + +import com.zepben.testutils.junit.SystemLogExtension +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class CaptureChunkedJsonResponseTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun captured() { + val response = CaptureChunkedJsonResponse() + + response.ofArray { + item("this is") + assertThat(response.toString(), equalTo("[\"this is\"")) + + item("my") + assertThat(response.toString(), equalTo("[\"this is\",\"my\"")) + + item("test data") + assertThat(response.toString(), equalTo("[\"this is\",\"my\",\"test data\"")) + } + + assertThat(response.toString(), equalTo("[\"this is\",\"my\",\"test data\"]")) + } + + @Test + internal fun `can be cleared`() { + val response = CaptureChunkedJsonResponse() + + response.ofArray { + item("used") + } + + assertThat(response.toString(), equalTo("[\"used\"]")) + + response.clear() + + assertThat(response.toString(), equalTo("")) + + response.ofArray { + item("used again") + } + + assertThat(response.toString(), equalTo("[\"used again\"]")) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponseTest.kt b/src/test/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponseTest.kt new file mode 100644 index 0000000..29f5710 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponseTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.chunked + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.kotlin.core.json.jsonArrayOf +import io.vertx.kotlin.core.json.jsonObjectOf +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +/** + * All tests in this suite call end on the response in order to capture the internal buffer for checking. + */ +class ChunkedJsonResponseTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val response: ChunkedJsonResponse = CaptureChunkedJsonResponse() + + @Test + fun beginResponseObject() { + response.ofObject {} + assertThat(response.toString(), equalTo("{}")) + + // Shouldn't be able to start a new object while the buffer is not empty. + expect { response.ofObject { } } + .toThrow() + .withMessage("Can't reuse a non-clean response builder") + + // Shouldn't be able to start a new array while the buffer is not empty. + expect { response.ofArray { } } + .toThrow() + .withMessage("Can't reuse a non-clean response builder") + } + + @Test + fun beginResponseArray() { + response.ofArray { } + assertThat(response.toString(), equalTo("[]")) + + // Shouldn't be able to start a new object while the buffer is not empty. + expect { response.ofObject { } } + .toThrow() + .withMessage("Can't reuse a non-clean response builder") + + // Shouldn't be able to start a new array while the buffer is not empty. + expect { response.ofArray { } } + .toThrow() + .withMessage("Can't reuse a non-clean response builder") + } + + @Test + fun canReuseIfBufferIsCleared() { + val reusable = ReuseableChunkedJsonResponse() + + reusable.ofObject { } + reusable.ofObject { } + reusable.ofArray { } + reusable.ofArray { } + } + + @Test + fun generatesExpectedJson() { + response.ofObject { + obj("o1") { + field("o1k1", 0) + field("o1k2", "text") + array("o1k3") { + array { + obj { } + } + } + array("o1k4") { } + } + obj("o2") { + obj("o3") { } + array("o2k1") { + item(0) + item(1) + item(2) + } + } + } + + val expected = "{\"o1\":{\"o1k1\":0,\"o1k2\":\"text\",\"o1k3\":[[{}]],\"o1k4\":[]},\"o2\":{\"o3\":{},\"o2k1\":[0,1,2]}}" + + assertThat(response.toString(), equalTo(expected)) + } + + @Test + fun `encodes JsonObject and JsonArray values`() { + response.ofArray { + item(jsonObjectOf("a" to 1, "b" to "c")) + item(jsonArrayOf("d", 2, 3)) + } + + val expected = "[{\"a\":1,\"b\":\"c\"},[\"d\",2,3]]" + + assertThat(response.toString(), equalTo(expected)) + } + + private class ReuseableChunkedJsonResponse : ChunkedJsonResponse() { + + override fun checkWrite(sb: StringBuilder, force: Boolean) { + } + + override fun onResponseCompleted(sb: StringBuilder) { + sb.setLength(0) + } + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponseTest.kt b/src/test/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponseTest.kt new file mode 100644 index 0000000..b433c51 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponseTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.chunked + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import io.netty.handler.codec.http.HttpResponseStatus +import io.vertx.core.http.HttpServerResponse +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.* + +class HttpChunkedJsonResponseTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val httpServerResponse = mock() + + @Test + fun doEnd() { + doReturn(null).`when`(httpServerResponse).end(anyString()) + + // Check that ending the response sends the remaining buffer. + HttpChunkedJsonResponse(httpServerResponse, ChunkedJsonResponse.DEFAULT_BUFFER_SIZE).ofArray { } + verify(httpServerResponse).end("[]") + } + + @Test + fun doCheckWrite() { + doReturn(null).`when`(httpServerResponse).write(anyString()) + + // NOTE: Buffer needs to be big enough for the added escaping. + HttpChunkedJsonResponse(httpServerResponse, 14).ofArray { + // + // NOTE: Every add of an item does a non-forced write check. + // + + // Check that a non-forced send does nothing if the buffer is undersized. + item("this") + item("is") + verify(httpServerResponse, never()).write(anyString()) + + // Check that a forced send works even if the buffer is undersized. + checkWrite(true) + verify(httpServerResponse).write("[\"this\",\"is\"") + + // Check that the buffer has been reset and is again undersized. + clearInvocations(httpServerResponse) + item("my") + verify(httpServerResponse, never()).write(anyString()) + + // Check that a non-forced sends works once the buffer size is exceeded. + item("test data") + verify(httpServerResponse).write(",\"my\",\"test data\"") + } + } + + @Test + fun responseCheck() { + doReturn(true).`when`(httpServerResponse).closed() + HttpChunkedJsonResponse(httpServerResponse, ChunkedJsonResponse.DEFAULT_BUFFER_SIZE).ofArray { } + verify(httpServerResponse, never()).end(anyString()) + + doReturn(false).`when`(httpServerResponse).closed() + HttpChunkedJsonResponse(httpServerResponse, ChunkedJsonResponse.DEFAULT_BUFFER_SIZE).ofArray { } + verify(httpServerResponse).end("[]") + + // Mark the response as closed to ensure the buffer isn't cleared when sending. + doReturn(true).`when`(httpServerResponse).closed() + doReturn(null).`when`(httpServerResponse).write(anyString()) + HttpChunkedJsonResponse(httpServerResponse, 10).ofArray { + item("this") + item("is") + checkWrite(true) + item("my") + item("test data") + + verify(httpServerResponse, never()).write(anyString()) + + // Mark the response as open to ensure the buffer is sent on array close. + doReturn(false).`when`(httpServerResponse).closed() + } + + verify(httpServerResponse).end("[\"this\",\"is\",\"my\",\"test data\"]") + } + + @Test + internal fun `can read and change status code`() { + var statusCode = HttpResponseStatus.EARLY_HINTS.code() + doAnswer { statusCode = it.getArgument(0); null }.`when`(httpServerResponse).statusCode = anyInt() + doAnswer { statusCode }.`when`(httpServerResponse).statusCode + + val response = HttpChunkedJsonResponse(httpServerResponse) + assertThat(response.statusCode, equalTo(HttpResponseStatus.EARLY_HINTS)) + assertThat(statusCode, equalTo(HttpResponseStatus.EARLY_HINTS.code())) + + fun validateChange(newStatus: HttpResponseStatus) { + response.statusCode = newStatus + // Did the mock get called with the correct value? + assertThat(statusCode, equalTo(newStatus.code())) + // Did we return the code for the value set? + assertThat(response.statusCode, equalTo(newStatus)) + } + + validateChange(HttpResponseStatus.PROCESSING) + validateChange(HttpResponseStatus.OK) + } + + @Test + internal fun `can't change status code after the response has been committed via write`() { + // Ensure once a response is committed (first write or end), it is too late to change the status. + val response = HttpChunkedJsonResponse(httpServerResponse) + response.ofObject { + // Should be able to set it before we send anything + response.statusCode = HttpResponseStatus.OK + + checkWrite(force = true) + + // Now we have committed the message via a `write`, we shouldn't be able to change the status. + expect { response.statusCode = HttpResponseStatus.NOT_FOUND } + .toThrow() + .withMessage("You can't set the status after the response has been committed") + } + } + + @Test + internal fun `can't change status code after the response has been committed via end`() { + // Ensure once a response is committed (first write or end), it is too late to change the status. + val response = HttpChunkedJsonResponse(httpServerResponse) + + // Should be able to set it before we send anything + response.statusCode = HttpResponseStatus.OK + + response.ofObject {} + + // Now we have committed the message via ending the response, we shouldn't be able to change the status. + expect { response.statusCode = HttpResponseStatus.NOT_FOUND } + .toThrow() + .withMessage("You can't set the status after the response has been committed") + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/example/GetFromFridgeRoute.java b/src/test/java/com/zepben/vertxutils/routing/example/GetFromFridgeRoute.java deleted file mode 100644 index db35f08..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/example/GetFromFridgeRoute.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.example; - - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import com.zepben.vertxutils.routing.Route; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.UtilHandlers; -import com.zepben.vertxutils.routing.handlers.params.BodyType; -import com.zepben.vertxutils.routing.handlers.params.ParamType; -import com.zepben.vertxutils.routing.handlers.params.PathParamRule; -import com.zepben.vertxutils.routing.handlers.params.QueryParamRule; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; - -import java.util.Optional; - -@EverythingIsNonnullByDefault -public class GetFromFridgeRoute { - - private static class Params { - private static final PathParamRule ITEM = PathParamRule.of("item", ParamType.STRING); - private static final QueryParamRule AMOUNT = QueryParamRule.of("amount", ParamType.INT_POSITIVE); - } - - public Route buildRoute() { - // Alternative if you don't extend Route. - // All the @Override methods in this class can disappear and you would use a builder like this: - return Route.builder() - .method(HttpMethod.GET) - .path("/api/v1/fridge/:%s", Params.ITEM) - .queryParams(Params.AMOUNT) - .bodySizeLimit(1000) - .decodeBody(BodyType.JSON_OBJECT, false) // Body required by default, have to specify if not required. - .addBlockingHandler(this::someHandlerThatBlocks) - .addHandler(this::aRegularHandler) - .addFailureHandler(this::logFailure) - .addFailureHandler(UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER) - .build(); - } - - private void aRegularHandler(RoutingContext context) { - String item = RoutingContextEx.getPathParams(context).get(Params.ITEM); - Integer amount = RoutingContextEx.getQueryParams(context).getOrElse(Params.AMOUNT, 1); - Optional body = RoutingContextEx.getOptionalDecodedBody(context); - // If body was required you would go - // JsonObject body = RoutingContextEx.getDecodedBody(context); - - if (body.isPresent() && body.get().containsKey("types")) { - context.response() - .setStatusCode(200) - .end(String.format("You asked for %d of each %s in the %s category", amount, item, body.get().getJsonArray("types"))); - } else { - context.response() - .setStatusCode(200) - .end(String.format("You asked for a %s", item)); - } - } - - private void someHandlerThatBlocks(RoutingContext context) { - System.out.println("Someone is using the fridge, waiting for my turn..."); - context.next(); - } - - private void logFailure(RoutingContext context) { - System.out.println("This should be a logger!"); - context.next(); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/example/GetFromFridgeRoute.kt b/src/test/java/com/zepben/vertxutils/routing/example/GetFromFridgeRoute.kt new file mode 100644 index 0000000..12a8809 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/example/GetFromFridgeRoute.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.example + +import com.zepben.vertxutils.routing.Route +import com.zepben.vertxutils.routing.Route.Companion.builder +import com.zepben.vertxutils.routing.RoutingContextEx.getOptionalDecodedBody +import com.zepben.vertxutils.routing.RoutingContextEx.getPathParams +import com.zepben.vertxutils.routing.RoutingContextEx.getQueryParams +import com.zepben.vertxutils.routing.handlers.UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER +import com.zepben.vertxutils.routing.handlers.params.BodyType.JSON_OBJECT +import com.zepben.vertxutils.routing.handlers.params.ParamType.INT_POSITIVE +import com.zepben.vertxutils.routing.handlers.params.ParamType.STRING +import com.zepben.vertxutils.routing.handlers.params.PathParamRule +import com.zepben.vertxutils.routing.handlers.params.QueryParamRule +import io.vertx.core.http.HttpMethod +import io.vertx.core.json.JsonObject +import io.vertx.ext.web.RoutingContext + +class GetFromFridgeRoute { + + object Params { + + val ITEM: PathParamRule = PathParamRule.of("item", STRING) + val AMOUNT: QueryParamRule = QueryParamRule.of("amount", INT_POSITIVE) + + } + + fun buildRoute(): Route { + return builder() + .method(HttpMethod.GET) + .path("/api/v1/fridge/:%s", Params.ITEM) + .queryParams(Params.AMOUNT) + .bodySizeLimit(1000) + .decodeBody(JSON_OBJECT, false) // Body required by default, have to specify if not required. + .addBlockingHandler { context -> someHandlerThatBlocks(context) } + .addHandler { context -> aRegularHandler(context) } + .addFailureHandler { context -> logFailure(context) } + .addFailureHandler(CATCH_ALL_API_FAILURE_HANDLER) + .build() + } + + private fun aRegularHandler(context: RoutingContext) { + val item = getPathParams(context)[Params.ITEM] + val amount: Int = getQueryParams(context).getOrElse(Params.AMOUNT, 1)!! + val body = getOptionalDecodedBody(context) + + // If body was required you would go + // JsonObject body = RoutingContextEx.getDecodedBody(context); + if (body != null && body.containsKey("types")) { + context.response() + .setStatusCode(200) + .end(String.format("You asked for %d of each %s in the %s category", amount, item, body.getJsonArray("types"))) + } else { + context.response() + .setStatusCode(200) + .end(String.format("You asked for a %s", item)) + } + } + + private fun someHandlerThatBlocks(context: RoutingContext) { + println("Someone is using the fridge, waiting for my turn...") + context?.next() + } + + private fun logFailure(context: RoutingContext) { + println("This should be a logger!") + context?.next() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandlerTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandlerTest.java deleted file mode 100644 index 4971294..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandlerTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.vertxutils.routing.ErrorFormatter; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.BadParamException; -import com.zepben.vertxutils.routing.handlers.params.BodyRule; -import com.zepben.vertxutils.routing.handlers.params.BodyType; -import com.zepben.vertxutils.routing.handlers.params.ValueConversionException; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.*; - -public class DecodeBodyHandlerTest { - - private final BodyRule requiredRule = new BodyRule<>(BodyType.JSON_OBJECT, true); - private final BodyRule notRequiredRule = new BodyRule<>(BodyType.JSON_OBJECT, false); - - private final ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(Object.class); - private final RoutingContext context = mock(RoutingContext.class); - private final HttpServerResponse response = mock(HttpServerResponse.class, RETURNS_SELF); - - @BeforeEach - public void setUp() { - doReturn(response).when(context).response(); - } - - @Test - public void callContextNext() { - new DecodeBodyHandler(notRequiredRule).handle(context); - verify(context).next(); - } - - @Test - public void body() { - DecodeBodyHandler handler = new DecodeBodyHandler(requiredRule); - - JsonObject jsonObject = new JsonObject().put("test", "value"); - doReturn(Buffer.buffer(jsonObject.encode())).when(context).getBody(); - - handler.handle(context); - - verify(context).put(eq(RoutingContextEx.BODY_KEY), paramsCaptor.capture()); - Object decodedBody = paramsCaptor.getValue(); - assertThat(decodedBody, is(jsonObject)); - } - - @Test - public void getFromContext() { - doReturn("expected").when(context).get(RoutingContextEx.BODY_KEY); - assertThat(RoutingContextEx.getDecodedBody(context), is("expected")); - } - - @Test - public void requiredBodyMissing() { - DecodeBodyHandler handler = new DecodeBodyHandler(requiredRule); - handler.handle(context); - verifyBadParamResponse(BadParamException.missingBody()); - } - - @Test - public void bodyNull() { - DecodeBodyHandler handler = new DecodeBodyHandler(notRequiredRule); - handler.handle(context); - - verify(context, never()).put(eq(RoutingContextEx.BODY_KEY), any()); - } - - @Test - public void bodyEmpty() { - doReturn(Buffer.buffer()).when(context).getBody(); - DecodeBodyHandler handler = new DecodeBodyHandler(notRequiredRule); - handler.handle(context); - - verify(context, never()).put(eq(RoutingContextEx.BODY_KEY), any()); - } - - @Test - public void bodyBad() { - DecodeBodyHandler handler = new DecodeBodyHandler(requiredRule); - - Buffer buffer = Buffer.buffer("test"); - doReturn(buffer).when(context).getBody(); - handler.handle(context); - - String reason = ""; - try { - requiredRule.converter().convert(buffer); - } catch (ValueConversionException ex) { - reason = ex.getMessage(); - } - - verifyBadParamResponse(BadParamException.invalidBody(requiredRule, reason)); - } - - private void verifyBadParamResponse(BadParamException e) { - verify(response).setStatusCode(400); - String json = ErrorFormatter.asJson(e.getMessage()); - verify(response).end(json); - verify(context, never()).next(); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandlerTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandlerTest.kt new file mode 100644 index 0000000..0d98c6d --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandlerTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.ErrorFormatter.asJson +import com.zepben.vertxutils.routing.RoutingContextEx.BODY_KEY +import com.zepben.vertxutils.routing.RoutingContextEx.getDecodedBody +import com.zepben.vertxutils.routing.handlers.params.BadParamException +import com.zepben.vertxutils.routing.handlers.params.BadParamException.Companion.invalidBody +import com.zepben.vertxutils.routing.handlers.params.BadParamException.Companion.missingBody +import com.zepben.vertxutils.routing.handlers.params.BodyRule +import com.zepben.vertxutils.routing.handlers.params.BodyType.JSON_OBJECT +import com.zepben.vertxutils.routing.handlers.params.ValueConversionException +import io.vertx.core.http.HttpServerResponse +import io.vertx.core.json.JsonObject +import io.vertx.ext.web.RequestBody +import io.vertx.ext.web.RoutingContext +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.* + +class DecodeBodyHandlerTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val requiredRule = BodyRule(JSON_OBJECT, true) + private val notRequiredRule = BodyRule(JSON_OBJECT, false) + + private val paramsCaptor = ArgumentCaptor.forClass(Any::class.java) + private val context = mock() + private val requestBody = mock(RETURNS_SELF) + private val response = mock(RETURNS_SELF) + + @BeforeEach + fun setUp() { + doReturn(requestBody).`when`(context).body() + doReturn("").`when`(requestBody).asString() + doAnswer { requestBody.asString()?.length }.`when`(requestBody).length() + doReturn(response).`when`(context).response() + } + + @Test + fun callContextNext() { + DecodeBodyHandler(notRequiredRule).handle(context) + verify(context).next() + } + + @Test + fun body() { + val handler = DecodeBodyHandler(requiredRule) + + val jsonObject = JsonObject().put("test", "value") + doReturn(jsonObject.encode()).`when`(requestBody).asString() + + handler.handle(context) + + verify(context).put(eq(BODY_KEY), paramsCaptor.capture()) + val decodedBody = paramsCaptor.getValue() + assertThat(decodedBody, equalTo(jsonObject)) + } + + @Test + fun getFromContext() { + doReturn("expected").`when`(context).get(BODY_KEY) + assertThat(getDecodedBody(context), equalTo("expected")) + } + + @Test + fun requiredBodyMissing() { + val handler = DecodeBodyHandler(requiredRule) + handler.handle(context) + verifyBadParamResponse(missingBody()) + } + + @Test + fun bodyNull() { + val handler = DecodeBodyHandler(notRequiredRule) + handler.handle(context) + + verify(context, never()).put(eq(BODY_KEY), any()) + } + + @Test + fun bodyEmpty() { + doReturn("").`when`(requestBody).asString() + val handler = DecodeBodyHandler(notRequiredRule) + handler.handle(context) + + verify(context, never()).put(eq(BODY_KEY), any()) + } + + @Test + fun bodyBad() { + val handler = DecodeBodyHandler(requiredRule) + + doReturn("test").`when`(requestBody).asString() + handler.handle(context) + + var reason: String? = "" + try { + requiredRule.converter.convert(requestBody) + } catch (ex: ValueConversionException) { + reason = ex.message + } + + verifyBadParamResponse(invalidBody(requiredRule, reason)) + } + + @Test + internal fun `body missing`() { + val handler = DecodeBodyHandler(requiredRule) + + handler.handle(context) + + verifyBadParamResponse(missingBody()) + } + + @Test + internal fun `body missing -1`() { + // Real world testing revealed a missing body actually returns -1 for the length, as opposed to 0 for an empty body. + // The value of `asString` was also `null`, rather than an empty string. + doReturn(-1).`when`(requestBody).length() + doReturn(null).`when`(requestBody).asString() + val handler = DecodeBodyHandler(requiredRule) + + handler.handle(context) + + verifyBadParamResponse(missingBody()) + } + + private fun verifyBadParamResponse(e: BadParamException) { + verify(response).statusCode = 400 + val json = asJson(e.message) + verify(response).end(json) + verify(context, never()).next() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/FaviconHandlerTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/FaviconHandlerTest.java deleted file mode 100644 index 91ce8c9..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/FaviconHandlerTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.mockito.Mockito.*; - -public class FaviconHandlerTest { - - @TempDir - public File temporaryFolder; - - private final Vertx vertx = Vertx.vertx(); - private FaviconHandler handler; - private final Buffer buffer = Buffer.buffer("the icon!"); - private final RoutingContext context = mock(RoutingContext.class); - private final HttpServerResponse response = mock(HttpServerResponse.class); - - @BeforeEach - public void setUp() throws Exception { - Path faviconPath = Paths.get(temporaryFolder.getPath(), "favicon.ico"); - Files.write(faviconPath, buffer.getBytes()); - handler = new FaviconHandler(faviconPath.toString(), 10); - doReturn(response).when(context).response(); - doReturn(vertx).when(context).vertx(); - } - - @AfterEach - public void tearDown() { - vertx.close(); - } - - @Test - public void handle() { - handler.handle(context); - - verify(response).putHeader("Content-Type", "image/x-icon"); - verify(response).putHeader("Content-Length", Integer.toString(buffer.length())); - verify(response).putHeader("Cache-Control", "public, max-age=" + 10); - verify(response).end(buffer); - } - - @Test - public void cachesIcon() throws Exception { - handler.handle(context); - Files.delete(Paths.get(handler.faviconPath())); - handler.handle(context); - verify(response, times(2)).end(buffer); - } - - @Test - public void maxAgeMustBePositive() { - expect(() -> new FaviconHandler(handler.faviconPath(), -1)).toThrow(IllegalArgumentException.class); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/FaviconHandlerTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/FaviconHandlerTest.kt new file mode 100644 index 0000000..d15e679 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/FaviconHandlerTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.http.HttpServerResponse +import io.vertx.ext.web.RoutingContext +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mockito.* +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths + +class FaviconHandlerTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @TempDir + lateinit var temporaryFolder: File + + private val vertx: Vertx = Vertx.vertx() + private val buffer = Buffer.buffer("the icon!") + private val response = mock() + private val context = mock().also { + doReturn(response).`when`(it).response() + doReturn(vertx).`when`(it).vertx() + } + + private val handler: FaviconHandler by lazy { + val faviconPath = Paths.get(temporaryFolder.path, "favicon.ico") + Files.write(faviconPath, buffer.bytes) + FaviconHandler(faviconPath.toString(), 10) + } + + @AfterEach + fun tearDown() { + vertx.close() + } + + @Test + fun handle() { + handler.handle(context) + + verify(response).putHeader("Content-Type", "image/x-icon") + verify(response).putHeader("Content-Length", buffer.length().toString()) + verify(response).putHeader("Cache-Control", "public, max-age=" + 10) + verify(response).end(buffer) + } + + @Test + fun cachesIcon() { + handler.handle(context) + Files.delete(Paths.get(handler.faviconPath())) + + // Previously you got really strange error messages if this was wrong, so make it throw an exception with a useful message. + doThrow(IllegalStateException("should have been cached and not called `setStatusCode`")) + .`when`(response).statusCode = anyInt() + + handler.handle(context) + verify(response, times(2)).end(buffer) + } + + @Test + fun maxAgeMustBePositive() { + expect { FaviconHandler(handler.faviconPath(), -1) }.toThrow() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/PathParamsHandlerTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/PathParamsHandlerTest.java deleted file mode 100644 index 6d811ec..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/PathParamsHandlerTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.vertxutils.routing.ErrorFormatter; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.*; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import java.util.Arrays; -import java.util.Collections; - -import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.*; - -public class PathParamsHandlerTest { - - private final PathParamRule numParam = PathParamRule.of("num", ParamType.INT); - private final PathParamRule num2Param = PathParamRule.of("num2", ParamType.INT); - - private final ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(PathParams.class); - private final RoutingContext context = mock(RoutingContext.class); - private final HttpServerResponse response = mock(HttpServerResponse.class, RETURNS_SELF); - - @BeforeEach - public void setUp() { - doReturn(response).when(context).response(); - } - - @Test - public void callContextNext() { - new PathParamsHandler().handle(context); - verify(context).next(); - } - - @Test - public void pathParam() { - PathParamsHandler handler = new PathParamsHandler(numParam); - - doReturn("4").when(context).pathParam(numParam.name()); - handler.handle(context); - - verify(context).put(eq(RoutingContextEx.PATH_PARAMS_KEY), paramsCaptor.capture()); - PathParams params = paramsCaptor.getValue(); - assertThat(params.get(numParam), is(4)); - } - - @Test - public void getFromContext() { - PathParams expected = new PathParams(Collections.emptyMap()); - doReturn(expected).when(context).get(RoutingContextEx.PATH_PARAMS_KEY); - assertThat(RoutingContextEx.getPathParams(context), is(expected)); - } - - @Test - public void pathParamMissing() { - PathParamsHandler handler = new PathParamsHandler(numParam); - handler.handle(context); - - verifyBadParamResponse(BadParamException.missingParam(numParam.name())); - } - - @Test - public void pathParamBad() { - PathParamsHandler handler = new PathParamsHandler(numParam, num2Param); - doReturn("not a number").when(context).pathParam(numParam.name()); - doReturn("true").when(context).pathParam(num2Param.name()); - - ValueConversionException ex1 = captureException(() -> numParam.converter().convert("not a number"), ValueConversionException.class); - ValueConversionException ex2 = captureException(() -> num2Param.converter().convert("true"), ValueConversionException.class); - handler.handle(context); - - verifyBadParamResponse( - BadParamException.invalidParam(numParam, "not a number", ex1.getMessage()), - BadParamException.invalidParam(num2Param, "true", ex2.getMessage())); - } - - private T captureException(Runnable runnable, Class expectedExType) { - try { - runnable.run(); - } catch (Exception ex) { - return expectedExType.cast(ex); - } - - throw new AssertionError("Expected exception but none was thrown"); - } - - private void verifyBadParamResponse(BadParamException... e) { - verify(response).setStatusCode(400); - String json = ErrorFormatter.asJson(Arrays.stream(e).map(Throwable::getMessage).collect(toList())); - verify(response).end(json); - verify(context, never()).next(); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/PathParamsHandlerTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/PathParamsHandlerTest.kt new file mode 100644 index 0000000..6a5a6f6 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/PathParamsHandlerTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.ErrorFormatter.asJson +import com.zepben.vertxutils.routing.RoutingContextEx.PATH_PARAMS_KEY +import com.zepben.vertxutils.routing.RoutingContextEx.getPathParams +import com.zepben.vertxutils.routing.handlers.params.* +import io.vertx.core.http.HttpServerResponse +import io.vertx.ext.web.RoutingContext +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.sameInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.* + +class PathParamsHandlerTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val numParam = PathParamRule.of("num", ParamType.INT) + private val num2Param = PathParamRule.of("num2", ParamType.INT) + + private val paramsCaptor = ArgumentCaptor.forClass(PathParams::class.java) + private val response = mock(RETURNS_SELF) + private val context = mock().also { + doReturn(response).`when`(it).response() + } + + @Test + fun callContextNext() { + PathParamsHandler().handle(context) + verify(context).next() + } + + @Test + fun pathParam() { + val handler = PathParamsHandler(numParam) + + doReturn("4").`when`(context).pathParam(numParam.name) + handler.handle(context) + + verify(context).put(eq(PATH_PARAMS_KEY), paramsCaptor.capture()) + val params = paramsCaptor.getValue() + assertThat(params[numParam], equalTo(4)) + } + + @Test + fun getFromContext() { + val expected = PathParams(emptyMap()) + doReturn(expected).`when`(context).get(PATH_PARAMS_KEY) + assertThat(getPathParams(context), sameInstance(expected)) + } + + @Test + fun pathParamMissing() { + val handler = PathParamsHandler(numParam) + handler.handle(context) + + verifyBadParamResponse(BadParamException.missingParam(numParam.name)) + } + + @Test + fun pathParamBad() { + val handler = PathParamsHandler(numParam, num2Param) + doReturn("not a number").`when`(context).pathParam(numParam.name) + doReturn("true").`when`(context).pathParam(num2Param.name) + + val ex1 = captureException { numParam.converter.convert("not a number") } + val ex2 = captureException { num2Param.converter.convert("true") } + handler.handle(context) + + verifyBadParamResponse( + BadParamException.invalidParam(numParam, "not a number", ex1.message), + BadParamException.invalidParam(num2Param, "true", ex2.message), + ) + } + + private inline fun captureException(runnable: Runnable): T { + try { + runnable.run() + } catch (ex: Exception) { + return ex as T + } + + throw AssertionError("Expected exception but none was thrown") + } + + private fun verifyBadParamResponse(vararg e: BadParamException) { + verify(response).statusCode = 400 + val json = asJson(e.map { it.message }) + verify(response).end(json) + verify(context, never()).next() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandlerTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandlerTest.java deleted file mode 100644 index 35ec3d0..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandlerTest.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers; - -import com.zepben.vertxutils.routing.ErrorFormatter; -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.*; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import java.util.Arrays; -import java.util.List; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.*; - -public class QueryParamsHandlerTest { - - private final QueryParamRule noDefaultParam = QueryParamRule.of("noDefault", ParamType.STRING); - private final QueryParamRule defaultParam = QueryParamRule.of("hasDefault", ParamType.INT, 1); - private final QueryParamRule requiredParam = QueryParamRule.ofRequired("required", ParamType.BOOL); - - private final ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(QueryParams.class); - private final RoutingContext context = mock(RoutingContext.class); - private final HttpServerResponse response = mock(HttpServerResponse.class, RETURNS_SELF); - - @BeforeEach - public void setUp() { - doReturn(response).when(context).response(); - } - - @Test - public void detectsDuplicateRules() { - expect(() -> new QueryParamsHandler(requiredParam, requiredParam)).toThrow(IllegalArgumentException.class); - } - - @Test - public void callContextNext() { - new QueryParamsHandler().handle(context); - verify(context).next(); - } - - @Test - public void queryParamNoDefault() { - QueryParamsHandler handler = new QueryParamsHandler(noDefaultParam); - handler.handle(context); - - verify(context).put(eq(RoutingContextEx.QUERY_PARAMS_KEY), paramsCaptor.capture()); - QueryParams params = paramsCaptor.getValue(); - assertThat(params.exists(noDefaultParam), is(false)); - } - - @Test - public void queryParamList() { - QueryParamsHandler handler = new QueryParamsHandler(noDefaultParam); - - List rawParams = Arrays.asList("a", "b"); - doReturn(rawParams).when(context).queryParam(noDefaultParam.name()); - handler.handle(context); - - verify(context).put(eq(RoutingContextEx.QUERY_PARAMS_KEY), paramsCaptor.capture()); - QueryParams params = paramsCaptor.getValue(); - assertThat(params.getAll(noDefaultParam), is(rawParams)); - } - - @Test - public void queryParamWithDefault() { - QueryParamsHandler handler = new QueryParamsHandler(defaultParam); - - doReturn(emptyList()).when(context).queryParam(defaultParam.name()); - handler.handle(context); - - verify(context).put(eq(RoutingContextEx.QUERY_PARAMS_KEY), paramsCaptor.capture()); - QueryParams params = paramsCaptor.getValue(); - assertThat(params.exists(defaultParam), is(false)); - assertThat(params.get(defaultParam), is(defaultParam.defaultValue())); - } - - @Test - public void queryParamRequired() { - QueryParamsHandler handler = new QueryParamsHandler(requiredParam); - - doReturn(singletonList("true")).when(context).queryParam(requiredParam.name()); - handler.handle(context); - - verify(context).put(eq(RoutingContextEx.QUERY_PARAMS_KEY), paramsCaptor.capture()); - QueryParams params = paramsCaptor.getValue(); - assertThat(params.exists(requiredParam), is(true)); - assertThat(params.get(requiredParam), is(true)); - } - - @Test - public void queryParamRequiredMissing() { - QueryParamsHandler handler = new QueryParamsHandler(requiredParam); - handler.handle(context); - - verify(context, never()).put(any(), any()); - verifyBadParamResponse(BadParamException.missingParam(requiredParam.name())); - } - - @Test - public void queryParamBad() { - QueryParamsHandler handler = new QueryParamsHandler(defaultParam, requiredParam); - doReturn(singletonList("not a number")).when(context).queryParam(defaultParam.name()); - ValueConversionException ex = captureException(() -> defaultParam.converter().convert("not a number"), ValueConversionException.class); - - handler.handle(context); - - verifyBadParamResponse( - BadParamException.invalidParam(defaultParam, "not a number", ex.getMessage()), - BadParamException.missingParam(requiredParam.name())); - } - - private T captureException(Runnable runnable, Class expectedExType) { - try { - runnable.run(); - } catch (Exception ex) { - return expectedExType.cast(ex); - } - - throw new AssertionError("Expected exception but none was thrown"); - } - - private void verifyBadParamResponse(BadParamException... e) { - verify(response).setStatusCode(400); - String json = ErrorFormatter.asJson(Arrays.stream(e).map(Throwable::getMessage).collect(toList())); - verify(response).end(json); - verify(context, never()).next(); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandlerTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandlerTest.kt new file mode 100644 index 0000000..30f74ec --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/QueryParamsHandlerTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.ErrorFormatter.asJson +import com.zepben.vertxutils.routing.RoutingContextEx.QUERY_PARAMS_KEY +import com.zepben.vertxutils.routing.handlers.params.* +import io.vertx.core.http.HttpServerResponse +import io.vertx.ext.web.RoutingContext +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.* + +class QueryParamsHandlerTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val noDefaultParam = QueryParamRule.of("noDefault", ParamType.STRING) + private val defaultParam = QueryParamRule.of("hasDefault", ParamType.INT, 1) + private val requiredParam = QueryParamRule.ofRequired("required", ParamType.BOOL) + + private val paramsCaptor = ArgumentCaptor.forClass(QueryParams::class.java) + private val response = mock(RETURNS_SELF) + private val context = mock().also { + doReturn(response).`when`(it).response() + } + + @Test + fun detectsDuplicateRules() { + expect { QueryParamsHandler(requiredParam, requiredParam) }.toThrow() + } + + @Test + fun callContextNext() { + QueryParamsHandler().handle(context) + verify(context).next() + } + + @Test + fun queryParamNoDefault() { + val handler = QueryParamsHandler(noDefaultParam) + handler.handle(context) + + verify(context).put(eq(QUERY_PARAMS_KEY), paramsCaptor.capture()) + val params = paramsCaptor.getValue() + assertThat(params.contains(noDefaultParam), equalTo(false)) + } + + @Test + fun queryParamList() { + val handler = QueryParamsHandler(noDefaultParam) + + val rawParams = mutableListOf("a", "b") + doReturn(rawParams).`when`(context).queryParam(noDefaultParam.name) + handler.handle(context) + + verify(context).put(eq(QUERY_PARAMS_KEY), paramsCaptor.capture()) + val params = paramsCaptor.getValue() + assertThat(params.getAll(noDefaultParam), equalTo(rawParams)) + } + + @Test + fun queryParamWithDefault() { + val handler = QueryParamsHandler(defaultParam) + + doReturn(listOf()).`when`(context).queryParam(defaultParam.name) + handler.handle(context) + + verify(context).put(eq(QUERY_PARAMS_KEY), paramsCaptor.capture()) + val params = paramsCaptor.getValue() + assertThat(params.contains(defaultParam), equalTo(false)) + assertThat(params[defaultParam], equalTo(defaultParam.defaultValue)) + } + + @Test + fun queryParamRequired() { + val handler = QueryParamsHandler(requiredParam) + + doReturn(mutableListOf("true")).`when`(context).queryParam(requiredParam.name) + handler.handle(context) + + verify(context).put(eq(QUERY_PARAMS_KEY), paramsCaptor.capture()) + val params = paramsCaptor.getValue() + assertThat(params.contains(requiredParam), equalTo(true)) + assertThat(params[requiredParam], equalTo(true)) + } + + @Test + fun queryParamRequiredMissing() { + val handler = QueryParamsHandler(requiredParam) + handler.handle(context) + + verify(context, never()).put(any(), any()) + verifyBadParamResponse(BadParamException.missingParam(requiredParam.name)) + } + + @Test + fun queryParamBad() { + val handler = QueryParamsHandler(defaultParam, requiredParam) + doReturn(mutableListOf("not a number")).`when`(context).queryParam(defaultParam.name) + val ex = captureException { defaultParam.converter.convert("not a number") } + + handler.handle(context) + + verifyBadParamResponse( + BadParamException.invalidParam(defaultParam, "not a number", ex.message), + BadParamException.missingParam(requiredParam.name), + ) + } + + private inline fun captureException(runnable: Runnable): T { + try { + runnable.run() + } catch (ex: Exception) { + return ex as T + } + + throw AssertionError("Expected exception but none was thrown") + } + + private fun verifyBadParamResponse(vararg e: BadParamException) { + verify(response).statusCode = 400 + val json = asJson(e.map { it.message }) + verify(response).end(json) + verify(context, never()).next() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/BodyTypeTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/params/BodyTypeTest.java deleted file mode 100644 index 0b0af6f..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/params/BodyTypeTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import org.junit.jupiter.api.Test; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -public class BodyTypeTest { - - @Test - public void jsonObject() { - JsonObject jsonObject = new JsonObject().put("test", "value"); - JsonObject converted = BodyType.JSON_OBJECT.convert(Buffer.buffer(jsonObject.encode())); - assertThat(converted, equalTo(jsonObject)); - } - - @Test - public void badJsonObjectReturnsNull() { - expect(() -> BodyType.JSON_OBJECT.convert(Buffer.buffer("rubbish"))).toThrow(ValueConversionException.class); - } - - @Test - public void emptyJsonObjectReturnsNull() { - expect(() -> BodyType.JSON_OBJECT.convert(Buffer.buffer())).toThrow(ValueConversionException.class); - } - - @Test - public void jsonArray() { - JsonArray jsonArray = new JsonArray().add(1).add("a string"); - JsonArray converted = BodyType.JSON_ARRAY.convert(Buffer.buffer(jsonArray.encode())); - assertThat(converted, equalTo(jsonArray)); - } - - @Test - public void badJsonArrayReturnsNull() { - expect(() -> BodyType.JSON_ARRAY.convert(Buffer.buffer("rubbish"))).toThrow(ValueConversionException.class); - } - - @Test - public void emptyJsonArrayReturnsNull() { - expect(() -> BodyType.JSON_ARRAY.convert(Buffer.buffer())).toThrow(ValueConversionException.class); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/BodyTypeTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/params/BodyTypeTest.kt new file mode 100644 index 0000000..c03d2db --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/params/BodyTypeTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.handlers.params.BodyType.JSON_ARRAY +import com.zepben.vertxutils.routing.handlers.params.BodyType.JSON_OBJECT +import io.vertx.core.buffer.Buffer +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import io.vertx.ext.web.impl.RequestBodyImpl +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.mock + +class BodyTypeTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + var body: RequestBodyImpl = RequestBodyImpl(mock()) + + @Test + fun jsonObject() { + val jsonObject = JsonObject().put("test", "value") + body.setBuffer(Buffer.buffer(jsonObject.encode())) + + val converted = JSON_OBJECT.convert(body) + assertThat(converted, equalTo(jsonObject)) + } + + @Test + fun badJsonObjectReturnsNull() { + body.setBuffer(Buffer.buffer("rubbish")) + + expect { JSON_OBJECT.convert(body) }.toThrow() + } + + @Test + fun emptyJsonObjectReturnsNull() { + body.setBuffer(Buffer.buffer()) + + expect { JSON_OBJECT.convert(body) }.toThrow() + } + + @Test + fun jsonArray() { + val jsonArray = JsonArray().add(1).add("a string") + body.setBuffer(Buffer.buffer(jsonArray.encode())) + val converted = JSON_ARRAY.convert(body) + assertThat(converted, equalTo(jsonArray)) + } + + @Test + fun badJsonArrayReturnsNull() { + body.setBuffer(Buffer.buffer("rubbish")) + expect { JSON_ARRAY.convert(body) }.toThrow() + } + + @Test + fun emptyJsonArrayReturnsNull() { + body.setBuffer(Buffer.buffer()) + expect { JSON_ARRAY.convert(body) }.toThrow() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/ParamTypeTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/params/ParamTypeTest.java deleted file mode 100644 index 43a3409..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/params/ParamTypeTest.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalTime; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -public class ParamTypeTest { - - @Test - public void stringParam() { - testValid(ParamType.STRING, "a%20string", "a string"); - } - - @Test - public void invlidStringParam() { - testInvalid(ParamType.STRING, "%x"); - } - - @Test - public void intParam() { - testValid(ParamType.INT, "-4", -4); - } - - @Test - public void invalidIntParam() { - testInvalid(ParamType.INT, "bad"); - } - - @Test - public void intPositiveParam() { - testValid(ParamType.INT_POSITIVE, "4", 4); - } - - @Test - public void invalidIntPositiveParam() { - testInvalid(ParamType.INT_POSITIVE, "-4"); - } - - @Test - public void longParam() { - testValid(ParamType.LONG, Long.toString(Long.MIN_VALUE), Long.MIN_VALUE); - } - - @Test - public void invalidLongParam() { - testInvalid(ParamType.LONG, "bad"); - } - - @Test - public void longPositiveParam() { - testValid(ParamType.LONG_POSITIVE, Long.toString(Long.MAX_VALUE), Long.MAX_VALUE); - } - - @Test - public void invalidLongPositiveParam() { - testInvalid(ParamType.LONG_POSITIVE, "-4"); - } - - @Test - public void floatParam() { - testValid(ParamType.FLOAT, "-1.2", -1.2f); - } - - @Test - public void invalidFloatParam() { - testInvalid(ParamType.FLOAT, "bad"); - } - - @Test - public void floatPositiveParam() { - testValid(ParamType.FLOAT, "2.3", 2.3f); - } - - @Test - public void invalidFloatPositiveParam() { - testInvalid(ParamType.FLOAT_POSITIVE, "-1.1"); - } - - @Test - public void badFloatPositiveParam() { - testInvalid(ParamType.FLOAT_POSITIVE, "bad"); - } - - @Test - public void doubleParam() { - testValid(ParamType.DOUBLE, "-1.2", -1.2); - } - - @Test - public void invalidDoubleParam() { - testInvalid(ParamType.DOUBLE, "bad"); - } - - @Test - public void doublePositiveParam() { - testValid(ParamType.DOUBLE, "2.3", 2.3); - } - - @Test - public void badDoublePositiveParam() { - testInvalid(ParamType.DOUBLE, "bad"); - } - - - @Test - public void invalidDoublePositiveParam() { - testInvalid(ParamType.DOUBLE_POSITIVE, "-1.1"); - } - - @Test - public void boolParam() { - testValid(ParamType.BOOL, "tRuE", true); - testValid(ParamType.BOOL, "1", true); - testValid(ParamType.BOOL, "0", false); - testValid(ParamType.BOOL, "fAlSe", false); - testValid(ParamType.BOOL, "2", false); - testValid(ParamType.BOOL, "yes", false); - } - - @Test - public void dateParam() { - testValid(ParamType.LOCAL_DATE, "2018-06-25", LocalDate.of(2018, 6, 25)); - testInvalid(ParamType.LOCAL_DATE, "25-06-2018"); - } - - @Test - public void timeParam() { - testValid(ParamType.LOCAL_TIME, "6:45", LocalTime.of(6, 45)); - testValid(ParamType.LOCAL_TIME, "06:45", LocalTime.of(6, 45)); - testValid(ParamType.LOCAL_TIME, "20:18", LocalTime.of(20, 18)); - testInvalid(ParamType.LOCAL_TIME, "25:06"); - } - - @Test - public void instantParam() { - Instant now = Instant.now(); - testValid(ParamType.INSTANT, now.toString(), now); - testInvalid(ParamType.INSTANT, "2018-06-01 12:01:14.000Z"); - } - - enum TestEnum {VALUE_1, VALUE_2} - - @Test - public void enumParam() { - testValid(ParamType.ofEnum(TestEnum.class), "VALUE_1", TestEnum.VALUE_1); - testValid(ParamType.ofEnum(TestEnum.class), "VALUE_2", TestEnum.VALUE_2); - testValid(ParamType.ofEnum(TestEnum.class), "value_1", TestEnum.VALUE_1); - testValid(ParamType.ofEnum(TestEnum.class), "value_2", TestEnum.VALUE_2); - testInvalid(ParamType.ofEnum(TestEnum.class), "VALUE_3"); - } - - private void testValid(RequestValueConverter converter, String param, T expected) { - T value = converter.convert(param); - assertThat(value, equalTo(expected)); - } - - private void testInvalid(RequestValueConverter converter, String param) { - expect(() -> converter.convert(param)).toThrow(ValueConversionException.class); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/ParamTypeTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/params/ParamTypeTest.kt new file mode 100644 index 0000000..9c55fe6 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/params/ParamTypeTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.handlers.params.ParamType.BOOL +import com.zepben.vertxutils.routing.handlers.params.ParamType.DOUBLE +import com.zepben.vertxutils.routing.handlers.params.ParamType.DOUBLE_POSITIVE +import com.zepben.vertxutils.routing.handlers.params.ParamType.FLOAT +import com.zepben.vertxutils.routing.handlers.params.ParamType.FLOAT_POSITIVE +import com.zepben.vertxutils.routing.handlers.params.ParamType.INSTANT +import com.zepben.vertxutils.routing.handlers.params.ParamType.INT +import com.zepben.vertxutils.routing.handlers.params.ParamType.INT_POSITIVE +import com.zepben.vertxutils.routing.handlers.params.ParamType.LOCAL_DATE +import com.zepben.vertxutils.routing.handlers.params.ParamType.LOCAL_TIME +import com.zepben.vertxutils.routing.handlers.params.ParamType.LONG +import com.zepben.vertxutils.routing.handlers.params.ParamType.LONG_POSITIVE +import com.zepben.vertxutils.routing.handlers.params.ParamType.STRING +import com.zepben.vertxutils.routing.handlers.params.ParamType.ofEnum +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime + +class ParamTypeTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun stringParam() { + testValid(STRING, "a%20string", "a string") + } + + @Test + fun invalidStringParam() { + testInvalid(STRING, "%x") + } + + @Test + fun intParam() { + testValid(INT, "-4", -4) + } + + @Test + fun invalidIntParam() { + testInvalid(INT, "bad") + } + + @Test + fun intPositiveParam() { + testValid(INT_POSITIVE, "4", 4) + } + + @Test + fun invalidIntPositiveParam() { + testInvalid(INT_POSITIVE, "-4") + } + + @Test + fun longParam() { + testValid(LONG, Long.MIN_VALUE.toString(), Long.MIN_VALUE) + } + + @Test + fun invalidLongParam() { + testInvalid(LONG, "bad") + } + + @Test + fun longPositiveParam() { + testValid(LONG_POSITIVE, Long.MAX_VALUE.toString(), Long.MAX_VALUE) + } + + @Test + fun invalidLongPositiveParam() { + testInvalid(LONG_POSITIVE, "-4") + } + + @Test + fun floatParam() { + testValid(FLOAT, "-1.2", -1.2f) + } + + @Test + fun invalidFloatParam() { + testInvalid(FLOAT, "bad") + } + + @Test + fun floatPositiveParam() { + testValid(FLOAT, "2.3", 2.3f) + } + + @Test + fun invalidFloatPositiveParam() { + testInvalid(FLOAT_POSITIVE, "-1.1") + } + + @Test + fun badFloatPositiveParam() { + testInvalid(FLOAT_POSITIVE, "bad") + } + + @Test + fun doubleParam() { + testValid(DOUBLE, "-1.2", -1.2) + } + + @Test + fun invalidDoubleParam() { + testInvalid(DOUBLE, "bad") + } + + @Test + fun doublePositiveParam() { + testValid(DOUBLE, "2.3", 2.3) + } + + @Test + fun badDoublePositiveParam() { + testInvalid(DOUBLE, "bad") + } + + @Test + fun invalidDoublePositiveParam() { + testInvalid(DOUBLE_POSITIVE, "-1.1") + } + + @Test + fun boolParam() { + testValid(BOOL, "tRuE", true) + testValid(BOOL, "1", true) + testValid(BOOL, "0", false) + testValid(BOOL, "fAlSe", false) + testValid(BOOL, "2", false) + testValid(BOOL, "yes", false) + } + + @Test + fun dateParam() { + testValid(LOCAL_DATE, "2018-06-25", LocalDate.of(2018, 6, 25)) + testInvalid(LOCAL_DATE, "25-06-2018") + } + + @Test + fun timeParam() { + testValid(LOCAL_TIME, "6:45", LocalTime.of(6, 45)) + testValid(LOCAL_TIME, "06:45", LocalTime.of(6, 45)) + testValid(LOCAL_TIME, "20:18", LocalTime.of(20, 18)) + testInvalid(LOCAL_TIME, "25:06") + } + + @Test + fun instantParam() { + val now = Instant.now() + testValid(INSTANT, now.toString(), now) + testInvalid(INSTANT, "2018-06-01 12:01:14.000Z") + } + + internal enum class TestEnum { + VALUE_1, VALUE_2 + } + + @Test + fun enumParam() { + testValid(ofEnum(), "VALUE_1", TestEnum.VALUE_1) + testValid(ofEnum(), "VALUE_2", TestEnum.VALUE_2) + testValid(ofEnum(), "value_1", TestEnum.VALUE_1) + testValid(ofEnum(), "value_2", TestEnum.VALUE_2) + testInvalid(ofEnum(), "VALUE_3") + } + + private fun testValid(converter: RequestValueConverter, param: String, expected: T) { + assertThat(converter.convert(param), equalTo(expected)) + } + + private fun testInvalid(converter: RequestValueConverter, param: String) { + expect { converter.convert(param) }.toThrow() + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/PathParamsTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/params/PathParamsTest.java deleted file mode 100644 index 26b439c..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/params/PathParamsTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.google.common.collect.ImmutableMap; -import org.junit.jupiter.api.Test; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -public class PathParamsTest { - - private final PathParamRule p1 = PathParamRule.of("p1", ParamType.STRING); - private final PathParamRule p2 = PathParamRule.of("p2", ParamType.INT); - private final PathParamRule p3 = PathParamRule.of("p3", ParamType.INT); - private final PathParamRule notRegistered = PathParamRule.of("none", ParamType.STRING); - - private final PathParams params = new PathParams(ImmutableMap.of(p1.name(), "aString", p2.name(), 4)); - - @Test - public void get() { - assertThat(params.get(p1), is("aString")); - assertThat(params.get(p2), is(4)); - expect(() -> params.get(p3)).toThrow(IllegalArgumentException.class); - expect(() -> params.get(notRegistered)).toThrow(IllegalArgumentException.class); - } - - @Test - public void exists() { - assertThat(params.exists(p1), is(true)); - assertThat(params.exists(p2), is(true)); - assertThat(params.exists(p3), is(false)); - assertThat(params.exists(notRegistered), is(false)); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/PathParamsTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/params/PathParamsTest.kt new file mode 100644 index 0000000..94f615c --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/params/PathParamsTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.handlers.params.ParamType.INT +import com.zepben.vertxutils.routing.handlers.params.ParamType.STRING +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class PathParamsTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val p1 = PathParamRule.of("p1", STRING) + private val p2 = PathParamRule.of("p2", INT) + private val p3 = PathParamRule.of("p3", INT) + private val notRegistered = PathParamRule.of("none", STRING) + + private val params = PathParams(mapOf(p1.name to "aString", p2.name to 4)) + + @Test + fun get() { + assertThat(params[p1], equalTo("aString")) + assertThat(params[p2], equalTo(4)) + expect { params[p3] }.toThrow() + expect { params[notRegistered] }.toThrow() + } + + @Test + fun contains() { + assertThat(p1 in params, equalTo(true)) + assertThat(p2 in params, equalTo(true)) + assertThat(p3 in params, equalTo(false)) + assertThat(notRegistered in params, equalTo(false)) + } +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRuleTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRuleTest.java deleted file mode 100644 index fbff1cd..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRuleTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; - -public class QueryParamRuleTest { - - private final RequestValueConverter converter = RequestValueConverter.create("string", s -> s); - - @Test - public void basicConstructor() { - QueryParamRule rule = QueryParamRule.of("test", converter); - assertThat(rule.name(), is("test")); - assertThat(rule.converter(), is(converter)); - assertThat(rule.isRequired(), is(false)); - assertThat(rule.defaultValue(), is(nullValue())); - } - - @Test - public void defaultValueConstructor() { - QueryParamRule rule = QueryParamRule.of("test", converter, "default"); - assertThat(rule.name(), is("test")); - assertThat(rule.converter(), is(converter)); - assertThat(rule.isRequired(), is(false)); - assertThat(rule.defaultValue(), is("default")); - } - - @Test - public void isRequiredConstructor() { - QueryParamRule rule = QueryParamRule.ofRequired("test", converter); - assertThat(rule.name(), is("test")); - assertThat(rule.converter(), is(converter)); - assertThat(rule.isRequired(), is(true)); - assertThat(rule.defaultValue(), is(nullValue())); - } -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRuleTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRuleTest.kt new file mode 100644 index 0000000..5671e7d --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamRuleTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.handlers.params.RequestValueConverter.Companion.create +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class QueryParamRuleTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val converter = create("string") { s: String -> s } + + @Test + fun basicConstructor() { + val rule = QueryParamRule.of("test", converter) + assertThat(rule.name, equalTo("test")) + assertThat(rule.converter, equalTo(converter)) + assertThat(rule.isRequired, equalTo(false)) + assertThat(rule.defaultValue, nullValue()) + } + + @Test + fun defaultValueConstructor() { + val rule = QueryParamRule.of("test", converter, "default") + assertThat(rule.name, equalTo("test")) + assertThat(rule.converter, equalTo(converter)) + assertThat(rule.isRequired, equalTo(false)) + assertThat(rule.defaultValue, equalTo("default")) + } + + @Test + fun ofRequiredConstructor() { + val rule = QueryParamRule.ofRequired("test", converter) + assertThat(rule.name, equalTo("test")) + assertThat(rule.converter, equalTo(converter)) + assertThat(rule.isRequired, equalTo(true)) + assertThat(rule.defaultValue, nullValue()) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamsTest.java b/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamsTest.java deleted file mode 100644 index 38f2851..0000000 --- a/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamsTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.routing.handlers.params; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.zepben.testutils.exception.ExpectException.expect; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; - -public class QueryParamsTest { - - private final QueryParamRule p1 = QueryParamRule.of("p1", ParamType.STRING); - private final QueryParamRule p2 = QueryParamRule.of("p2", ParamType.STRING, "default2"); - private final QueryParamRule p3 = QueryParamRule.of("p3", ParamType.STRING); - private final QueryParamRule p4 = QueryParamRule.of("p4", ParamType.STRING, "default4"); - private final QueryParamRule notRegistered = QueryParamRule.of("none", ParamType.STRING); - - private QueryParams params; - - @BeforeEach - public void setUp() { - Map> paramValues = new HashMap<>(); - paramValues.put(p1.name(), ImmutableList.of("aString", "4")); - paramValues.put(p2.name(), ImmutableList.of("value")); - params = new QueryParams(ImmutableSet.of(p1, p2, p3, p4), paramValues); - } - - @Test - public void get() { - assertThat(params.get(p1), is("aString")); - assertThat(params.get(p2), is("value")); - expect(() -> params.get(p3)).toThrow(IllegalArgumentException.class); - assertThat(params.get(p4), is(p4.defaultValue())); - expect(() -> params.get(notRegistered)).toThrow(IllegalArgumentException.class); - } - - @Test - public void getOrElse() { - assertThat(params.getOrElse(p1, "other1"), is(params.get(p1))); - assertThat(params.getOrElse(p2, "other2"), is(params.get(p2))); - assertThat(params.getOrElse(p3, "other3"), is("other3")); - assertThat(params.getOrElse(p3, "other4"), is("other4")); - assertThat(params.getOrElse(p3, null), nullValue()); - expect(() -> params.getOrElse(notRegistered, "other")).toThrow(IllegalArgumentException.class); - } - - @Test - public void getAll() { - assertThat(params.getAll(p1), contains("aString", "4")); - assertThat(params.getAll(p2), contains("value")); - expect(() -> params.getAll(p3)).toThrow(IllegalArgumentException.class); - assertThat(params.getAll(p4), contains(p4.defaultValue())); - expect(() -> params.getAll(notRegistered)).toThrow(IllegalArgumentException.class); - } - - @Test - public void getAllOrElse() { - assertThat(params.getAllOrElse(p1, "other1"), contains(params.getAll(p1).toArray())); - assertThat(params.getAllOrElse(p2, "other2"), contains(params.getAll(p2).toArray())); - assertThat(params.getAllOrElse(p3, "other3"), contains("other3")); - assertThat(params.getAllOrElse(p4, "other4"), contains("other4")); - expect(() -> params.getAllOrElse(notRegistered, "other4")).toThrow(IllegalArgumentException.class); - - assertThat(params.getAllOrElse(p1, ImmutableList.of("other1", "other2")), contains(params.getAll(p1).toArray())); - assertThat(params.getAllOrElse(p2, ImmutableList.of("other3", "other4")), contains(params.getAll(p2).toArray())); - assertThat(params.getAllOrElse(p3, ImmutableList.of("other5", "other6")), contains("other5", "other6")); - assertThat(params.getAllOrElse(p3, ImmutableList.of("other7", "other8")), contains("other7", "other8")); - expect(() -> params.getAllOrElse(notRegistered, ImmutableList.of("other7", "other8"))).toThrow(IllegalArgumentException.class); - } - - @Test - public void exists() { - assertThat(params.exists(p1), is(true)); - assertThat(params.exists(p2), is(true)); - assertThat(params.exists(p3), is(false)); - assertThat(params.exists(p4), is(false)); - assertThat(params.exists(notRegistered), is(false)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamsTest.kt b/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamsTest.kt new file mode 100644 index 0000000..10a35aa --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/routing/handlers/params/QueryParamsTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.routing.handlers.params + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class QueryParamsTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + private val p1 = QueryParamRule.of("p1", ParamType.STRING) + private val p2 = QueryParamRule.of("p2", ParamType.STRING, "default2") + private val p3 = QueryParamRule.of("p3", ParamType.STRING) + private val p4 = QueryParamRule.of("p4", ParamType.STRING, "default4") + private val notRegistered = QueryParamRule.of("none", ParamType.STRING) + + private val params = QueryParams( + setOf(p1, p2, p3, p4), + mapOf( + p1.name to listOf("aString", "4"), + p2.name to listOf("value"), + ), + ) + + @Test + fun get() { + assertThat(params[p1], equalTo("aString")) + assertThat(params[p2], equalTo("value")) + expect { params[p3] }.toThrow() + assertThat(params[p4], equalTo(p4.defaultValue)) + expect { params[notRegistered] }.toThrow() + } + + @Test + fun getOrElse() { + assertThat(params.getOrElse(p1, "other1"), equalTo(params[p1])) + assertThat(params.getOrElse(p2, "other2"), equalTo(params[p2])) + assertThat(params.getOrElse(p3, "other3"), equalTo("other3")) + assertThat(params.getOrElse(p3, "other4"), equalTo("other4")) + assertThat(params.getOrElse(p3, null), nullValue()) + expect { params.getOrElse(notRegistered, "other") }.toThrow() + } + + @Test + fun getAll() { + assertThat(params.getAll(p1), contains("aString", "4")) + assertThat(params.getAll(p2), contains("value")) + expect { params.getAll(p3) }.toThrow() + assertThat(params.getAll(p4), contains(p4.defaultValue)) + expect { params.getAll(notRegistered) }.toThrow() + } + + @Test + fun getAllOrElse() { + assertThat( + params.getAllOrElse(p1, "other1"), + contains(*params.getAll(p1).toTypedArray()), + ) + assertThat( + params.getAllOrElse(p2, "other2"), + contains(*params.getAll(p2).toTypedArray()), + ) + assertThat(params.getAllOrElse(p3, "other3"), contains("other3")) + assertThat(params.getAllOrElse(p4, "other4"), contains("other4")) + expect { params.getAllOrElse(notRegistered, "other4") }.toThrow() + + assertThat( + params.getAllOrElse(p1, listOf("other1", "other2")), + contains(*params.getAll(p1).toTypedArray()), + ) + assertThat( + params.getAllOrElse(p2, listOf("other3", "other4")), + contains(*params.getAll(p2).toTypedArray()), + ) + assertThat( + params.getAllOrElse(p3, listOf("other5", "other6")), + contains("other5", "other6"), + ) + assertThat( + params.getAllOrElse(p3, listOf("other7", "other8")), + contains("other7", "other8"), + ) + expect { params.getAllOrElse(notRegistered, listOf("other7", "other8")) }.toThrow() + } + + @Test + fun exists() { + assertThat(params.contains(p1), equalTo(true)) + assertThat(params.contains(p2), equalTo(true)) + assertThat(params.contains(p3), equalTo(false)) + assertThat(params.contains(p4), equalTo(false)) + assertThat(params.contains(notRegistered), equalTo(false)) + } + + @Test + internal fun `has non-nullable default or throws when using get`() { + // + // NOTE: This is here to make sure we return non-null query parameters, as this is not detected with passing stuff + // directly to hamcrest. + fun validate(value: String, expected: String) = + assertThat(value, equalTo(expected)) + + validate(params[p4], p4.defaultValue!!) + + expect { params[p3] }.toThrow().withMessage( + "INTERNAL ERROR: Param ${p3.name} has no values and no default. Either mark the param as required, provide a default or use with getOrElse or getAllOrElse.", + ) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/testing/DeployRestVerticleHelperTest.java b/src/test/java/com/zepben/vertxutils/testing/DeployRestVerticleHelperTest.java deleted file mode 100644 index 5e970be..0000000 --- a/src/test/java/com/zepben/vertxutils/testing/DeployRestVerticleHelperTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2022 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.testing; - -import com.zepben.testutils.junit.SystemLogExtension; -import io.vertx.core.json.JsonObject; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static com.zepben.testutils.exception.ExpectException.expect; - -public class DeployRestVerticleHelperTest { - - @RegisterExtension - public static final SystemLogExtension systemErr = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess(); - - @AfterEach - void afterEach() { - TestVerticle.setOnStop(() -> {}); - TestVerticle.setOnStart(() -> {}); - } - - @Test - public void coverageOnlyTest() throws Exception { - // TODO: Make this an actual test. - DeployRestVerticleHelper helper = buildHelper(); - helper.requestSpec(); - helper.getRandomPortNumber(); - helper.close(); - - TestVerticle.setOnStart((promise) -> promise.fail("test start fail")); - expect(this::buildHelper).toThrow(AssertionError.class); - } - - private DeployRestVerticleHelper buildHelper() { - return new DeployRestVerticleHelper(TestVerticle.class, new JsonObject()); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/testing/DeployRestVerticleHelperTest.kt b/src/test/java/com/zepben/vertxutils/testing/DeployRestVerticleHelperTest.kt new file mode 100644 index 0000000..83c7009 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/testing/DeployRestVerticleHelperTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.testing + +import com.zepben.testutils.exception.ExpectException.Companion.expect +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.core.json.JsonObject +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class DeployRestVerticleHelperTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @AfterEach + fun afterEach() { + TestVerticle.onStart = { it.complete() } + TestVerticle.onStop = { it.complete() } + } + + @Test + fun coverageOnlyTest() { + val helper = buildHelper() + helper.requestSpec + helper.randomPortNumber + helper.close() + + TestVerticle.onStart = { it.fail("test start fail") } + + expect { buildHelper() }.toThrow().withMessage("test start fail") + } + + private fun buildHelper(): DeployRestVerticleHelper = + DeployRestVerticleHelper(TestVerticle::class.java, JsonObject()) + +} diff --git a/src/test/java/com/zepben/vertxutils/testing/MockRoutingContextTest.java b/src/test/java/com/zepben/vertxutils/testing/MockRoutingContextTest.java deleted file mode 100644 index 67db67f..0000000 --- a/src/test/java/com/zepben/vertxutils/testing/MockRoutingContextTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.testing; - -import com.zepben.vertxutils.routing.RoutingContextEx; -import com.zepben.vertxutils.routing.handlers.params.*; -import io.vertx.ext.web.RoutingContext; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.mock; - -public class MockRoutingContextTest { - - @Test - public void builderPathParamsObject() { - PathParams pathParams = mock(PathParams.class); - - RoutingContext context = MockRoutingContext.builder() - .pathParams(pathParams) - .build(); - - assertThat(RoutingContextEx.getPathParams(context), is(pathParams)); - } - - @Test - public void builderPathParams() { - PathParamRule param1 = PathParamRule.of("param1", ParamType.STRING); - PathParamRule param2 = PathParamRule.of("param2", ParamType.STRING); - PathParamRule param3 = PathParamRule.of("param3", ParamType.INT); - - RoutingContext context = MockRoutingContext.builder() - .pathParam(param1, "value1") - .pathParam(param2, "value2") - .pathParam(param3, 3) - .build(); - - PathParams pathParams = RoutingContextEx.getPathParams(context); - assertThat(pathParams.get(param1), equalTo("value1")); - assertThat(pathParams.get(param2), equalTo("value2")); - assertThat(pathParams.get(param3), equalTo(3)); - } - - @Test - public void builderQueryParamsObject() { - QueryParams queryParams = mock(QueryParams.class); - - RoutingContext context = MockRoutingContext.builder() - .queryParams(queryParams) - .build(); - - assertThat(RoutingContextEx.getQueryParams(context), is(queryParams)); - } - - @Test - public void builderQueryParams() { - QueryParamRule param1 = QueryParamRule.of("param1", ParamType.STRING); - QueryParamRule param2 = QueryParamRule.of("param2", ParamType.STRING, "default"); - QueryParamRule param3 = QueryParamRule.of("param3", ParamType.STRING); - QueryParamRule param4 = QueryParamRule.of("param4", ParamType.STRING); - QueryParamRule param5 = QueryParamRule.of("param5", ParamType.INT); - - RoutingContext context = MockRoutingContext.builder() - .queryParam(param1, "value1") - .queryParam(param2) - .queryParams(param3, param4) - .queryParam(param5, 5) - .build(); - - QueryParams queryParams = RoutingContextEx.getQueryParams(context); - assertThat(queryParams.get(param1), equalTo("value1")); - assertThat(queryParams.get(param2), equalTo("default")); - assertThat(queryParams.exists(param3), equalTo(false)); - assertThat(queryParams.exists(param4), equalTo(false)); - assertThat(queryParams.get(param5), equalTo(5)); - } - - @Test - public void builderBody() { - Object body = new Object(); - - RoutingContext context = MockRoutingContext.builder() - .decodedBody(body) - .build(); - - Object decodedBody = RoutingContextEx.getDecodedBody(context); - assertThat(decodedBody, is(body)); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/testing/MockRoutingContextTest.kt b/src/test/java/com/zepben/vertxutils/testing/MockRoutingContextTest.kt new file mode 100644 index 0000000..7bf8819 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/testing/MockRoutingContextTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.testing + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.RoutingContextEx.getDecodedBody +import com.zepben.vertxutils.routing.RoutingContextEx.getPathParams +import com.zepben.vertxutils.routing.RoutingContextEx.getQueryParams +import com.zepben.vertxutils.routing.handlers.params.* +import com.zepben.vertxutils.testing.MockRoutingContext.builder +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.Mockito.mock + +class MockRoutingContextTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun builderPathParamsObject() { + val pathParams = mock() + + val context = builder() + .pathParams(pathParams) + .build() + + assertThat(getPathParams(context), equalTo(pathParams)) + } + + @Test + fun builderPathParams() { + val param1 = PathParamRule.of("param1", ParamType.STRING) + val param2 = PathParamRule.of("param2", ParamType.STRING) + val param3 = PathParamRule.of("param3", ParamType.INT) + + val context = builder() + .pathParam(param1, "value1") + .pathParam(param2, "value2") + .pathParam(param3, 3) + .build() + + val pathParams = getPathParams(context) + assertThat(pathParams[param1], equalTo("value1")) + assertThat(pathParams[param2], equalTo("value2")) + assertThat(pathParams[param3], equalTo(3)) + } + + @Test + fun builderQueryParamsObject() { + val queryParams = mock(QueryParams::class.java) + + val context = builder() + .queryParams(queryParams) + .build() + + assertThat(getQueryParams(context), equalTo(queryParams)) + } + + @Test + fun builderQueryParams() { + val param1 = QueryParamRule.of("param1", ParamType.STRING) + val param2 = QueryParamRule.of("param2", ParamType.STRING, "default") + val param3 = QueryParamRule.of("param3", ParamType.STRING) + val param4 = QueryParamRule.of("param4", ParamType.STRING) + val param5 = QueryParamRule.of("param5", ParamType.INT) + + val context = builder() + .queryParam(param1, "value1") + .queryParam(param2) + .queryParams(param3, param4) + .queryParam(param5, 5) + .build() + + val queryParams = getQueryParams(context) + assertThat(queryParams[param1], equalTo("value1")) + assertThat(queryParams[param2], equalTo("default")) + assertThat(queryParams.contains(param3), equalTo(false)) + assertThat(queryParams.contains(param4), equalTo(false)) + assertThat(queryParams[param5], equalTo(5)) + } + + @Test + fun builderBody() { + val body = Any() + + val context = builder() + .decodedBody(body) + .build() + + val decodedBody = getDecodedBody(context) + assertThat(decodedBody, equalTo(body)) + } + +} diff --git a/src/test/java/com/zepben/vertxutils/testing/TestHttpServerTest.java b/src/test/java/com/zepben/vertxutils/testing/TestHttpServerTest.java deleted file mode 100644 index f8e3183..0000000 --- a/src/test/java/com/zepben/vertxutils/testing/TestHttpServerTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.testing; - -import com.zepben.vertxutils.routing.Route; -import org.junit.jupiter.api.Test; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.equalTo; - -public class TestHttpServerTest { - - @Test - public void canServe() { - try (TestHttpServer server = new TestHttpServer()) { - Route route = Route.builder().path("/").addHandler(ctx -> ctx.response().end("The response!")).build(); - int port = server.addRoute(route).listen(); - - given() - .port(port) - .get("/") - .then() - .statusCode(200) - .body(equalTo("The response!")); - } - } - -} diff --git a/src/test/java/com/zepben/vertxutils/testing/TestHttpServerTest.kt b/src/test/java/com/zepben/vertxutils/testing/TestHttpServerTest.kt new file mode 100644 index 0000000..69a797b --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/testing/TestHttpServerTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.testing + +import com.zepben.testutils.junit.SystemLogExtension +import com.zepben.vertxutils.routing.Route.Companion.builder +import io.restassured.RestAssured +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class TestHttpServerTest { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + } + + @Test + fun canServe() { + TestHttpServer().use { server -> + val route = builder().path("/").addHandler { ctx -> ctx!!.response().end("The response!") }.build() + val port = server.addRoute(route).listen() + RestAssured.given() + .port(port) + .get("/") + .then() + .statusCode(200) + .body(equalTo("The response!")) + } + } + +} diff --git a/src/test/java/com/zepben/vertxutils/testing/TestVerticle.java b/src/test/java/com/zepben/vertxutils/testing/TestVerticle.java deleted file mode 100644 index 7628ea3..0000000 --- a/src/test/java/com/zepben/vertxutils/testing/TestVerticle.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 Zeppelin Bend Pty Ltd - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.zepben.vertxutils.testing; - -import com.zepben.annotations.EverythingIsNonnullByDefault; -import io.vertx.core.AbstractVerticle; -import io.vertx.core.Promise; - -import java.util.function.Consumer; - -@SuppressWarnings("WeakerAccess") -@EverythingIsNonnullByDefault -public class TestVerticle extends AbstractVerticle { - - public static void setOnStart(Runnable onStart) { - setOnStart((promise) -> { - onStart.run(); - promise.complete(); - }); - } - - public static void setOnStart(Consumer> onStart) { - TestVerticle.onStart = onStart; - } - - public static void setOnStop(Runnable onStop) { - setOnStop((promise) -> { - onStop.run(); - promise.complete(); - }); - } - - public static void setOnStop(Consumer> onStop) { - TestVerticle.onStop = onStop; - } - - private static Consumer> onStart = Promise::complete; - private static Consumer> onStop = Promise::complete; - - @Override - public void start(Promise startPromise) { - onStart.accept(startPromise); - } - - @Override - public void stop(Promise stopPromise) { - onStop.accept(stopPromise); - } - -} diff --git a/src/test/java/com/zepben/vertxutils/testing/TestVerticle.kt b/src/test/java/com/zepben/vertxutils/testing/TestVerticle.kt new file mode 100644 index 0000000..3a3c2f9 --- /dev/null +++ b/src/test/java/com/zepben/vertxutils/testing/TestVerticle.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Zeppelin Bend Pty Ltd + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.zepben.vertxutils.testing + +import com.zepben.testutils.junit.SystemLogExtension +import io.vertx.core.AbstractVerticle +import io.vertx.core.Promise +import org.junit.jupiter.api.extension.RegisterExtension + +class TestVerticle : AbstractVerticle() { + + companion object { + + @JvmField + @RegisterExtension + val systemOut: SystemLogExtension = SystemLogExtension.SYSTEM_OUT.captureLog().muteOnSuccess() + + var onStart: (Promise) -> Unit = { it.complete() } + var onStop: (Promise) -> Unit = { it.complete() } + + } + + override fun start(startPromise: Promise) { + onStart(startPromise) + } + + override fun stop(stopPromise: Promise) { + onStop(stopPromise) + } + +}