diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index 0d5f969364a0f..2a07b990f48b2 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -277,6 +277,7 @@ As mentioned above, the API is divided into groups: - stream (not available yet) - string - `.value(valueType)` - transactions - `withTransaction` +- json - `.json()` (requires the https://redis.com/modules/redis-json/[redis-json] module on the server side) Each of these methods returns an object that lets you execute the commands related to the group. The following snippet demonstrates how to use the _hash_ group: diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java index 3004aaccfd595..4026035911272 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java @@ -7,6 +7,7 @@ import io.quarkus.redis.datasource.geo.ReactiveGeoCommands; import io.quarkus.redis.datasource.hash.ReactiveHashCommands; import io.quarkus.redis.datasource.hyperloglog.ReactiveHyperLogLogCommands; +import io.quarkus.redis.datasource.json.ReactiveJsonCommands; import io.quarkus.redis.datasource.keys.ReactiveKeyCommands; import io.quarkus.redis.datasource.list.ReactiveListCommands; import io.quarkus.redis.datasource.pubsub.ReactivePubSubCommands; @@ -372,7 +373,7 @@ default ReactiveBitMapCommands bitmap() { } /** - * Gets the objects to publish and receive messages. + * Gets the object to publish and receive messages. * * @param messageType the type of message * @param the type of message @@ -380,6 +381,25 @@ default ReactiveBitMapCommands bitmap() { */ ReactivePubSubCommands pubsub(Class messageType); + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @return the object to manipulate JSON values. + */ + default ReactiveJsonCommands json() { + return json(String.class); + } + + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @param the type of keys + * @return the object to manipulate JSON values. + */ + ReactiveJsonCommands json(Class redisKeyType); + /** * Executes a command. * This method is used to execute commands not offered by the API. diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java index 10783371f62d7..298900e8ede64 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java @@ -8,6 +8,7 @@ import io.quarkus.redis.datasource.geo.GeoCommands; import io.quarkus.redis.datasource.hash.HashCommands; import io.quarkus.redis.datasource.hyperloglog.HyperLogLogCommands; +import io.quarkus.redis.datasource.json.JsonCommands; import io.quarkus.redis.datasource.keys.KeyCommands; import io.quarkus.redis.datasource.list.ListCommands; import io.quarkus.redis.datasource.pubsub.PubSubCommands; @@ -369,6 +370,25 @@ default BitMapCommands bitmap() { return bitmap(String.class); } + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @return the object to manipulate JSON values. + */ + default JsonCommands json() { + return json(String.class); + } + + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @param the type of keys + * @return the object to manipulate JSON values. + */ + JsonCommands json(Class redisKeyType); + /** * Gets the objects to publish and receive messages. * diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonCommands.java new file mode 100644 index 0000000000000..04392391cbc2f --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonCommands.java @@ -0,0 +1,546 @@ +package io.quarkus.redis.datasource.json; + +import java.util.List; +import java.util.OptionalInt; + +import io.quarkus.redis.datasource.RedisCommands; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +/** + * Allows executing commands from the {@code json} group (requires the Redis stack). + * See the json command list for further information about these commands. + *

+ * Redis JSON lets you store, update, and retrieve JSON values in a Redis database, similar to any other Redis data type. + *

+ * Ths API is based on the {@link JsonObject} and {@link JsonArray} types. + * Some methods also allows direct mapping from and to objects. In the case of deserialization, the + * {@link Class Class} must be passed. + * + * @param the type of the key + */ +public interface JsonCommands extends RedisCommands { + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value, encoded to JSON + * @param the type for the value + **/ + void jsonSet(K key, String path, T value); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param value the value, encoded to JSON + * @param the type for the value + **/ + default void jsonSet(K key, T value) { + jsonSet(key, "$", value); + } + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + **/ + void jsonSet(K key, String path, JsonObject json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + *

+ * This variant uses {@code $} as path. + * + * @param key the key, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + **/ + default void jsonSet(K key, JsonObject json) { + jsonSet(key, "$", json); + } + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + * @param args the extra arguments + **/ + void jsonSet(K key, String path, JsonObject json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + **/ + void jsonSet(K key, String path, JsonArray json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + **/ + default void jsonSet(K key, JsonArray json) { + jsonSet(key, "$", json); + } + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + * @param args the extra arguments + **/ + void jsonSet(K key, String path, JsonArray json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to store, encoded to JSON. + * @param args the extra arguments + **/ + void jsonSet(K key, String path, T value, JsonSetArgs args); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * It map the retrieve JSON document to an object of type {@code }. + * + * @param key the key, must not be {@code null} + * @param clazz the type of object to recreate from the JSON content + * @return the object, {@code null} if it does not exist + **/ + T jsonGet(K key, Class clazz); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonObject}. + * + * @param key the key, must not be {@code null} + * @return the stored JSON object, {@code null} if it does not exist + **/ + JsonObject jsonGetObject(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonArray}. + * + * @param key the key, must not be {@code null} + * @return the stored JSON array, {@code null} if it does not exist + **/ + JsonArray jsonGetArray(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @return the JSON array containing the different results, {@code null} if it does not exist. + **/ + JsonArray jsonGet(K key, String path); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param paths the paths, must not be {@code null}. If no path are passed, this is equivalent to + * {@link #jsonGetObject(Object)}, + * if multiple paths are passed, the produced JSON object contains the result (as a json array) for each path. + * @return the stored JSON object, {@code null} if it does not exist. + * If no path are passed, this is equivalent to {@link #jsonGetObject(Object)}. + * If multiple paths are passed, the produced JSON object contains the result for each pass as a JSON array. + **/ + JsonObject jsonGet(K key, String... paths); + + /** + * Execute the command JSON.ARRAPPEND. + * Summary: Append the json values into the array at path after the last element in it. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param values the values to append, encoded to JSON + * @param the type of value + * @return a list with the new sizes of each modified array (in order) or {@code null} (instead of + * the size) if the point object was not an array. + **/ + List jsonArrAppend(K key, String path, T... values); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param start the start index + * @param end the end index + * @param the type of value + * @return a list with the first position in the array of each JSON value that matches the path, + * {@code -1} if not found in the array, or {@code null} if the matching JSON value is not an array. + **/ + List jsonArrIndex(K key, String path, T value, int start, int end); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param the type of value + * @return a list with the first position in the array of each JSON value that matches the path, + * {@code -1} if not found in the array, or {@code null} if the matching JSON value is not an array. + **/ + default List jsonArrIndex(K key, String path, T value) { + return jsonArrIndex(key, path, value, 0, 0); + } + + /** + * Execute the command JSON.ARRINSERT. + * Summary: Inserts the json values into the array at path before the index (shifts to the right). + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param index the index. The index must be in the array's range. Inserting at index 0 prepends to the array. + * Negative index values start from the end of the array. + * @param values the values to insert, encoded to JSON + * @param the type of value + * @return a list of integer containing for each path, the array's new size or {@code null} if the + * matching JSON value is not an array. + **/ + List jsonArrInsert(K key, String path, int index, T... values); + + /** + * Execute the command JSON.ARRLEN. + * Summary: Reports the length of the JSON Array at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, {@code null} means {@code $} + * @return a list of integer containing for each path, the array's length, or @{code null} if the + * matching JSON value is not an array. + **/ + List jsonArrLen(K key, String path); + + /** + * Execute the command JSON.ARRLEN. + * Summary: Reports the length of the JSON Array at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return an optional containing the array's length, empty if the matching JSON value is not an array. + **/ + default OptionalInt jsonArrLen(K key) { + List list = jsonArrLen(key, null); + if (list.get(0) == null) { + return OptionalInt.empty(); + } + return OptionalInt.of(list.get(0)); + } + + /** + * Execute the command JSON.ARRPOP. + * Summary: Removes and returns an element from the index in the array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param clazz the type of the popped object + * @param path path the path, defaults to root if not provided. + * @param index is the position in the array to start popping from (defaults to -1, meaning the last element). + * Out-of-range indexes round to their respective array ends. + * @return a list of T including for each path, an instance of T rebuilt from the JSON value, or + * {@code null} if the matching JSON value is not an array. Popping an empty array produces {@code null}. + **/ + List jsonArrPop(K key, Class clazz, String path, int index); + + /** + * Execute the command JSON.ARRPOP. + * Summary: Removes and returns an element from the index in the array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param clazz the type of the popped object + * @param path path the path, defaults to root if not provided. + * @return a list of T including for each path, an instance of T rebuilt from the JSON value, or + * {@code null} if the matching JSON value is not an array. Popping an empty array produces {@code null}. + **/ + default List jsonArrPop(K key, Class clazz, String path) { + return jsonArrPop(key, clazz, path, -1); + } + + /** + * Execute the command JSON.ARRPOP. + * Summary: Removes and returns an element from the index in the array. + * Group: json + *

+ *

+ * This variant popped from the root object (must be a JSON array), and returns a single item. + * + * @param key the key, must not be {@code null} + * @param clazz the type of the popped object + * @return an instance of T. + **/ + default T jsonArrPop(K key, Class clazz) { + List list = jsonArrPop(key, clazz, null, -1); + return list.get(0); + } + + /** + * Execute the command JSON.ARRTRIM. + * Summary: Trims an array so that it contains only the specified inclusive range of elements. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + * @param start the start index + * @param stop the stop index + * @return a list of integer containing, for each path, the array's new size, or {@code null} if + * the matching JSON value is not an array. + **/ + List jsonArrTrim(K key, String path, int start, int stop); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @return the number of value cleared + **/ + int jsonClear(K key, String path); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return the number of value cleared + **/ + default int jsonClear(K key) { + return jsonClear(key, null); + } + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @return the number of path deleted + **/ + int jsonDel(K key, String path); + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return the number of path deleted (0 or more) + **/ + default int jsonDel(K key) { + return jsonDel(key, null); + } + + /** + * Execute the command JSON.MGET. + * Summary: Returns the values at path from multiple key arguments. Returns {@code null} for nonexistent keys + * and nonexistent paths. + * Group: json + *

+ * + * @param path path the path + * @param keys the keys, must not be {@code null}, must not contain {@code null} + * @return a list of JsonArray containing each retrieved value + **/ + List jsonMget(String path, K... keys); + + /** + * Execute the command JSON.NUMINCRBY. + * Summary: Increments the number value stored at path by number. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @param value the value to add + **/ + void jsonNumincrby(K key, String path, double value); + + /** + * Execute the command JSON.OBJKEYS. + * Summary: Returns the keys in the object that's referenced by path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a list containing, for each matching path, the list of keys, or {@code null} if the + * matching JSON value is not an object. + **/ + List> jsonObjKeys(K key, String path); + + /** + * Execute the command JSON.OBJKEYS. + * Summary: Returns the keys in the object that's referenced by path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return a list containing the list of keys, or {@code null} if the stored value is not a JSON object. + **/ + default List jsonObjKeys(K key) { + List> lists = jsonObjKeys(key, null); + return lists.get(0); + } + + /** + * Execute the command JSON.OBJLEN. + * Summary: Reports the number of keys in the JSON Object at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a list containing, for each path, the length of the object, {@code null} if the matching + * JSON value is not an object + **/ + List jsonObjLen(K key, String path); + + /** + * Execute the command JSON.OBJLEN. + * Summary: Reports the number of keys in the JSON Object at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return the length of the JSON object, {@code null} if the matching JSON value is not an object + **/ + default OptionalInt jsonObjLen(K key) { + List integers = jsonObjLen(key, null); + if (integers.get(0) == null) { + return OptionalInt.empty(); + } + return OptionalInt.of(integers.get(0)); + } + + /** + * Execute the command JSON.STRAPPEND. + * Summary: Appends the json-string values to the string at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @param value the string to append, must not be {@code null} + * @return a list containing, for each path, the new string length, {@code null} if the matching JSON + * value is not a string. + **/ + List jsonStrAppend(K key, String path, String value); + + /** + * Execute the command JSON.STRLEN. + * Summary: Reports the length of the JSON String at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a list containing, for each path, the length of the string, {@code null} if the matching JSON + * value is not a string. Returns {@code null} if the key or path do not exist. + **/ + List jsonStrLen(K key, String path); + + /** + * Execute the command JSON.TOGGLE. + * Summary: Toggle a boolean value stored at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + * @return a list containing, for each path, the new boolean value, {@code null} if the matching JSON + * value is not a boolean. + **/ + List jsonToggle(K key, String path); + + /** + * Execute the command JSON.TYPE. + * Summary: Reports the type of JSON value at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a list containing, for each path, the json type as String (string, integer, number, boolean, + * object, array), empty if no match. + **/ + List jsonType(K key, String path); + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java new file mode 100644 index 0000000000000..cb5f37c0dd8db --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java @@ -0,0 +1,47 @@ +package io.quarkus.redis.datasource.json; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.RedisCommandExtraArguments; + +public class JsonSetArgs implements RedisCommandExtraArguments { + + private boolean nx = false; + private boolean xx = false; + + /** + * Don't update already existing elements. Always add new elements. + * + * @return the current {@code GeoaddArgs} + **/ + public JsonSetArgs nx() { + this.nx = true; + return this; + } + + /** + * Only update elements that already exist. Never add elements. + * + * @return the current {@code GeoaddArgs} + **/ + public JsonSetArgs xx() { + this.xx = true; + return this; + } + + @Override + public List toArgs() { + if (xx && nx) { + throw new IllegalArgumentException("Cannot set XX and NX together"); + } + List args = new ArrayList<>(); + if (xx) { + args.add("XX"); + } + if (nx) { + args.add("NX"); + } + return args; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/ReactiveJsonCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/ReactiveJsonCommands.java new file mode 100644 index 0000000000000..629e353b91a30 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/ReactiveJsonCommands.java @@ -0,0 +1,495 @@ +package io.quarkus.redis.datasource.json; + +import java.util.Collections; +import java.util.List; + +import io.quarkus.redis.datasource.ReactiveRedisCommands; +import io.smallrye.mutiny.Uni; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +/** + * Allows executing commands from the {@code json} group (requires the Redis stack). + * See the json command list for further information about these commands. + *

+ * Redis JSON lets you store, update, and retrieve JSON values in a Redis database, similar to any other Redis data type. + *

+ * Ths API is based on the {@link JsonObject} and {@link JsonArray} types. + * Some methods also allows direct mapping from and to objects. In the case of deserialization, the + * {@link Class Class} must be passed. + * + * @param the type of the key + */ +public interface ReactiveJsonCommands extends ReactiveRedisCommands { + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value, encoded to JSON + * @param the type for the value + * @return A uni emitting {@code null} when the operation completes + **/ + Uni jsonSet(K key, String path, T value); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + * @return A uni emitting {@code null} when the operation completes + **/ + Uni jsonSet(K key, String path, JsonObject json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + * @param args the extra arguments + * @return A uni emitting {@code null} when the operation completes + **/ + Uni jsonSet(K key, String path, JsonObject json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + * @return A uni emitting {@code null} when the operation completes + **/ + Uni jsonSet(K key, String path, JsonArray json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + * @param args the extra arguments + * @return A uni emitting {@code null} when the operation completes + **/ + Uni jsonSet(K key, String path, JsonArray json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to store, encoded to JSON. + * @param args the extra arguments + * @return A uni emitting {@code null} when the operation completes + **/ + Uni jsonSet(K key, String path, T value, JsonSetArgs args); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * It map the retrieve JSON document to an object of type {@code }. + * + * @param key the key, must not be {@code null} + * @param clazz the type of object to recreate from the JSON content + * @return a uni emitting the object, {@code null} if it does not exist + **/ + Uni jsonGet(K key, Class clazz); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonObject}. + * + * @param key the key, must not be {@code null} + * @return a uni emitting the stored JSON object, {@code null} if it does not exist + **/ + Uni jsonGetObject(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonArray}. + * + * @param key the key, must not be {@code null} + * @return a uni emitting the stored JSON array, {@code null} if it does not exist + **/ + Uni jsonGetArray(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @return a uni emitting the JSON array containing the different results, {@code null} if it does not exist. + **/ + Uni jsonGet(K key, String path); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param paths the paths, must not be {@code null}. If no path are passed, this is equivalent to + * {@link #jsonGetObject(Object)}, + * if multiple paths are passed, the produced JSON object contains the result (as a json array) for each path. + * @return a uni emitting the stored JSON object, {@code null} if it does not exist. + * If no path are passed, this is equivalent to {@link #jsonGetObject(Object)}. + * If multiple paths are passed, the produced JSON object contains the result for each pass as a JSON array. + **/ + Uni jsonGet(K key, String... paths); + + /** + * Execute the command JSON.ARRAPPEND. + * Summary: Append the json values into the array at path after the last element in it. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param values the values to append, encoded to JSON + * @param the type of value + * @return a uni emitting a list with the new sizes of each modified array (in order) or {@code null} (instead of + * the size) if the point object was not an array. + **/ + Uni> jsonArrAppend(K key, String path, T... values); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param start the start index + * @param end the end index + * @param the type of value + * @return a uni emitting a list with the first position in the array of each JSON value that matches the path, + * {@code -1} if not found in the array, or {@code null} if the matching JSON value is not an array. + **/ + Uni> jsonArrIndex(K key, String path, T value, int start, int end); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param the type of value + * @return a uni emitting a list with the first position in the array of each JSON value that matches the path, + * {@code -1} if not found in the array, or {@code null} if the matching JSON value is not an array. + **/ + default Uni> jsonArrIndex(K key, String path, T value) { + return jsonArrIndex(key, path, value, 0, 0); + } + + /** + * Execute the command JSON.ARRINSERT. + * Summary: Inserts the json values into the array at path before the index (shifts to the right). + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param index the index. The index must be in the array's range. Inserting at index 0 prepends to the array. + * Negative index values start from the end of the array. + * @param values the values to insert, encoded to JSON + * @param the type of value + * @return a uni emitting a list of integer containing for each path, the array's new size or {@code null} if the + * matching JSON value is not an array. + **/ + Uni> jsonArrInsert(K key, String path, int index, T... values); + + /** + * Execute the command JSON.ARRLEN. + * Summary: Reports the length of the JSON Array at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, {@code null} means {@code $} + * @return a uni emitting a list of integer containing for each path, the array's length, or @{code null} if the + * matching JSON value is not an array. + **/ + Uni> jsonArrLen(K key, String path); + + /** + * Execute the command JSON.ARRLEN. + * Summary: Reports the length of the JSON Array at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return a uni emitting the array's length, or @{code null} if the matching JSON value is not an array. + **/ + default Uni jsonArrLen(K key) { + return jsonArrLen(key, null).map(l -> l.get(0)); + } + + /** + * Execute the command JSON.ARRPOP. + * Summary: Removes and returns an element from the index in the array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param clazz the type of the popped object + * @param path path the path, defaults to root if not provided. + * @param index is the position in the array to start popping from (defaults to -1, meaning the last element). + * Out-of-range indexes round to their respective array ends. + * @return a uni emitting a list of T including for each path, an instance of T rebuilt from the JSON value, or + * {@code null} if the matching JSON value is not an array. Popping an empty array produces {@code null}. + **/ + Uni> jsonArrPop(K key, Class clazz, String path, int index); + + /** + * Execute the command JSON.ARRPOP. + * Summary: Removes and returns an element from the index in the array. + * Group: json + *

+ *

+ * This variant popped from the root object (must be a JSON array), and returns a single item. + * + * @param key the key, must not be {@code null} + * @param clazz the type of the popped object + * @return a uni emitting an instance of T. + **/ + default Uni jsonArrPop(K key, Class clazz) { + return jsonArrPop(key, clazz, null, -1).map(l -> l.get(0)); + } + + /** + * Execute the command JSON.ARRTRIM. + * Summary: Trims an array so that it contains only the specified inclusive range of elements. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + * @param start the start index + * @param stop the stop index + * @return a uni emitting a list of integer containing, for each path, the array's new size, or {@code null} if + * the matching JSON value is not an array. + **/ + Uni> jsonArrTrim(K key, String path, int start, int stop); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @return a uni emitting the number of value cleared + **/ + Uni jsonClear(K key, String path); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return a uni emitting the number of value cleared + **/ + default Uni jsonClear(K key) { + return jsonClear(key, null); + } + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @return a uni emitting the number of path deleted + **/ + Uni jsonDel(K key, String path); + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return a uni emitting the number of path deleted (0 or more) + **/ + default Uni jsonDel(K key) { + return jsonDel(key, null); + } + + /** + * Execute the command JSON.MGET. + * Summary: Returns the values at path from multiple key arguments. Returns {@code null} for nonexistent keys + * and nonexistent paths. + * Group: json + *

+ * + * @param path path the path + * @param keys the keys, must not be {@code null}, must not contain {@code null} + * @return a uni emitting a list of JsonArray containing each retrieved value + **/ + Uni> jsonMget(String path, K... keys); + + /** + * Execute the command JSON.NUMINCRBY. + * Summary: Increments the number value stored at path by number. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @param value the value to add + * @return a uni emitting {@code null} when the operation completes + **/ + Uni jsonNumincrby(K key, String path, double value); + + /** + * Execute the command JSON.OBJKEYS. + * Summary: Returns the keys in the object that's referenced by path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a uni emitting a list containing, for each matching path, the list of keys, or {@code null} if the + * matching JSON value is not an object. + **/ + Uni>> jsonObjKeys(K key, String path); + + /** + * Execute the command JSON.OBJKEYS. + * Summary: Returns the keys in the object that's referenced by path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return a uni emitting a list containing the list of keys, or {@code null} if the stored value is not a JSON object. + **/ + default Uni> jsonObjKeys(K key) { + return jsonObjKeys(key, null).map(l -> { + if (l != null && !l.isEmpty()) { + return l.get(0); + } + return Collections.emptyList(); + }); + } + + /** + * Execute the command JSON.OBJLEN. + * Summary: Reports the number of keys in the JSON Object at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a uni emitting a list containing, for each path, the length of the object, {@code null} if the matching + * JSON value is not an object + **/ + Uni> jsonObjLen(K key, String path); + + /** + * Execute the command JSON.OBJLEN. + * Summary: Reports the number of keys in the JSON Object at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return a uni emitting the length of the JSON object, {@code null} if the matching JSON value is not an object + **/ + default Uni jsonObjLen(K key) { + return jsonObjLen(key, null) + .map(l -> l.get(0)); + } + + /** + * Execute the command JSON.STRAPPEND. + * Summary: Appends the json-string values to the string at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @param value the string to append, must not be {@code null} + * @return a uni emitting a list containing, for each path, the new string length, {@code null} if the matching JSON + * value is not a string. + **/ + Uni> jsonStrAppend(K key, String path, String value); + + /** + * Execute the command JSON.STRLEN. + * Summary: Reports the length of the JSON String at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a uni emitting a list containing, for each path, the length of the string, {@code null} if the matching JSON + * value is not a string. Returns {@code null} if the key or path do not exist. + **/ + Uni> jsonStrLen(K key, String path); + + /** + * Execute the command JSON.TOGGLE. + * Summary: Toggle a boolean value stored at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + * @return a uni emitting a list containing, for each path, the new boolean value, {@code null} if the matching JSON + * value is not a boolean. + **/ + Uni> jsonToggle(K key, String path); + + /** + * Execute the command JSON.TYPE. + * Summary: Reports the type of JSON value at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return a uni emitting a list containing, for each path, the json type as String (string, integer, number, boolean, + * object, array), empty if no match. + **/ + Uni> jsonType(K key, String path); + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/ReactiveTransactionalJsonCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/ReactiveTransactionalJsonCommands.java new file mode 100644 index 0000000000000..a71ba1a74fbf8 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/ReactiveTransactionalJsonCommands.java @@ -0,0 +1,434 @@ +package io.quarkus.redis.datasource.json; + +import io.quarkus.redis.datasource.ReactiveTransactionalRedisCommands; +import io.smallrye.mutiny.Uni; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public interface ReactiveTransactionalJsonCommands extends ReactiveTransactionalRedisCommands { + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value, encoded to JSON + * @param the type for the value + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonSet(K key, String path, T value); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonSet(K key, String path, JsonObject json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + * @param args the extra arguments + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonSet(K key, String path, JsonObject json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonSet(K key, String path, JsonArray json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + * @param args the extra arguments + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonSet(K key, String path, JsonArray json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to store, encoded to JSON. + * @param args the extra arguments + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonSet(K key, String path, T value, JsonSetArgs args); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * It maps the retrieve JSON document to an object of type {@code }. + * + * @param key the key, must not be {@code null} + * @param clazz the type of object to recreate from the JSON content + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonGet(K key, Class clazz); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonObject}. + * + * @param key the key, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonGetObject(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonArray}. + * + * @param key the key, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonGetArray(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonGet(K key, String path); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param paths the paths, must not be {@code null}. If no path are passed, this is equivalent to + * {@link #jsonGetObject(Object)}, + * if multiple paths are passed, the produced JSON object contains the result (as a json array) for each path. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonGet(K key, String... paths); + + /** + * Execute the command JSON.ARRAPPEND. + * Summary: Append the json values into the array at path after the last element in it. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param values the values to append, encoded to JSON + * @param the type of value + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonArrAppend(K key, String path, T... values); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param start the start index + * @param end the end index + * @param the type of value + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonArrIndex(K key, String path, T value, int start, int end); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param the type of value + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + default Uni jsonArrIndex(K key, String path, T value) { + return jsonArrIndex(key, path, value, 0, 0); + } + + /** + * Execute the command JSON.ARRINSERT. + * Summary: Inserts the json values into the array at path before the index (shifts to the right). + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param index the index. The index must be in the array's range. Inserting at index 0 prepends to the array. + * Negative index values start from the end of the array. + * @param values the values to insert, encoded to JSON + * @param the type of value + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonArrInsert(K key, String path, int index, T... values); + + /** + * Execute the command JSON.ARRLEN. + * Summary: Reports the length of the JSON Array at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, {@code null} means {@code $} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonArrLen(K key, String path); + + /** + * Execute the command JSON.ARRPOP. + * Summary: Removes and returns an element from the index in the array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param clazz the type of the popped object + * @param path path the path, defaults to root if not provided. + * @param index is the position in the array to start popping from (defaults to -1, meaning the last element). + * Out-of-range indexes round to their respective array ends. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonArrPop(K key, Class clazz, String path, int index); + + /** + * Execute the command JSON.ARRTRIM. + * Summary: Trims an array so that it contains only the specified inclusive range of elements. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + * @param start the start index + * @param stop the stop index + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonArrTrim(K key, String path, int start, int stop); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonClear(K key, String path); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + default Uni jsonClear(K key) { + return jsonClear(key, null); + } + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonDel(K key, String path); + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + default Uni jsonDel(K key) { + return jsonDel(key, null); + } + + /** + * Execute the command JSON.MGET. + * Summary: Returns the values at path from multiple key arguments. Returns {@code null} for nonexistent keys + * and nonexistent paths. + * Group: json + *

+ * + * @param path path the path + * @param keys the keys, must not be {@code null}, must not contain {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonMget(String path, K... keys); + + /** + * Execute the command JSON.NUMINCRBY. + * Summary: Increments the number value stored at path by number. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @param value the value to add + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonNumincrby(K key, String path, double value); + + /** + * Execute the command JSON.OBJKEYS. + * Summary: Returns the keys in the object that's referenced by path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonObjKeys(K key, String path); + + /** + * Execute the command JSON.OBJLEN. + * Summary: Reports the number of keys in the JSON Object at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonObjLen(K key, String path); + + /** + * Execute the command JSON.STRAPPEND. + * Summary: Appends the json-string values to the string at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @param value the string to append, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonStrAppend(K key, String path, String value); + + /** + * Execute the command JSON.STRLEN. + * Summary: Reports the length of the JSON String at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonStrLen(K key, String path); + + /** + * Execute the command JSON.TOGGLE. + * Summary: Toggle a boolean value stored at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonToggle(K key, String path); + + /** + * Execute the command JSON.TYPE. + * Summary: Reports the type of JSON value at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni jsonType(K key, String path); + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/TransactionalJsonCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/TransactionalJsonCommands.java new file mode 100644 index 0000000000000..cd44cdd07bffc --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/TransactionalJsonCommands.java @@ -0,0 +1,372 @@ +package io.quarkus.redis.datasource.json; + +import io.quarkus.redis.datasource.TransactionalRedisCommands; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public interface TransactionalJsonCommands extends TransactionalRedisCommands { + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value, encoded to JSON + * @param the type for the value + */ + void jsonSet(K key, String path, T value); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + */ + void jsonSet(K key, String path, JsonObject json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON object to store, must not be {@code null} + * @param args the extra arguments + */ + void jsonSet(K key, String path, JsonObject json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + */ + void jsonSet(K key, String path, JsonArray json); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param json the JSON array to store, must not be {@code null} + * @param args the extra arguments + */ + void jsonSet(K key, String path, JsonArray json, JsonSetArgs args); + + /** + * Execute the command JSON.SET. + * Summary: Sets the JSON value at path in key. + * Group: json + * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to store, encoded to JSON. + * @param args the extra arguments + */ + void jsonSet(K key, String path, T value, JsonSetArgs args); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * It maps the retrieve JSON document to an object of type {@code }. + * + * @param key the key, must not be {@code null} + * @param clazz the type of object to recreate from the JSON content + */ + void jsonGet(K key, Class clazz); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonObject}. + * + * @param key the key, must not be {@code null} + */ + void jsonGetObject(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * This method uses the root path ({@code $}). + * Unlike {@link #jsonGet(Object, Class)}, it returns a {@link JsonArray}. + * + * @param key the key, must not be {@code null} + */ + void jsonGetArray(K key); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + */ + void jsonGet(K key, String path); + + /** + * Execute the command JSON.GET. + * Summary: Returns the value at path in JSON serialized form. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param paths the paths, must not be {@code null}. If no path are passed, this is equivalent to + * {@link #jsonGetObject(Object)}, + * if multiple paths are passed, the produced JSON object contains the result (as a json array) for each path. + */ + void jsonGet(K key, String... paths); + + /** + * Execute the command JSON.ARRAPPEND. + * Summary: Append the json values into the array at path after the last element in it. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param values the values to append, encoded to JSON + * @param the type of value + */ + void jsonArrAppend(K key, String path, T... values); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param start the start index + * @param end the end index + * @param the type of value + */ + void jsonArrIndex(K key, String path, T value, int start, int end); + + /** + * Execute the command JSON.ARRINDEX. + * Summary: Searches for the first occurrence of a scalar JSON value in an array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param value the value to be searched, encoded to JSON + * @param the type of value + */ + default void jsonArrIndex(K key, String path, T value) { + jsonArrIndex(key, path, value, 0, 0); + } + + /** + * Execute the command JSON.ARRINSERT. + * Summary: Inserts the json values into the array at path before the index (shifts to the right). + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, must not be {@code null} + * @param index the index. The index must be in the array's range. Inserting at index 0 prepends to the array. + * Negative index values start from the end of the array. + * @param values the values to insert, encoded to JSON + * @param the type of value + */ + void jsonArrInsert(K key, String path, int index, T... values); + + /** + * Execute the command JSON.ARRLEN. + * Summary: Reports the length of the JSON Array at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path the path, {@code null} means {@code $} + */ + void jsonArrLen(K key, String path); + + /** + * Execute the command JSON.ARRPOP. + * Summary: Removes and returns an element from the index in the array. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param clazz the type of the popped object + * @param path path the path, defaults to root if not provided. + * @param index is the position in the array to start popping from (defaults to -1, meaning the last element). + * Out-of-range indexes round to their respective array ends. + */ + void jsonArrPop(K key, Class clazz, String path, int index); + + /** + * Execute the command JSON.ARRTRIM. + * Summary: Trims an array so that it contains only the specified inclusive range of elements. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + * @param start the start index + * @param stop the stop index + */ + void jsonArrTrim(K key, String path, int start, int stop); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + */ + void jsonClear(K key, String path); + + /** + * Execute the command JSON.CLEAR. + * Summary: Clears container values (Arrays/Objects), and sets numeric values to 0. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + */ + default void jsonClear(K key) { + jsonClear(key, null); + } + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + */ + void jsonDel(K key, String path); + + /** + * Execute the command JSON.DEL. + * Summary: Deletes a value. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + */ + default void jsonDel(K key) { + jsonDel(key, null); + } + + /** + * Execute the command JSON.MGET. + * Summary: Returns the values at path from multiple key arguments. Returns {@code null} for nonexistent keys + * and nonexistent paths. + * Group: json + *

+ * + * @param path path the path + * @param keys the keys, must not be {@code null}, must not contain {@code null} + */ + void jsonMget(String path, K... keys); + + /** + * Execute the command JSON.NUMINCRBY. + * Summary: Increments the number value stored at path by number. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. Non-existing paths are ignored. + * @param value the value to add + */ + void jsonNumincrby(K key, String path, double value); + + /** + * Execute the command JSON.OBJKEYS. + * Summary: Returns the keys in the object that's referenced by path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + */ + void jsonObjKeys(K key, String path); + + /** + * Execute the command JSON.OBJLEN. + * Summary: Reports the number of keys in the JSON Object at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + */ + void jsonObjLen(K key, String path); + + /** + * Execute the command JSON.STRAPPEND. + * Summary: Appends the json-string values to the string at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + * @param value the string to append, must not be {@code null} + */ + void jsonStrAppend(K key, String path, String value); + + /** + * Execute the command JSON.STRLEN. + * Summary: Reports the length of the JSON String at path in key. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + */ + void jsonStrLen(K key, String path); + + /** + * Execute the command JSON.TOGGLE. + * Summary: Toggle a boolean value stored at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, must not be {@code null} + */ + void jsonToggle(K key, String path); + + /** + * Execute the command JSON.TYPE. + * Summary: Reports the type of JSON value at path. + * Group: json + *

+ * + * @param key the key, must not be {@code null} + * @param path path the path, path defaults to {@code $} if not provided. + */ + void jsonType(K key, String path); +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java index d5bc340eb0d93..cf99a063b5d76 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java @@ -4,6 +4,7 @@ import io.quarkus.redis.datasource.geo.ReactiveTransactionalGeoCommands; import io.quarkus.redis.datasource.hash.ReactiveTransactionalHashCommands; import io.quarkus.redis.datasource.hyperloglog.ReactiveTransactionalHyperLogLogCommands; +import io.quarkus.redis.datasource.json.ReactiveTransactionalJsonCommands; import io.quarkus.redis.datasource.keys.ReactiveTransactionalKeyCommands; import io.quarkus.redis.datasource.list.ReactiveTransactionalListCommands; import io.quarkus.redis.datasource.set.ReactiveTransactionalSetCommands; @@ -269,6 +270,25 @@ default ReactiveTransactionalBitMapCommands bitmap() { return bitmap(String.class); } + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @return the object to manipulate JSON values. + */ + default ReactiveTransactionalJsonCommands json() { + return json(String.class); + } + + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @param the type of keys + * @return the object to manipulate JSON values. + */ + ReactiveTransactionalJsonCommands json(Class redisKeyType); + /** * Executes a command. * This method is used to execute commands not offered by the API. diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java index 817fb9015522b..f0e37802c9509 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java @@ -4,6 +4,7 @@ import io.quarkus.redis.datasource.geo.TransactionalGeoCommands; import io.quarkus.redis.datasource.hash.TransactionalHashCommands; import io.quarkus.redis.datasource.hyperloglog.TransactionalHyperLogLogCommands; +import io.quarkus.redis.datasource.json.TransactionalJsonCommands; import io.quarkus.redis.datasource.keys.TransactionalKeyCommands; import io.quarkus.redis.datasource.list.TransactionalListCommands; import io.quarkus.redis.datasource.set.TransactionalSetCommands; @@ -267,6 +268,25 @@ default TransactionalBitMapCommands bitmap() { return bitmap(String.class); } + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @return the object to manipulate JSON values. + */ + default TransactionalJsonCommands json() { + return json(String.class); + } + + /** + * Gets the object to manipulate JSON values. + * This group requires the RedisJSON module. + * + * @param the type of keys + * @return the object to manipulate JSON values. + */ + TransactionalJsonCommands json(Class redisKeyType); + /** * Executes a command. * This method is used to execute commands not offered by the API. diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractJsonCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractJsonCommands.java new file mode 100644 index 0000000000000..a2a31ead49674 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractJsonCommands.java @@ -0,0 +1,286 @@ +package io.quarkus.redis.runtime.datasource; + +import static io.quarkus.redis.runtime.datasource.Validation.notNullOrBlank; +import static io.smallrye.mutiny.helpers.ParameterValidation.doesNotContainNull; +import static io.smallrye.mutiny.helpers.ParameterValidation.nonNull; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.json.JsonSetArgs; +import io.smallrye.mutiny.Uni; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.redis.client.Command; +import io.vertx.mutiny.redis.client.Response; + +public class AbstractJsonCommands extends AbstractRedisCommands { + private static final JsonSetArgs JSON_SET_DEFAULT = new JsonSetArgs(); + + public AbstractJsonCommands(RedisCommandExecutor api, Class k) { + super(api, new Marshaller(k)); + } + + Uni _jsonSet(K key, String path, T value) { + return _jsonSet(key, path, Json.encode(value).getBytes(), JSON_SET_DEFAULT); + } + + Uni _jsonSet(K key, String path, JsonObject json) { + nonNull(json, "json"); + return _jsonSet(key, path, json.toBuffer().getBytes(), JSON_SET_DEFAULT); + } + + Uni _jsonSet(K key, String path, JsonArray json) { + nonNull(json, "json"); + return _jsonSet(key, path, json.toBuffer().getBytes(), JSON_SET_DEFAULT); + } + + Uni _jsonSet(K key, String path, JsonObject json, JsonSetArgs args) { + nonNull(json, "json"); + return _jsonSet(key, path, json.toBuffer().getBytes(), args); + } + + Uni _jsonSet(K key, String path, JsonArray json, JsonSetArgs args) { + nonNull(json, "json"); + return _jsonSet(key, path, json.toBuffer().getBytes(), args); + } + + Uni _jsonSet(K key, String path, T value, JsonSetArgs args) { + return _jsonSet(key, path, Json.encode(value).getBytes(), args); + } + + Uni _jsonSet(K key, String path, byte[] encoded, JsonSetArgs args) { + nonNull(key, "key"); + notNullOrBlank(path, "path"); + nonNull(args, "args"); + RedisCommand cmd = RedisCommand.of(Command.JSON_SET) + .put(marshaller.encode(key)) + .put(path) + .put(encoded) + .putAll(args.toArgs()); + return execute(cmd); + } + + Uni _jsonGet(K key) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of((Command.JSON_GET)) + .put(marshaller.encode(key)); + return execute(cmd); + } + + Uni _jsonGet(K key, String path) { + nonNull(key, "key"); + nonNull(path, "path"); + RedisCommand cmd = RedisCommand.of((Command.JSON_GET)) + .put(marshaller.encode(key)) + .put(path); + return execute(cmd); + } + + Uni _jsonGet(K key, String... paths) { + nonNull(key, "key"); + doesNotContainNull(paths, "path"); + RedisCommand cmd = RedisCommand.of((Command.JSON_GET)) + .put(marshaller.encode(key)) + .putAll(paths); + return execute(cmd); + } + + Uni _jsonArrAppend(K key, String path, T... values) { + nonNull(key, "key"); + doesNotContainNull(values, "values"); + List encoded = new ArrayList<>(); + for (T value : values) { + encoded.add(Json.encode(value)); + } + RedisCommand cmd = RedisCommand.of((Command.JSON_ARRAPPEND)) + .put(marshaller.encode(key)); + + if (path != null) { + cmd.put(path); + } + cmd.putAll(encoded); + return execute(cmd); + } + + Uni _jsonArrIndex(K key, String path, T value, long start, long end) { + nonNull(key, "key"); + nonNull(path, "path"); + nonNull(value, "value"); + RedisCommand cmd = RedisCommand.of(Command.JSON_ARRINDEX) + .put(marshaller.encode(key)) + .put(path) + .put(Json.encode(value)) + .put(start) + .put(end); + return execute(cmd); + } + + Uni _jsonArrInsert(K key, String path, int index, T[] values) { + nonNull(key, "key"); + nonNull(path, "path"); + doesNotContainNull(values, "values"); + RedisCommand cmd = RedisCommand.of(Command.JSON_ARRINSERT) + .put(marshaller.encode(key)) + .put(path) + .put(index); + + for (T value : values) { + cmd.put(Json.encode(value)); + } + + return execute(cmd); + } + + Uni _jsonArrLen(K key, String path) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of((Command.JSON_ARRLEN)) + .put(marshaller.encode(key)); + if (path != null) { + cmd.put(path); + } + return execute(cmd); + } + + Uni _jsonArrPop(K key, String path, int index) { + nonNull(key, "key"); + + RedisCommand cmd = RedisCommand.of(Command.JSON_ARRPOP) + .put(marshaller.encode(key)); + + if (path != null) { + cmd.put(path); + } + if (index != -1) { + cmd.put(-1); + } + return execute(cmd); + } + + Uni _jsonArrTrim(K key, String path, int start, int stop) { + nonNull(key, "key"); + nonNull(path, "path"); + RedisCommand cmd = RedisCommand.of(Command.JSON_ARRTRIM) + .put(marshaller.encode(key)) + .put(path) + .put(start) + .put(stop); + + return execute(cmd); + } + + Uni _jsonClear(K key, String path) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of(Command.JSON_CLEAR) + .put(marshaller.encode(key)); + + if (path != null) { + cmd.put(path); + } + return execute(cmd); + } + + Uni _jsonDel(K key, String path) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of(Command.JSON_DEL) + .put(marshaller.encode(key)); + + if (path != null) { + cmd.put(path); + } + return execute(cmd); + } + + Uni _jsonMget(String path, K... keys) { + notNullOrBlank(path, "path"); + doesNotContainNull(keys, "keys"); + + RedisCommand cmd = RedisCommand.of(Command.JSON_MGET) + .put(path); + + for (K key : keys) { + cmd.put(marshaller.encode(key)); + } + + return execute(cmd); + } + + Uni _jsonNumincrby(K key, String path, double value) { + nonNull(key, "key"); + notNullOrBlank(path, "path"); + + RedisCommand cmd = RedisCommand.of(Command.JSON_NUMINCRBY) + .put(marshaller.encode(key)) + .put(path) + .put(value); + + return execute(cmd); + } + + Uni _jsonObjKeys(K key, String path) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of(Command.JSON_OBJKEYS) + .put(marshaller.encode(key)); + if (path != null) { + cmd.put(path); + } + + return execute(cmd); + } + + Uni _jsonObjLen(K key, String path) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of(Command.JSON_OBJLEN) + .put(marshaller.encode(key)); + if (path != null) { + cmd.put(path); + } + + return execute(cmd); + } + + Uni _jsonStrAppend(K key, String path, String value) { + nonNull(key, "key"); + nonNull(value, "value"); + RedisCommand cmd = RedisCommand.of(Command.JSON_STRAPPEND) + .put(marshaller.encode(key)); + if (path != null) { + cmd.put(path); + } + cmd.put(Json.encode(value)); + return execute(cmd); + } + + Uni _jsonToggle(K key, String path) { + nonNull(key, "key"); + nonNull(path, "path"); + RedisCommand cmd = RedisCommand.of(Command.JSON_TOGGLE) + .put(marshaller.encode(key)) + .put(path); + return execute(cmd); + } + + Uni _jsonStrLen(K key, String path) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of(Command.JSON_STRLEN) + .put(marshaller.encode(key)); + if (path != null) { + cmd.put(path); + } + + return execute(cmd); + } + + Uni _jsonType(K key, String path) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of(Command.JSON_TYPE) + .put(marshaller.encode(key)); + if (path != null) { + cmd.put(path); + } + + return execute(cmd); + } + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingJsonCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingJsonCommandsImpl.java new file mode 100644 index 0000000000000..b9f9676b7745f --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingJsonCommandsImpl.java @@ -0,0 +1,189 @@ +package io.quarkus.redis.runtime.datasource; + +import java.time.Duration; +import java.util.List; + +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.json.JsonCommands; +import io.quarkus.redis.datasource.json.JsonSetArgs; +import io.quarkus.redis.datasource.json.ReactiveJsonCommands; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class BlockingJsonCommandsImpl extends AbstractRedisCommandGroup implements JsonCommands { + + private final ReactiveJsonCommands reactive; + + public BlockingJsonCommandsImpl(RedisDataSource ds, ReactiveJsonCommands reactive, Duration timeout) { + super(ds, timeout); + this.reactive = reactive; + } + + @Override + public void jsonSet(K key, String path, T value) { + reactive.jsonSet(key, path, value) + .await().atMost(timeout); + } + + @Override + public void jsonSet(K key, String path, JsonObject json) { + reactive.jsonSet(key, path, json) + .await().atMost(timeout); + } + + @Override + public void jsonSet(K key, String path, JsonObject json, JsonSetArgs args) { + reactive.jsonSet(key, path, json, args) + .await().atMost(timeout); + } + + @Override + public void jsonSet(K key, String path, JsonArray json) { + reactive.jsonSet(key, path, json) + .await().atMost(timeout); + } + + @Override + public void jsonSet(K key, String path, JsonArray json, JsonSetArgs args) { + reactive.jsonSet(key, path, json, args) + .await().atMost(timeout); + } + + @Override + public void jsonSet(K key, String path, T value, JsonSetArgs args) { + reactive.jsonSet(key, path, value, args) + .await().atMost(timeout); + } + + @Override + public T jsonGet(K key, Class clazz) { + return reactive.jsonGet(key, clazz) + .await().atMost(timeout); + } + + @Override + public JsonObject jsonGetObject(K key) { + return reactive.jsonGetObject(key) + .await().atMost(timeout); + } + + @Override + public JsonArray jsonGetArray(K key) { + return reactive.jsonGetArray(key) + .await().atMost(timeout); + } + + @Override + public JsonArray jsonGet(K key, String path) { + return reactive.jsonGet(key, path) + .await().atMost(timeout); + } + + @Override + public JsonObject jsonGet(K key, String... paths) { + return reactive.jsonGet(key, paths) + .await().atMost(timeout); + } + + @Override + public List jsonArrAppend(K key, String path, T... values) { + return reactive.jsonArrAppend(key, path, values) + .await().atMost(timeout); + } + + @Override + public List jsonArrIndex(K key, String path, T value, int start, int end) { + return reactive.jsonArrIndex(key, path, value, start, end) + .await().atMost(timeout); + } + + @Override + public List jsonArrIndex(K key, String path, T value) { + return reactive.jsonArrIndex(key, path, value) + .await().atMost(timeout); + } + + @Override + public List jsonArrInsert(K key, String path, int index, T... values) { + return reactive.jsonArrInsert(key, path, index, values) + .await().atMost(timeout); + } + + @Override + public List jsonArrLen(K key, String path) { + return reactive.jsonArrLen(key, path) + .await().atMost(timeout); + } + + @Override + public List jsonArrPop(K key, Class clazz, String path, int index) { + return reactive.jsonArrPop(key, clazz, path, index) + .await().atMost(timeout); + } + + @Override + public List jsonArrTrim(K key, String path, int start, int stop) { + return reactive.jsonArrTrim(key, path, start, stop) + .await().atMost(timeout); + } + + @Override + public int jsonClear(K key, String path) { + return reactive.jsonClear(key, path) + .await().atMost(timeout); + } + + @Override + public int jsonDel(K key, String path) { + return reactive.jsonDel(key, path) + .await().atMost(timeout); + } + + @Override + public List jsonMget(String path, K... keys) { + return reactive.jsonMget(path, keys) + .await().atMost(timeout); + } + + @Override + public void jsonNumincrby(K key, String path, double value) { + reactive.jsonNumincrby(key, path, value) + .await().atMost(timeout); + } + + @Override + public List> jsonObjKeys(K key, String path) { + return reactive.jsonObjKeys(key, path) + .await().atMost(timeout); + } + + @Override + public List jsonObjLen(K key, String path) { + return reactive.jsonObjLen(key, path) + .await().atMost(timeout); + } + + @Override + public List jsonStrAppend(K key, String path, String value) { + return reactive.jsonStrAppend(key, path, value) + .await().atMost(timeout); + } + + @Override + public List jsonStrLen(K key, String path) { + return reactive.jsonStrLen(key, path) + .await().atMost(timeout); + } + + @Override + public List jsonToggle(K key, String path) { + return reactive.jsonToggle(key, path) + .await().atMost(timeout); + } + + @Override + public List jsonType(K key, String path) { + return reactive.jsonType(key, path) + .await().atMost(timeout); + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java index 4a18d9e8e7cff..ad1cc652ff6ad 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java @@ -13,6 +13,7 @@ import io.quarkus.redis.datasource.geo.GeoCommands; import io.quarkus.redis.datasource.hash.HashCommands; import io.quarkus.redis.datasource.hyperloglog.HyperLogLogCommands; +import io.quarkus.redis.datasource.json.JsonCommands; import io.quarkus.redis.datasource.keys.KeyCommands; import io.quarkus.redis.datasource.list.ListCommands; import io.quarkus.redis.datasource.pubsub.PubSubCommands; @@ -221,6 +222,11 @@ public BitMapCommands bitmap(Class redisKeyType) { return new BlockingBitmapCommandsImpl<>(this, reactive.bitmap(redisKeyType), timeout); } + @Override + public JsonCommands json(Class redisKeyType) { + return new BlockingJsonCommandsImpl<>(this, reactive.json(redisKeyType), timeout); + } + @Override public PubSubCommands pubsub(Class messageType) { return new BlockingPubSubCommandsImpl<>(this, reactive.pubsub(messageType), timeout); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalJsonCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalJsonCommandsImpl.java new file mode 100644 index 0000000000000..29145fc4faa6c --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalJsonCommandsImpl.java @@ -0,0 +1,185 @@ +package io.quarkus.redis.runtime.datasource; + +import java.time.Duration; + +import io.quarkus.redis.datasource.json.JsonSetArgs; +import io.quarkus.redis.datasource.json.ReactiveTransactionalJsonCommands; +import io.quarkus.redis.datasource.json.TransactionalJsonCommands; +import io.quarkus.redis.datasource.transactions.TransactionalRedisDataSource; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class BlockingTransactionalJsonCommandsImpl extends AbstractTransactionalRedisCommandGroup + implements TransactionalJsonCommands { + + private final ReactiveTransactionalJsonCommands reactive; + + public BlockingTransactionalJsonCommandsImpl(TransactionalRedisDataSource ds, + ReactiveTransactionalJsonCommands reactive, + Duration timeout) { + super(ds, timeout); + this.reactive = reactive; + } + + @Override + public void jsonSet(K key, String path, T value) { + this.reactive.jsonSet(key, path, value) + .await().atMost(this.timeout); + } + + @Override + public void jsonSet(K key, String path, JsonObject json) { + this.reactive.jsonSet(key, path, json) + .await().atMost(this.timeout); + } + + @Override + public void jsonSet(K key, String path, JsonObject json, JsonSetArgs args) { + this.reactive.jsonSet(key, path, json, args) + .await().atMost(this.timeout); + } + + @Override + public void jsonSet(K key, String path, JsonArray json) { + this.reactive.jsonSet(key, path, json) + .await().atMost(this.timeout); + } + + @Override + public void jsonSet(K key, String path, JsonArray json, JsonSetArgs args) { + this.reactive.jsonSet(key, path, json, args) + .await().atMost(this.timeout); + } + + @Override + public void jsonSet(K key, String path, T value, JsonSetArgs args) { + this.reactive.jsonSet(key, path, value, args) + .await().atMost(this.timeout); + } + + @Override + public void jsonGet(K key, Class clazz) { + this.reactive.jsonGet(key, clazz) + .await().atMost(this.timeout); + } + + @Override + public void jsonGetObject(K key) { + this.reactive.jsonGetObject(key) + .await().atMost(this.timeout); + } + + @Override + public void jsonGetArray(K key) { + this.reactive.jsonGetArray(key) + .await().atMost(this.timeout); + } + + @Override + public void jsonGet(K key, String path) { + this.reactive.jsonGet(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonGet(K key, String... paths) { + this.reactive.jsonGet(key, paths) + .await().atMost(this.timeout); + } + + @Override + public void jsonArrAppend(K key, String path, T... values) { + this.reactive.jsonArrAppend(key, path, values) + .await().atMost(this.timeout); + } + + @Override + public void jsonArrIndex(K key, String path, T value, int start, int end) { + this.reactive.jsonArrIndex(key, path, value, start, end) + .await().atMost(this.timeout); + } + + @Override + public void jsonArrInsert(K key, String path, int index, T... values) { + this.reactive.jsonArrInsert(key, path, index, values) + .await().atMost(this.timeout); + } + + @Override + public void jsonArrLen(K key, String path) { + this.reactive.jsonArrLen(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonArrPop(K key, Class clazz, String path, int index) { + this.reactive.jsonArrPop(key, clazz, path, index) + .await().atMost(this.timeout); + } + + @Override + public void jsonArrTrim(K key, String path, int start, int stop) { + this.reactive.jsonArrTrim(key, path, stop, stop) + .await().atMost(this.timeout); + } + + @Override + public void jsonClear(K key, String path) { + this.reactive.jsonClear(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonDel(K key, String path) { + this.reactive.jsonDel(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonMget(String path, K... keys) { + this.reactive.jsonMget(path, keys) + .await().atMost(this.timeout); + } + + @Override + public void jsonNumincrby(K key, String path, double value) { + this.reactive.jsonNumincrby(key, path, value) + .await().atMost(this.timeout); + } + + @Override + public void jsonObjKeys(K key, String path) { + this.reactive.jsonObjKeys(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonObjLen(K key, String path) { + this.reactive.jsonObjLen(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonStrAppend(K key, String path, String value) { + this.reactive.jsonStrAppend(key, path, value) + .await().atMost(this.timeout); + } + + @Override + public void jsonStrLen(K key, String path) { + this.reactive.jsonStrLen(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonToggle(K key, String path) { + this.reactive.jsonToggle(key, path) + .await().atMost(this.timeout); + } + + @Override + public void jsonType(K key, String path) { + this.reactive.jsonType(key, path) + .await().atMost(this.timeout); + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java index 69f5579c91eba..1e421d497de77 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java @@ -6,6 +6,7 @@ import io.quarkus.redis.datasource.geo.TransactionalGeoCommands; import io.quarkus.redis.datasource.hash.TransactionalHashCommands; import io.quarkus.redis.datasource.hyperloglog.TransactionalHyperLogLogCommands; +import io.quarkus.redis.datasource.json.TransactionalJsonCommands; import io.quarkus.redis.datasource.keys.TransactionalKeyCommands; import io.quarkus.redis.datasource.list.TransactionalListCommands; import io.quarkus.redis.datasource.set.TransactionalSetCommands; @@ -89,6 +90,11 @@ public TransactionalBitMapCommands bitmap(Class redisKeyType) { return new BlockingTransactionalBitMapCommandsImpl<>(this, reactive.bitmap(redisKeyType), timeout); } + @Override + public TransactionalJsonCommands json(Class redisKeyType) { + return new BlockingTransactionalJsonCommandsImpl<>(this, reactive.json(redisKeyType), timeout); + } + @Override public void execute(String command, String... args) { reactive.execute(command, args).await().atMost(timeout); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveJsonCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveJsonCommandsImpl.java new file mode 100644 index 0000000000000..3284ddfbfb05f --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveJsonCommandsImpl.java @@ -0,0 +1,293 @@ +package io.quarkus.redis.runtime.datasource; + +import static io.smallrye.mutiny.helpers.ParameterValidation.nonNull; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.ReactiveRedisDataSource; +import io.quarkus.redis.datasource.json.JsonSetArgs; +import io.quarkus.redis.datasource.json.ReactiveJsonCommands; +import io.smallrye.mutiny.Uni; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.redis.client.Response; +import io.vertx.redis.client.ResponseType; + +public class ReactiveJsonCommandsImpl extends AbstractJsonCommands implements ReactiveJsonCommands { + + private final ReactiveRedisDataSource reactive; + + public ReactiveJsonCommandsImpl(ReactiveRedisDataSourceImpl redis, Class k) { + super(redis, k); + this.reactive = redis; + } + + @Override + public ReactiveRedisDataSource getDataSource() { + return reactive; + } + + @Override + public Uni jsonSet(K key, String path, T value) { + return _jsonSet(key, path, value) + .replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonObject json) { + return _jsonSet(key, path, json) + .replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonObject json, JsonSetArgs args) { + return _jsonSet(key, path, json, args) + .replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonArray json) { + return _jsonSet(key, path, json) + .replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonArray json, JsonSetArgs args) { + return _jsonSet(key, path, json, args) + .replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, T value, JsonSetArgs args) { + return _jsonSet(key, path, value, args) + .replaceWithVoid(); + } + + @Override + public Uni jsonGet(K key, Class clazz) { + nonNull(clazz, "clazz"); + return _jsonGet(key) + .map(r -> { + if (r == null) { + return null; + } + return Json.decodeValue(r.getDelegate().toBuffer(), clazz); + }); + } + + @Override + public Uni jsonGetObject(K key) { + return _jsonGet(key) + .map(r -> r.toBuffer().toJsonObject()); + } + + @Override + public Uni jsonGetArray(K key) { + return _jsonGet(key) + .map(r -> r.toBuffer().toJsonArray()); + } + + @Override + public Uni jsonGet(K key, String path) { + return _jsonGet(key, path) + .map(r -> r.toBuffer().toJsonArray()); + } + + @Override + public Uni jsonGet(K key, String... paths) { + return _jsonGet(key, paths) + .map(r -> { + if (r == null || r.toString().equalsIgnoreCase("null")) { // JSON null + return null; + } + return r.toBuffer().toJsonObject(); + }); + } + + @Override + public Uni> jsonArrAppend(K key, String path, T... values) { + return _jsonArrAppend(key, path, values) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + @Override + public Uni> jsonArrIndex(K key, String path, T value, int start, int end) { + return _jsonArrIndex(key, path, value, start, end) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + static List decodeAsListOfInteger(Response r) { + List list = new ArrayList<>(); + if (r.type() == ResponseType.MULTI) { + for (Response response : r) { + list.add(response == null ? null : response.toInteger()); + } + } else { + list.add(r.toInteger()); + } + return list; + } + + @Override + public Uni> jsonArrInsert(K key, String path, int index, T... values) { + return _jsonArrInsert(key, path, index, values) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + @Override + public Uni> jsonArrLen(K key, String path) { + return _jsonArrLen(key, path) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + @Override + public Uni> jsonArrPop(K key, Class clazz, String path, int index) { + nonNull(clazz, "clazz"); + return _jsonArrPop(key, path, index) + .map(r -> decodeArrPopResponse(clazz, r)); + } + + static List decodeArrPopResponse(Class clazz, Response r) { + List list = new ArrayList<>(); + if (r == null) { + list.add(null); + return list; + } + if (r.type() == ResponseType.MULTI) { + for (Response response : r) { + list.add(response == null ? null : Json.decodeValue(response.toString(), clazz)); + } + } else { + list.add(Json.decodeValue(r.toString(), clazz)); + } + return list; + } + + @Override + public Uni> jsonArrTrim(K key, String path, int start, int stop) { + return _jsonArrTrim(key, path, start, stop) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + @Override + public Uni jsonClear(K key, String path) { + return _jsonClear(key, path) + .map(Response::toInteger); + } + + @Override + public Uni jsonDel(K key, String path) { + return _jsonDel(key, path) + .map(Response::toInteger); + } + + @Override + public Uni> jsonMget(String path, K... keys) { + return _jsonMget(path, keys) + .map(ReactiveJsonCommandsImpl::decodeMGetResponse); + } + + static List decodeMGetResponse(Response r) { + List list = new ArrayList<>(); + if (r.type() == ResponseType.MULTI) { + for (Response response : r) { + list.add(response == null ? null : response.toBuffer().toJsonArray()); + } + } else { + list.add(r.toBuffer().toJsonArray()); + } + return list; + } + + @Override + public Uni jsonNumincrby(K key, String path, double value) { + return _jsonNumincrby(key, path, value) + .replaceWithVoid(); + } + + @Override + public Uni>> jsonObjKeys(K key, String path) { + return _jsonObjKeys(key, path) + .map(ReactiveJsonCommandsImpl::decodeObjKeysResponse); + } + + static List> decodeObjKeysResponse(Response r) { + List> list = new ArrayList<>(); + if (r.type() == ResponseType.MULTI) { + List sub = new ArrayList<>(); + for (Response item : r) { + if (item == null) { + list.add(null); + } else { + if (item.type() == ResponseType.MULTI) { + for (Response nested : item) { + sub.add(nested == null ? null : nested.toString()); + } + } else { // BULK + sub.add(item.toString()); + } + } + } + list.add(sub); + } else { + list.add(null); + } + return list; + } + + @Override + public Uni> jsonObjLen(K key, String path) { + return _jsonObjLen(key, path) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + @Override + public Uni> jsonStrAppend(K key, String path, String value) { + return _jsonStrAppend(key, path, value) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + @Override + public Uni> jsonStrLen(K key, String path) { + return _jsonStrLen(key, path) + .map(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + } + + @Override + public Uni> jsonToggle(K key, String path) { + return _jsonToggle(key, path) + .map(ReactiveJsonCommandsImpl::decodeToggleResponse); + } + + static List decodeToggleResponse(Response r) { + List list = new ArrayList<>(); + if (r.type() == ResponseType.MULTI) { + for (Response response : r) { + list.add(response == null ? null : response.toBoolean()); + } + } else { + list.add(r.toBoolean()); + } + return list; + } + + @Override + public Uni> jsonType(K key, String path) { + return _jsonType(key, path) + .map(ReactiveJsonCommandsImpl::decodeTypeResponse); + } + + static List decodeTypeResponse(Response r) { + List list = new ArrayList<>(); + if (r.type() == ResponseType.MULTI) { + for (Response response : r) { + list.add(response == null ? null : response.toString()); + } + } else { + list.add(r.toString()); + } + return list; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java index ed4e1aafe7fb9..056b6729b099c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java @@ -14,6 +14,7 @@ import io.quarkus.redis.datasource.geo.ReactiveGeoCommands; import io.quarkus.redis.datasource.hash.ReactiveHashCommands; import io.quarkus.redis.datasource.hyperloglog.ReactiveHyperLogLogCommands; +import io.quarkus.redis.datasource.json.ReactiveJsonCommands; import io.quarkus.redis.datasource.keys.ReactiveKeyCommands; import io.quarkus.redis.datasource.list.ReactiveListCommands; import io.quarkus.redis.datasource.pubsub.ReactivePubSubCommands; @@ -277,6 +278,11 @@ public ReactiveBitMapCommands bitmap(Class redisKeyType) { return new ReactiveBitMapCommandsImpl<>(this, redisKeyType); } + @Override + public ReactiveJsonCommands json(Class redisKeyType) { + return new ReactiveJsonCommandsImpl<>(this, redisKeyType); + } + @Override public ReactivePubSubCommands pubsub(Class messageType) { return new ReactivePubSubCommandsImpl<>(this, messageType); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalJsonCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalJsonCommandsImpl.java new file mode 100644 index 0000000000000..ea5ae52c3dd11 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalJsonCommandsImpl.java @@ -0,0 +1,213 @@ +package io.quarkus.redis.runtime.datasource; + +import static io.quarkus.redis.runtime.datasource.ReactiveJsonCommandsImpl.decodeArrPopResponse; + +import io.quarkus.redis.datasource.json.JsonSetArgs; +import io.quarkus.redis.datasource.json.ReactiveTransactionalJsonCommands; +import io.quarkus.redis.datasource.transactions.ReactiveTransactionalRedisDataSource; +import io.smallrye.mutiny.Uni; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.redis.client.Response; + +public class ReactiveTransactionalJsonCommandsImpl extends AbstractTransactionalCommands + implements ReactiveTransactionalJsonCommands { + + private final ReactiveJsonCommandsImpl reactive; + + public ReactiveTransactionalJsonCommandsImpl(ReactiveTransactionalRedisDataSource ds, + ReactiveJsonCommandsImpl reactive, TransactionHolder tx) { + super(ds, tx); + this.reactive = reactive; + } + + @Override + public Uni jsonSet(K key, String path, T value) { + this.tx.enqueue(resp -> null); + return this.reactive._jsonSet(key, path, value).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonObject json) { + this.tx.enqueue(resp -> null); + return this.reactive._jsonSet(key, path, json).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonObject json, JsonSetArgs args) { + this.tx.enqueue(resp -> null); + return this.reactive._jsonSet(key, path, json, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonArray json) { + this.tx.enqueue(resp -> null); + return this.reactive._jsonSet(key, path, json).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, JsonArray json, JsonSetArgs args) { + this.tx.enqueue(resp -> null); + return this.reactive._jsonSet(key, path, json, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonSet(K key, String path, T value, JsonSetArgs args) { + this.tx.enqueue(resp -> null); + return this.reactive._jsonSet(key, path, value, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonGet(K key, Class clazz) { + this.tx.enqueue(r -> { + if (r == null) { + return null; + } + return Json.decodeValue(r.getDelegate().toBuffer(), clazz); + }); + return this.reactive._jsonGet(key).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonGetObject(K key) { + this.tx.enqueue(r -> r.toBuffer().toJsonObject()); + return this.reactive._jsonGet(key).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonGetArray(K key) { + this.tx.enqueue(r -> r.toBuffer().toJsonArray()); + return this.reactive._jsonGet(key).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonGet(K key, String path) { + this.tx.enqueue(r -> r.toBuffer().toJsonArray()); + return this.reactive._jsonGet(key, path).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonGet(K key, String... paths) { + this.tx.enqueue(r -> { + if (r == null || r.toString().equalsIgnoreCase("null")) { // JSON null + return null; + } + return r.toBuffer().toJsonObject(); + }); + return this.reactive._jsonGet(key, paths).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonArrAppend(K key, String path, T... values) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonArrAppend(key, path, values) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonArrIndex(K key, String path, T value, int start, int end) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonArrIndex(key, path, value, start, end) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonArrInsert(K key, String path, int index, T... values) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonArrInsert(key, path, index, values) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonArrLen(K key, String path) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonArrLen(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonArrPop(K key, Class clazz, String path, int index) { + this.tx.enqueue(r -> decodeArrPopResponse(clazz, r)); + return this.reactive._jsonArrPop(key, path, index) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonArrTrim(K key, String path, int start, int stop) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonArrTrim(key, path, start, stop) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonClear(K key, String path) { + this.tx.enqueue(Response::toInteger); + return this.reactive._jsonClear(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonDel(K key, String path) { + this.tx.enqueue(Response::toInteger); + return this.reactive._jsonDel(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonMget(String path, K... keys) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeMGetResponse); + return this.reactive._jsonMget(path, keys) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonNumincrby(K key, String path, double value) { + this.tx.enqueue(r -> null); + return this.reactive._jsonNumincrby(key, path, value) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonObjKeys(K key, String path) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeObjKeysResponse); + return this.reactive._jsonObjKeys(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonObjLen(K key, String path) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonObjLen(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonStrAppend(K key, String path, String value) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonStrAppend(key, path, value) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonStrLen(K key, String path) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeAsListOfInteger); + return this.reactive._jsonStrLen(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonToggle(K key, String path) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeToggleResponse); + return this.reactive._jsonToggle(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni jsonType(K key, String path) { + this.tx.enqueue(ReactiveJsonCommandsImpl::decodeTypeResponse); + return this.reactive._jsonType(key, path) + .invoke(this::queuedOrDiscard).replaceWithVoid(); + } + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java index 4b50d6ff726b2..94ee58832e688 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java @@ -9,6 +9,7 @@ import io.quarkus.redis.datasource.geo.ReactiveTransactionalGeoCommands; import io.quarkus.redis.datasource.hash.ReactiveTransactionalHashCommands; import io.quarkus.redis.datasource.hyperloglog.ReactiveTransactionalHyperLogLogCommands; +import io.quarkus.redis.datasource.json.ReactiveTransactionalJsonCommands; import io.quarkus.redis.datasource.keys.ReactiveTransactionalKeyCommands; import io.quarkus.redis.datasource.list.ReactiveTransactionalListCommands; import io.quarkus.redis.datasource.set.ReactiveTransactionalSetCommands; @@ -97,6 +98,12 @@ public ReactiveTransactionalBitMapCommands bitmap(Class redisKeyType) (ReactiveBitMapCommandsImpl) this.reactive.bitmap(redisKeyType), tx); } + @Override + public ReactiveTransactionalJsonCommands json(Class redisKeyType) { + return new ReactiveTransactionalJsonCommandsImpl<>(this, + (ReactiveJsonCommandsImpl) this.reactive.json(redisKeyType), tx); + } + @Override public ReactiveTransactionalKeyCommands key(Class redisKeyType) { return new ReactiveTransactionalKeyCommandsImpl<>(this, diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java index 51ecf01e711fa..d21b48131cceb 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/RedisCommand.java @@ -54,6 +54,13 @@ public RedisCommand putAll(List args) { return this; } + public RedisCommand putAll(String[] args) { + for (Object arg : args) { + put(arg); + } + return this; + } + public RedisCommand putArgs(RedisCommandExtraArguments arguments) { putAll(arguments.toArgs()); return this; @@ -81,6 +88,6 @@ public void putNullable(byte[] encoded) { } else { this.request.arg(encoded); } - } + } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java index 7c6cddd7b4290..dce0d298cafcf 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java @@ -18,6 +18,15 @@ static void notNullOrEmpty(X[] array, String name) { } } + static void notNullOrBlank(String v, String name) { + if (v == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (v.isBlank()) { + throw new IllegalArgumentException("`" + name + "` must not be blank"); + } + } + static void notNullOrEmpty(Collection col, String name) { if (col == null) { throw new IllegalArgumentException("`" + name + "` must not be `null`"); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java index 60380b44dd6d5..4d7765cc83626 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java @@ -1,5 +1,7 @@ package io.quarkus.redis.datasource; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.AfterAll; @@ -12,6 +14,7 @@ import io.vertx.mutiny.redis.client.Redis; import io.vertx.mutiny.redis.client.RedisAPI; import io.vertx.mutiny.redis.client.Request; +import io.vertx.mutiny.redis.client.Response; public class DatasourceTestBase { @@ -22,7 +25,7 @@ public class DatasourceTestBase { public static RedisAPI api; static GenericContainer server = new GenericContainer<>( - DockerImageName.parse(System.getProperty("redis.base.image", "redis:7-alpine"))) + DockerImageName.parse(System.getProperty("redis.base.image", "redis/redis-stack:7.0.2-RC2"))) .withExposedPorts(6379); @BeforeAll @@ -41,6 +44,16 @@ static void cleanup() { vertx.closeAndAwait(); } + public static List getAvailableCommands() { + List commands = new ArrayList<>(); + Response list = redis.send(Request.cmd(Command.COMMAND)).await().indefinitely(); + for (Response response : list) { + commands.add(response.get(0).toString()); + } + return commands; + + } + public static String getRedisVersion() { String info = redis.send(Request.cmd(Command.INFO)).await().indefinitely().toString(); // Look for the redis_version line diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/JsonCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/JsonCommandsTest.java new file mode 100644 index 0000000000000..e1332920cd2ee --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/JsonCommandsTest.java @@ -0,0 +1,499 @@ +package io.quarkus.redis.datasource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.redis.datasource.json.JsonCommands; +import io.quarkus.redis.datasource.json.JsonSetArgs; +import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class JsonCommandsTest extends DatasourceTestBase { + + private RedisDataSource ds; + + private JsonCommands json; + + @BeforeEach + void initialize() { + ds = new BlockingRedisDataSourceImpl(vertx, redis, api, Duration.ofSeconds(1)); + json = ds.json(); + } + + @AfterEach + public void clear() { + ds.flushall(); + } + + @Test + void getDataSource() { + assertThat(ds).isEqualTo(json.getDataSource()); + } + + @Test + public void testJson() { + json.jsonSet("doc", "$", + new JsonObject().put("a", 2).put("b", 3).put("nested", new JsonObject().put("a", 4).put("b", null))); + assertThat(json.jsonGet("doc", "$..b")).containsExactly(3, null); + JsonObject object = json.jsonGet("doc", "..a", "$..b"); + assertThat(object).hasSize(2); + assertThat(object.getJsonArray("..a")).containsExactly(2, 4); + assertThat(object.getJsonArray("$..b")).containsExactly(3, null); + } + + @Test + public void jsonSetRoot() { + json.jsonSet("animal", "$", "dog"); + assertThat(json.jsonGet("animal", "$")).containsExactly("dog"); + } + + public static class Person { + public String name; + public int age; + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Person person = (Person) o; + return age == person.age && Objects.equals(name, person.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + } + + @Test + public void jsonSet() { + Person p = new Person(); + p.name = "luke"; + p.age = 20; + + json.jsonSet("luke", "$", p); + assertThat(json.jsonGet("luke", Person.class)).isEqualTo(p); + + Person p2 = new Person(); + p2.name = "leia"; + p2.age = 20; + json.jsonSet("luke", "$.sister", p2); + json.jsonSet("luke", "$.friends", new JsonArray().add("Obiwan").add("Han"), new JsonSetArgs().nx()); + json.jsonSet("luke", "$.enemies", new JsonArray().add("Darth Sidious"), new JsonSetArgs().nx()); + json.jsonSet("luke", "$.weapons", new JsonArray().add("Light Saber")); + json.jsonSet("luke", "$.father", new JsonObject().put("name", "Darth Vader").put("otherName", "Anakin")); + json.jsonSet("luke", "$.sister.father", new JsonObject().put("name", "Darth Vader").put("otherName", "Anakin")); + + JsonObject luke = json.jsonGetObject("luke"); + assertThat(luke.getString("name")).isEqualTo("luke"); + assertThat(luke.getInteger("age")).isEqualTo(20); + assertThat(luke.getJsonArray("enemies")).containsExactly("Darth Sidious"); + assertThat(luke.getJsonArray("weapons")).containsExactly("Light Saber"); + assertThat(luke.getJsonObject("sister").getInteger("age")).isEqualTo(20); + assertThat(luke.getJsonObject("sister").getString("name")).isEqualTo("leia"); + assertThat(luke.getJsonObject("sister").getJsonObject("father").getString("name")).isEqualTo("Darth Vader"); + assertThat(luke.getJsonArray("friends")).containsExactly("Obiwan", "Han"); + assertThat(luke.getJsonObject("father").getString("name")).isEqualTo("Darth Vader"); + + json.jsonSet("luke", "$", p, new JsonSetArgs().nx()); + + luke = json.jsonGetObject("luke"); + assertThat(luke.getString("name")).isEqualTo("luke"); + assertThat(luke.getInteger("age")).isEqualTo(20); + assertThat(luke.getJsonObject("sister").getInteger("age")).isEqualTo(20); + assertThat(luke.getJsonObject("sister").getString("name")).isEqualTo("leia"); + assertThat(luke.getJsonObject("sister").getJsonObject("father").getString("name")).isEqualTo("Darth Vader"); + assertThat(luke.getJsonArray("friends")).containsExactly("Obiwan", "Han"); + assertThat(luke.getJsonObject("father").getString("name")).isEqualTo("Darth Vader"); + + json.jsonSet("luke", "$", p, new JsonSetArgs().xx()); + + luke = json.jsonGetObject("luke"); + assertThat(luke.getString("name")).isEqualTo("luke"); + assertThat(luke.getInteger("age")).isEqualTo(20); + assertThat(luke.getJsonObject("sister")).isNull(); + } + + @Test + public void jsonGetArray() { + json.jsonSet("array", "$", List.of("a", "b", "c")); + assertThat(json.jsonGetArray("array")).containsExactly("a", "b", "c"); + } + + @Test + public void jsonSetNull() { + json.jsonSet("null", "$", (Object) null); + assertThatThrownBy(() -> json.jsonSet("null-json", "$", (JsonArray) null)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> json.jsonSet("null-json", "$", (JsonObject) null)) + .isInstanceOf(IllegalArgumentException.class); + + assertThat(json.jsonGet("null")).isNull(); + assertThat(json.jsonGet("null", String.class)).isNull(); + + assertThat(json.jsonGet("missing", String.class)).isNull(); + assertThat(json.jsonGet("missing")).isNull(); + } + + @Test + public void testJsonGet() { + Person p = new Person(); + p.name = "luke"; + p.age = 20; + + json.jsonSet("luke", "$", p); + assertThat(json.jsonGet("luke", Person.class)).isEqualTo(p); + + Person p2 = new Person(); + p2.name = "leia"; + p2.age = 20; + json.jsonSet("luke", "$.sister", p2); + json.jsonSet("luke", "$.friends", new JsonArray().add("Obiwan").add("Han"), new JsonSetArgs().nx()); + json.jsonSet("luke", "$.enemies", new JsonArray().add("Darth Sidious"), new JsonSetArgs().nx()); + json.jsonSet("luke", "$.weapons", new JsonArray().add("Light Saber")); + json.jsonSet("luke", "$.father", new JsonObject().put("name", "Darth Vader").put("otherName", "Anakin")); + json.jsonSet("luke", "$.sister.father", new JsonObject().put("name", "Darth Vader").put("otherName", "Anakin")); + + assertThat(json.jsonGet("luke", "$")).hasSize(1); + assertThat(json.jsonGet("luke", "$.missing")).hasSize(0); + assertThat(json.jsonGet("luke").getString("name")).isEqualTo("luke"); + assertThat(json.jsonGet("luke", "$.friends[0]").getString(0)).isEqualTo("Obiwan"); + assertThat(json.jsonGet("luke", "$.friends[2]")).hasSize(0); + JsonObject query = json.jsonGet("luke", "$.friends", "..father"); + assertThat(query).hasSize(2); + assertThat(query.getJsonArray("$.friends").getJsonArray(0)).containsExactly("Obiwan", "Han"); + assertThat(query.getJsonArray("..father").getJsonObject(0).getString("name")).isEqualTo("Darth Vader"); + assertThat(query.getJsonArray("..father").getJsonObject(1).getString("name")).isEqualTo("Darth Vader"); + + query = json.jsonGet("luke", "$.friends", ".missing", "..father"); + assertThat(query).hasSize(3); + assertThat(query.getJsonArray(".missing")).isEmpty(); + } + + @Test + void jsonArrAppend() { + JsonObject test = JsonObject.of("a", 10, "arr", JsonArray.of(1, 2)); + JsonArray arr = JsonArray.of("a", "b", "c"); + json.jsonSet(key, "$", test); + json.jsonSet("arr", "$", arr); + json.jsonSet("obj", "$", JsonObject.of("name", "clement")); + + List list = json.jsonArrAppend(key, "arr", new io.quarkus.redis.datasource.Person("luke", "skywalker")); + assertThat(list).containsExactly(3); // 1, 2, luke... + list = json.jsonArrAppend("arr", "$", new io.quarkus.redis.datasource.Person("luke", "skywalker")); + assertThat(list).containsExactly(4); // a, b, c, luke... + assertThatThrownBy(() -> json.jsonArrAppend("obj", ".name", "a", "b")) + .hasMessageContaining(".name"); + + assertThatThrownBy(() -> json.jsonArrAppend("missing", ".name", "a", "b")) + .hasMessageContaining("key"); + } + + @Test + void jsonArrIndex() { + JsonObject test = JsonObject.of("a", JsonArray.of(1, 2, 3, 2), + "nested", JsonObject.of("a", JsonArray.of(3, 4))); + json.jsonSet(key, "$", test); + assertThat(json.jsonArrIndex(key, "$..a", 2)).containsExactly(1, -1); + assertThat(json.jsonArrIndex(key, "$..a", 2, 0, 10)).containsExactly(1, -1); + + assertThatThrownBy(() -> json.jsonArrIndex(key, ".name", 2)) + .hasMessageContaining(".name"); + + assertThatThrownBy(() -> json.jsonArrIndex("missing", ".name", "a")) + .hasMessageContaining(".name"); + + test = JsonObject.of("a", JsonArray.of(1, 2, 3, 2), + "nested", JsonObject.of("a", false)); + json.jsonSet(key, "$", test); + assertThat(json.jsonArrIndex(key, "$..a", 2)).containsExactly(1, null); + } + + @Test + void jsonArrInsert() { + JsonObject test = JsonObject.of("a", JsonArray.of(3), + "nested", JsonObject.of("a", JsonArray.of(3, 4))); + json.jsonSet(key, "$", test); + assertThat(json.jsonArrInsert(key, "$..a", 0, 1, 2)).containsExactly(3, 4); + JsonObject object = json.jsonGetObject(key); + assertThat(object.getJsonArray("a")).containsExactly(1, 2, 3); + assertThat(object.getJsonObject("nested").getJsonArray("a")).containsExactly(1, 2, 3, 4); + } + + @Test + void jsonArrInsertWithNull() { + JsonObject test = JsonObject.of("a", JsonArray.of(1, 2, 3, 2), + "nested", JsonObject.of("a", 1)); + json.jsonSet(key, "$", test); + assertThat(json.jsonArrInsert(key, "$..a", 0, 1, 2)).containsExactly(6, null); + } + + @Test + void jsonArrayLen() { + JsonObject test = JsonObject.of("a", JsonArray.of(3), + "nested", JsonObject.of("a", JsonArray.of(3, 4))); + json.jsonSet(key, "$", test); + json.jsonSet("doc", "$", JsonArray.of("a", "b", "c")); + json.jsonSet("doc2", "$", JsonObject.of("a", "b")); + + assertThat(json.jsonArrLen(key, "$..a")).containsExactly(1, 2); + assertThat(json.jsonArrLen("doc")).hasValue(3); + assertThatThrownBy(() -> json.jsonArrLen("doc2")) + .hasMessageContaining("Path"); + } + + @Test + void jsonArrayLenWithNull() { + JsonObject test = JsonObject.of("a", JsonArray.of(1, 2, 3, 2), + "nested", JsonObject.of("a", 2)); + json.jsonSet(key, "$", test); + assertThat(json.jsonArrLen(key, "$..a")).containsExactly(4, null); + } + + @Test + void jsonArrayPop() { + JsonObject test = JsonObject.of("a", JsonArray.of(3), + "nested", JsonObject.of("a", JsonArray.of(3, 4))); + json.jsonSet(key, "$", test); + + assertThat(json.jsonArrPop(key, Integer.class, "$..a", -1)).containsExactly(3, 4); + assertThat(json.jsonGetObject(key).getJsonArray("a")).isEmpty(); + assertThat(json.jsonGetObject(key).getJsonObject("nested").getJsonArray("a")).containsExactly(3); + } + + @Test + void jsonArrayPopWithNull() { + JsonObject test = JsonObject.of("a", JsonArray.of("foo", "bar"), + "nested", JsonObject.of("a", 2), "nested2", JsonObject.of("a", new JsonArray())); + json.jsonSet(key, "$", test); + assertThat(json.jsonArrPop(key, String.class, "$..a")).containsExactly("bar", null, null); + } + + @Test + void jsonArrayPopWithDefault() { + JsonObject test = JsonObject.of("a", JsonArray.of(3), + "nested", JsonObject.of("a", JsonArray.of(3, 4))); + json.jsonSet(key, "$", test); + + assertThat(json.jsonArrPop(key, Integer.class, "$..a")).containsExactly(3, 4); + assertThat(json.jsonGetObject(key).getJsonArray("a")).isEmpty(); + assertThat(json.jsonGetObject(key).getJsonObject("nested").getJsonArray("a")).containsExactly(3); + + json.jsonSet("arr", "$", JsonArray.of(1, 2, 3, 4)); + assertThat(json.jsonArrPop("arr", Integer.class)).isEqualTo(4); + + json.jsonSet("empty", "$", JsonArray.of()); + assertThat(json.jsonArrPop("empty", Integer.class)).isNull(); + } + + @Test + void jsonArrayTrim() { + JsonObject test = JsonObject.of("a", JsonArray.of(), + "nested", JsonObject.of("a", JsonArray.of(1, 4))); + json.jsonSet(key, "$", test); + + assertThat(json.jsonArrTrim(key, "$..a", 1, 1)).containsExactly(0, 1); + assertThat(json.jsonGetObject(key).getJsonArray("a")).isEmpty(); + assertThat(json.jsonGetObject(key).getJsonObject("nested").getJsonArray("a")).containsExactly(4); + } + + @Test + void jsonArrayTrimWithNull() { + JsonObject test = JsonObject.of("a", JsonArray.of(1, 2, 3, 2), + "nested", JsonObject.of("a", 1)); + json.jsonSet(key, "$", test); + + assertThat(json.jsonArrTrim(key, "$..a", 1, 1)).containsExactly(1, null); + assertThat(json.jsonGetObject(key).getJsonArray("a")).containsExactly(2); + assertThat(json.jsonGetObject(key).getJsonObject("nested").getInteger("a")).isEqualTo(1); + } + + @Test + void jsonClearAll() { + JsonObject test = JsonObject.of("obj", JsonObject.of("a", 1, "b", 2), + "arr", JsonArray.of(1, 2, 3), "str", "foo", "bool", true, + "int", 42, "float", 3.14); + json.jsonSet(key, "$", test); + + json.jsonClear(key); + JsonObject object = json.jsonGet(key); + assertThat(object).isEmpty(); + } + + @Test + void jsonClear() { + JsonObject test = JsonObject.of("obj", JsonObject.of("a", 1, "b", 2), + "arr", JsonArray.of(1, 2, 3), "str", "foo", "bool", true, + "int", 42, "float", 3.14); + json.jsonSet(key, "$", test); + + json.jsonClear(key, "$.*"); + JsonObject object = json.jsonGet(key); + assertThat(object.getJsonObject("obj")).isEmpty(); + assertThat(object.getJsonArray("arr")).isEmpty(); + assertThat(object.getString("str")).isEqualTo("foo"); + assertThat(object.getBoolean("bool")).isTrue(); + assertThat(object.getInteger("int")).isEqualTo(0); + assertThat(object.getDouble("float")).isEqualTo(0.0); + } + + @Test + void jsonDel() { + JsonObject test = JsonObject.of("a", 1, "nested", JsonObject.of("a", 2, "b", 3)); + json.jsonSet(key, "$", test); + + json.jsonDel(key, "$..a"); + JsonObject object = json.jsonGet(key); + assertThat(object.getString("a")).isNull(); + assertThat(object.getJsonObject("nested")).containsExactly(entry("b", 3)); + } + + @Test + void jsonDelAll() { + JsonObject test = JsonObject.of("a", 1, "nested", JsonObject.of("a", 2, "b", 3)); + json.jsonSet(key, "$", test); + + json.jsonDel(key); + JsonObject object = json.jsonGet(key); + assertThat(object).isNull(); + } + + @Test + void mget() { + // redis> JSON.SET doc1 $ '{"a":1, "b": 2, "nested": {"a": 3}, "c": null}' + //OK + //redis> JSON.SET doc2 $ '{"a":4, "b": 5, "nested": {"a": 6}, "c": null}' + //OK + //redis> JSON.MGET doc1 doc2 $..a + //1) "[1,3]" + //2) "[4,6]" + + JsonObject j1 = JsonObject.of("a", 1, "b", 2, "nested", JsonObject.of("a", 3), "c", null); + JsonObject j2 = JsonObject.of("a", 4, "b", 5, "nested", JsonObject.of("a", 6), "c", null); + + json.jsonSet("doc1", j1); + json.jsonSet("doc2", j2); + + List arrays = json.jsonMget("doc1", "doc2", "$..a"); + assertThat(arrays.get(0)).containsExactly(1, 3); + assertThat(arrays.get(1)).containsExactly(4, 6); + + arrays = json.jsonMget("doc1", "doc2", "$..d"); + assertThat(arrays).hasSize(2).allSatisfy(a -> assertThat(a).isEmpty()); + + arrays = json.jsonMget("doc1", "$..a"); + assertThat(arrays.get(0)).containsExactly(1, 3); + } + + @Test + void numIncrBy() { + JsonObject test = JsonObject.of("a", "b", "b", + JsonArray.of(JsonObject.of("a", 2), JsonObject.of("a", 5), JsonObject.of("a", "c"))); + JsonObject res = JsonObject.of("a", "b", "b", + JsonArray.of(JsonObject.of("a", 4), JsonObject.of("a", 7), JsonObject.of("a", "c"))); + json.jsonSet(key, test); + json.jsonNumincrby(key, "$.a", 2); + assertThat(json.jsonGet(key)).isEqualTo(test); + json.jsonNumincrby(key, "$..a", 2); + assertThat(json.jsonGet(key)).isEqualTo(res); + } + + @Test + void objKeys() { + JsonObject test = JsonObject.of("a", JsonArray.of(3), "nested", JsonObject.of("a", JsonObject.of("b", 2, "c", 1))); + json.jsonSet(key, test); + assertThat(json.jsonObjKeys(key, "$..a")).containsExactly(null, List.of("b", "c")); + + assertThat(json.jsonObjKeys(key)).containsExactly("a", "nested"); + assertThat(json.jsonObjKeys(key, "$..missing")).containsExactly(Collections.emptyList()); + } + + @Test + void objLen() { + JsonObject test = JsonObject.of("a", JsonArray.of(3), "nested", JsonObject.of("a", JsonObject.of("b", 2, "c", 1))); + json.jsonSet(key, test); + json.jsonSet("empty", new JsonObject()); + json.jsonSet("arr", new JsonArray()); + assertThat(json.jsonObjLen(key, "$..a")).containsExactly(null, 2); + assertThat(json.jsonObjLen(key)).hasValue(2); + assertThat(json.jsonObjLen("empty")).hasValue(0); + assertThatThrownBy(() -> json.jsonObjLen("arr")).hasMessageContaining("array"); + assertThat(json.jsonObjLen(key, "$..missing")).isEmpty(); + } + + @Test + void strAppend() { + JsonObject test = JsonObject.of("a", "foo", "nested", JsonObject.of("a", "hello"), "nested2", JsonObject.of("a", 31)); + json.jsonSet(key, test); + json.jsonSet("empty", new JsonObject()); + json.jsonSet("str", "hello"); + assertThat(json.jsonStrAppend(key, "$..a", "baz buzz")).containsExactly(11, 13, null); + JsonObject object = json.jsonGet(key); + assertThat(object.getString("a")).isEqualTo("foobaz buzz"); + assertThat(object.getJsonObject("nested").getString("a")).isEqualTo("hellobaz buzz"); + + assertThat(json.jsonStrAppend("str", null, "-hello")).containsExactly(11); + assertThatThrownBy(() -> json.jsonStrAppend("empty", null, "goo")).hasMessageContaining("string"); + assertThat(json.jsonStrAppend(key, "$..missing", "gaa")).isEmpty(); + } + + @Test + void strLen() { + JsonObject test = JsonObject.of("a", "foo", "nested", JsonObject.of("a", "hello"), "nested2", JsonObject.of("a", 31)); + json.jsonSet(key, test); + json.jsonSet("empty", new JsonObject()); + json.jsonSet("str", "hello"); + assertThat(json.jsonStrLen(key, "$..a")).containsExactly(3, 5, null); + + assertThat(json.jsonStrLen("str", null)).containsExactly(5); + assertThatThrownBy(() -> json.jsonStrLen("empty", null)).hasMessageContaining("string"); + assertThat(json.jsonStrLen(key, "$..missing")).isEmpty(); + } + + @Test + void toggle() { + JsonObject test = JsonObject.of("a", true, "nested", JsonObject.of("a", false), "nested2", JsonObject.of("a", "true")); + json.jsonSet(key, test); + json.jsonSet("empty", new JsonObject()); + json.jsonSet("bool", false); + assertThat(json.jsonToggle(key, "$..a")).containsExactly(false, true, null); + JsonObject object = json.jsonGet(key); + assertThat(object.getBoolean("a")).isFalse(); + assertThat(object.getJsonObject("nested").getBoolean("a")).isTrue(); + assertThat(object.getJsonObject("nested2").getString("a")).isEqualTo("true"); + + assertThat(json.jsonToggle("bool", "$")).containsExactly(true); + assertThat(json.jsonToggle("empty", "$")).hasSize(1).allSatisfy(b -> assertThat(b).isNull()); + assertThat(json.jsonToggle(key, "$..missing")).isEmpty(); + } + + @Test + void type() { + JsonObject test = JsonObject.of("a", 2, "nested", JsonObject.of("a", true), "foo", "bar", "arr", JsonArray.of(1, 2, 3), + "next", JsonObject.of("a", 23.5)); + json.jsonSet(key, test); + json.jsonSet("empty", new JsonObject()); + assertThat(json.jsonType(key, "$..foo")).containsExactly("string"); + assertThat(json.jsonType(key, "$..a")).containsExactly("integer", "boolean", "number"); + assertThat(json.jsonType(key, "$..missing")).isEmpty(); + assertThat(json.jsonType(key, "$.arr")).containsExactly("array"); + assertThat(json.jsonType(key, "$.arr[0]")).containsExactly("integer"); + assertThat(json.jsonType("empty", "$")).containsExactly("object"); + assertThat(json.jsonType("empty", "$.a")).isEmpty(); + + } + +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Person.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Person.java index c944730c8ebb8..4db35e13e7125 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Person.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Person.java @@ -21,7 +21,7 @@ public Person(String firstname, String lastname) { } public Person() { - // USed by the mapper. + // Used by the mapper. } @Override diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisCommandCondition.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisCommandCondition.java new file mode 100644 index 0000000000000..4bbb799ccf909 --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisCommandCondition.java @@ -0,0 +1,37 @@ +package io.quarkus.redis.datasource; + +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.AnnotationUtils; + +class RedisCommandCondition implements ExecutionCondition { + + private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@RequiresCommand is not present"); + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional optional = AnnotationUtils.findAnnotation(context.getElement(), + RequiresCommand.class); + + if (optional.isPresent()) { + String[] cmd = optional.get().value(); + List commands = DatasourceTestBase.getAvailableCommands(); + + for (String c : cmd) { + if (!commands.contains(c.toLowerCase())) { + return disabled("Disabled, Redis command " + c + " not available."); + } + } + return enabled("Redis commands " + String.join(", ", cmd) + " are available"); + } + + return ENABLED_BY_DEFAULT; + } +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresCommand.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresCommand.java new file mode 100644 index 0000000000000..1ba03eb39eb26 --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresCommand.java @@ -0,0 +1,14 @@ +package io.quarkus.redis.datasource; + +import java.lang.annotation.*; + +import org.junit.jupiter.api.extension.ExtendWith; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@ExtendWith(RedisCommandCondition.class) +@interface RequiresCommand { + + String[] value(); +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalJsonCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalJsonCommandsTest.java new file mode 100644 index 0000000000000..9944b1764c83e --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalJsonCommandsTest.java @@ -0,0 +1,125 @@ + +package io.quarkus.redis.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.redis.datasource.json.ReactiveTransactionalJsonCommands; +import io.quarkus.redis.datasource.json.TransactionalJsonCommands; +import io.quarkus.redis.datasource.transactions.TransactionResult; +import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; +import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class TransactionalJsonCommandsTest extends DatasourceTestBase { + + private RedisDataSource blocking; + private ReactiveRedisDataSource reactive; + + Person person = new Person("luke", "skywalker"); + Person person2 = new Person("leia", "skywalker"); + + @BeforeEach + void initialize() { + blocking = new BlockingRedisDataSourceImpl(vertx, redis, api, Duration.ofSeconds(60)); + reactive = new ReactiveRedisDataSourceImpl(vertx, redis, api); + } + + @AfterEach + public void clear() { + blocking.flushall(); + } + + @SuppressWarnings("unchecked") + @Test + public void setBlocking() { + + TransactionResult result = blocking.withTransaction(tx -> { + TransactionalJsonCommands json = tx.json(); + assertThat(json.getDataSource()).isEqualTo(tx); + json.jsonSet(key, "$", person); // 0 + json.jsonSet(key, "$.sister", person2); // 1 + json.jsonSet(key, "$.a", JsonArray.of(1, 2, 3)); // 2 + + json.jsonArrPop(key, Integer.class, "$.a", -1); // 3 -> [3] + json.jsonArrLen(key, "$.a"); // 4 -> [2] + json.jsonClear(key, "$.a"); // 5 -> 1 + + json.jsonStrLen(key, "$.sister.lastname"); // 6 -> [9] + json.jsonStrAppend(key, "$.sister.lastname", "!"); // 7 -> 10 + json.jsonStrLen(key, "$.sister.lastname"); // 8 -> 10 + + json.jsonGet(key); // 9 {...} + + json.jsonSet("sister", "$", new JsonObject(Json.encode(person2))); + json.jsonGet("sister", Person.class); + }); + assertThat(result.size()).isEqualTo(12); + assertThat(result.discarded()).isFalse(); + + assertThat((Void) result.get(0)).isNull(); + assertThat((Void) result.get(1)).isNull(); + assertThat((Void) result.get(2)).isNull(); + assertThat((List) result.get(3)).containsExactly(3); + assertThat((List) result.get(4)).containsExactly(2); + assertThat((int) result.get(5)).isEqualTo(1); + assertThat((List) result.get(6)).containsExactly(person2.lastname.length()); + assertThat((List) result.get(7)).containsExactly(person2.lastname.length() + 1); + assertThat((List) result.get(8)).containsExactly(person2.lastname.length() + 1); + JsonObject actual = result.get(9); + assertThat(actual.getString("firstname")).isEqualTo(person.firstname); + assertThat(actual.getString("lastname")).isEqualTo(person.lastname); + assertThat(actual.getJsonObject("sister").getString("lastname")).isEqualTo(person2.lastname + "!"); + assertThat(actual.getJsonArray("a")).isEmpty();// cleared + assertThat((Void) result.get(10)).isNull(); + assertThat((Person) result.get(11)).isEqualTo(person2); + } + + @Test + public void setReactive() { + TransactionResult result = reactive.withTransaction(tx -> { + ReactiveTransactionalJsonCommands json = tx.json(); + assertThat(json.getDataSource()).isEqualTo(tx); + return json.jsonSet(key, "$", person) // 0 + .chain(() -> json.jsonSet(key, "$.sister", person2)) // 1 + .chain(() -> json.jsonSet(key, "$.a", JsonArray.of(1, 2, 3))) // 2 + .chain(() -> json.jsonArrPop(key, Integer.class, "$.a", -1)) // 3 -> [3] + .chain(() -> json.jsonArrLen(key, "$.a")) // 4 -> [2] + .chain(() -> json.jsonClear(key, "$.a")) // 5 -> 1 + .chain(() -> json.jsonStrLen(key, "$.sister.lastname")) // 6 -> [9] + .chain(() -> json.jsonStrAppend(key, "$.sister.lastname", "!")) // 7 -> 10 + .chain(() -> json.jsonStrLen(key, "$.sister.lastname")) // 8 -> 10 + .chain(() -> json.jsonGet(key)) // 9 {...} + .chain(() -> json.jsonSet("sister", "$", new JsonObject(Json.encode(person2)))) + .chain(() -> json.jsonGet("sister", Person.class)); + }).await().atMost(Duration.ofSeconds(5)); + assertThat(result.size()).isEqualTo(12); + assertThat(result.discarded()).isFalse(); + + assertThat((Void) result.get(0)).isNull(); + assertThat((Void) result.get(1)).isNull(); + assertThat((Void) result.get(2)).isNull(); + assertThat((List) result.get(3)).containsExactly(3); + assertThat((List) result.get(4)).containsExactly(2); + assertThat((int) result.get(5)).isEqualTo(1); + assertThat((List) result.get(6)).containsExactly(person2.lastname.length()); + assertThat((List) result.get(7)).containsExactly(person2.lastname.length() + 1); + assertThat((List) result.get(8)).containsExactly(person2.lastname.length() + 1); + JsonObject actual = result.get(9); + assertThat(actual.getString("firstname")).isEqualTo(person.firstname); + assertThat(actual.getString("lastname")).isEqualTo(person.lastname); + assertThat(actual.getJsonObject("sister").getString("lastname")).isEqualTo(person2.lastname + "!"); + assertThat(actual.getJsonArray("a")).isEmpty();// cleared + assertThat((Void) result.get(10)).isNull(); + assertThat((Person) result.get(11)).isEqualTo(person2); + } + +}