diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index f5ad60991c..a5acd8e2d2 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -190,6 +190,7 @@ enum RequestType { BitOp = 148; HStrlen = 149; FunctionLoad = 150; + FunctionList = 151; LMPop = 155; ExpireTime = 156; PExpireTime = 157; diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 7497cce94d..ed86f72d51 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -160,6 +160,7 @@ pub enum RequestType { BitOp = 148, HStrlen = 149, FunctionLoad = 150, + FunctionList = 151, LMPop = 155, ExpireTime = 156, PExpireTime = 157, @@ -338,6 +339,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GetBit => RequestType::GetBit, ProtobufRequestType::ZInter => RequestType::ZInter, ProtobufRequestType::FunctionLoad => RequestType::FunctionLoad, + ProtobufRequestType::FunctionList => RequestType::FunctionList, ProtobufRequestType::BitPos => RequestType::BitPos, ProtobufRequestType::BitOp => RequestType::BitOp, ProtobufRequestType::HStrlen => RequestType::HStrlen, @@ -513,6 +515,7 @@ impl RequestType { RequestType::GetBit => Some(cmd("GETBIT")), RequestType::ZInter => Some(cmd("ZINTER")), RequestType::FunctionLoad => Some(get_two_word_command("FUNCTION", "LOAD")), + RequestType::FunctionList => Some(get_two_word_command("FUNCTION", "LIST")), RequestType::BitPos => Some(cmd("BITPOS")), RequestType::BitOp => Some(cmd("BITOP")), RequestType::HStrlen => Some(cmd("HSTRLEN")), diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 1de4f2d2be..6392b8265d 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -378,6 +378,18 @@ protected Set handleSetResponse(Response response) throws RedisException return handleRedisResponse(Set.class, false, response); } + /** Process a FUNCTION LIST standalone response. */ + @SuppressWarnings("unchecked") + protected Map[] handleFunctionListResponse(Object[] response) { + Map[] data = castArray(response, Map.class); + for (Map libraryInfo : data) { + Object[] functions = (Object[]) libraryInfo.get("functions"); + var functionInfo = castArray(functions, Map.class); + libraryInfo.put("functions", functionInfo); + } + return data; + } + @Override public CompletableFuture del(@NonNull String[] keys) { return commandManager.submitNewCommand(Del, keys, this::handleLongResponse); diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index e090dcd2e4..8a77bbc9e6 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; import static glide.utils.ArrayTransformUtils.castArray; import static glide.utils.ArrayTransformUtils.concatenateArrays; @@ -14,6 +16,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; @@ -203,4 +206,23 @@ public CompletableFuture move(@NonNull String key, long dbIndex) { return commandManager.submitNewCommand( Move, new String[] {key, Long.toString(dbIndex)}, this::handleBooleanResponse); } + + @Override + public CompletableFuture[]> functionList(boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode ? new String[] {WITH_CODE_REDIS_API} : new String[0], + response -> handleFunctionListResponse(handleArrayResponse(response))); + } + + @Override + public CompletableFuture[]> functionList( + @NonNull String libNamePattern, boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode + ? new String[] {LIBRARY_NAME_REDIS_API, libNamePattern, WITH_CODE_REDIS_API} + : new String[] {LIBRARY_NAME_REDIS_API, libNamePattern}, + response -> handleFunctionListResponse(handleArrayResponse(response))); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index f3eef93797..f758ecd8eb 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -2,6 +2,8 @@ package glide.api; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; import static glide.utils.ArrayTransformUtils.castArray; import static glide.utils.ArrayTransformUtils.castMapOfArrays; @@ -16,6 +18,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; @@ -37,6 +40,7 @@ import glide.managers.CommandManager; import glide.managers.ConnectionManager; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -432,4 +436,62 @@ public CompletableFuture functionLoad( return commandManager.submitNewCommand( FunctionLoad, arguments, route, this::handleStringResponse); } + + /** Process a FUNCTION LIST cluster response. */ + protected ClusterValue[]> handleFunctionListResponse( + Response response, Route route) { + if (route instanceof SingleNodeRoute) { + Map[] data = handleFunctionListResponse(handleArrayResponse(response)); + return ClusterValue.ofSingleValue(data); + } else { + // each `Object` is a `Map[]` actually + Map info = handleMapResponse(response); + Map[]> data = new HashMap<>(); + for (var nodeInfo : info.entrySet()) { + data.put(nodeInfo.getKey(), handleFunctionListResponse((Object[]) nodeInfo.getValue())); + } + return ClusterValue.ofMultiValue(data); + } + } + + @Override + public CompletableFuture[]> functionList(boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode ? new String[] {WITH_CODE_REDIS_API} : new String[0], + response -> handleFunctionListResponse(handleArrayResponse(response))); + } + + @Override + public CompletableFuture[]> functionList( + @NonNull String libNamePattern, boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode + ? new String[] {LIBRARY_NAME_REDIS_API, libNamePattern, WITH_CODE_REDIS_API} + : new String[] {LIBRARY_NAME_REDIS_API, libNamePattern}, + response -> handleFunctionListResponse(handleArrayResponse(response))); + } + + @Override + public CompletableFuture[]>> functionList( + boolean withCode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionList, + withCode ? new String[] {WITH_CODE_REDIS_API} : new String[0], + route, + response -> handleFunctionListResponse(response, route)); + } + + @Override + public CompletableFuture[]>> functionList( + @NonNull String libNamePattern, boolean withCode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionList, + withCode + ? new String[] {LIBRARY_NAME_REDIS_API, libNamePattern, WITH_CODE_REDIS_API} + : new String[] {LIBRARY_NAME_REDIS_API, libNamePattern}, + route, + response -> handleFunctionListResponse(response, route)); + } } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java index 0df7077f4f..baa38ab057 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -1,7 +1,9 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.ClusterValue; import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import java.util.Map; import java.util.concurrent.CompletableFuture; /** @@ -51,4 +53,118 @@ public interface ScriptingAndFunctionsClusterCommands { * } */ CompletableFuture functionLoad(String libraryCode, boolean replace, Route route); + + /** + * Returns information about the functions and libraries.
+ * The command will be routed to a random node. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList(true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(boolean withCode); + + /** + * Returns information about the functions and libraries.
+ * The command will be routed to a random node. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList("myLib?_backup", true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(String libNamePattern, boolean withCode); + + /** + * Returns information about the functions and libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * ClusterValue[]> response = client.functionList(true, ALL_NODES).get();
+     * for (String node : response.getMultiValue().keySet()) {
+     *   for (Map libraryInfo : response.getMultiValue().get(node)) {
+     *     System.out.printf("Node '%s' has library '%s' which runs on %s engine%n",
+     *         node, libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture[]>> functionList( + boolean withCode, Route route); + + /** + * Returns information about the functions and libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * ClusterValue[]> response = client.functionList("myLib?_backup", true, ALL_NODES).get();
+     * for (String node : response.getMultiValue().keySet()) {
+     *   for (Map libraryInfo : response.getMultiValue().get(node)) {
+     *     System.out.printf("Node '%s' has library '%s' which runs on %s engine%n",
+     *         node, libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture[]>> functionList( + String libNamePattern, boolean withCode, Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java index 3da638d998..baffdf2e21 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import java.util.Map; import java.util.concurrent.CompletableFuture; /** @@ -29,4 +30,55 @@ public interface ScriptingAndFunctionsCommands { * } */ CompletableFuture functionLoad(String libraryCode, boolean replace); + + /** + * Returns information about the functions and libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList(true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String. join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(boolean withCode); + + /** + * Returns information about the functions and libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList("myLib?_backup", true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String. join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(String libNamePattern, boolean withCode); } diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 0d7c361aa7..f0e9653785 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -11,6 +11,8 @@ import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; import static glide.utils.ArrayTransformUtils.concatenateArrays; import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; @@ -45,6 +47,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; import static redis_request.RedisRequestOuterClass.RequestType.GeoDist; @@ -3640,6 +3643,38 @@ public T functionLoad(@NonNull String libraryCode, boolean replace) { return getThis(); } + /** + * Returns information about the functions and libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Command Response - Info about all libraries and their functions. + */ + public T functionList(boolean withCode) { + ArgsArray commandArgs = withCode ? buildArgs(WITH_CODE_REDIS_API) : buildArgs(); + protobufTransaction.addCommands(buildCommand(FunctionList, commandArgs)); + return getThis(); + } + + /** + * Returns information about the functions and libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Command Response - Info about queried libraries and their functions. + */ + public T functionList(@NonNull String libNamePattern, boolean withCode) { + ArgsArray commandArgs = + withCode + ? buildArgs(LIBRARY_NAME_REDIS_API, libNamePattern, WITH_CODE_REDIS_API) + : buildArgs(LIBRARY_NAME_REDIS_API, libNamePattern); + protobufTransaction.addCommands(buildCommand(FunctionList, commandArgs)); + return getThis(); + } + /** * Sets or clears the bit at offset in the string value stored at key. * The offset is a zero-based index, with 0 being the first element of diff --git a/java/client/src/main/java/glide/api/models/commands/function/FunctionListOptions.java b/java/client/src/main/java/glide/api/models/commands/function/FunctionListOptions.java new file mode 100644 index 0000000000..6cac32fbac --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/function/FunctionListOptions.java @@ -0,0 +1,19 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.function; + +import glide.api.commands.ScriptingAndFunctionsClusterCommands; +import glide.api.commands.ScriptingAndFunctionsCommands; + +/** + * Option for {@link ScriptingAndFunctionsCommands#functionList()} and {@link + * ScriptingAndFunctionsClusterCommands#functionList()} command. + * + * @see redis.io + */ +public class FunctionListOptions { + /** Causes the server to include the libraries source implementation in the reply. */ + public static final String WITH_CODE_REDIS_API = "WITHCODE"; + + /** REDIS API keyword followed by library name pattern. */ + public static final String LIBRARY_NAME_REDIS_API = "LIBRARYNAME"; +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 285cf142c4..008e7bd1dd 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -20,6 +20,8 @@ import static glide.api.models.commands.bitmap.BitFieldOptions.INCRBY_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.OVERFLOW_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.SET_COMMAND_STRING; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.geospatial.GeoAddOptions.CHANGED_REDIS_API; import static glide.api.models.commands.stream.StreamAddOptions.NO_MAKE_STREAM_REDIS_API; import static glide.api.models.commands.stream.StreamRange.MAXIMUM_RANGE_REDIS_API; @@ -72,6 +74,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; import static redis_request.RedisRequestOuterClass.RequestType.GeoDist; @@ -4917,6 +4920,53 @@ public void functionLoad_with_replace_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionList_returns_success() { + // setup + String[] args = new String[0]; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(false); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_with_pattern_returns_success() { + // setup + String pattern = "*"; + String[] args = new String[] {LIBRARY_NAME_REDIS_API, pattern, WITH_CODE_REDIS_API}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(pattern, true); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void bitcount_returns_success() { diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 0af49287f8..1a32dd7669 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -4,6 +4,8 @@ import static glide.api.BaseClient.OK; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.models.commands.FlushMode.SYNC; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; @@ -23,6 +25,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; @@ -1169,4 +1172,102 @@ public void functionLoad_with_replace_with_route_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void functionList_returns_success() { + // setup + String[] args = new String[0]; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(false); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_with_pattern_returns_success() { + // setup + String pattern = "*"; + String[] args = new String[] {LIBRARY_NAME_REDIS_API, pattern, WITH_CODE_REDIS_API}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(pattern, true); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_with_route_returns_success() { + // setup + String[] args = new String[] {WITH_CODE_REDIS_API}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]>> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.[]>>submitNewCommand( + eq(FunctionList), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]>> response = + service.functionList(true, RANDOM); + ClusterValue[]> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload.getSingleValue()); + } + + @SneakyThrows + @Test + public void functionList_with_pattern_and_route_returns_success() { + // setup + String pattern = "*"; + String[] args = new String[] {LIBRARY_NAME_REDIS_API, pattern}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]>> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.[]>>submitNewCommand( + eq(FunctionList), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]>> response = + service.functionList(pattern, false, RANDOM); + ClusterValue[]> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload.getSingleValue()); + } } diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index e1c7fdb269..e887b2e06a 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -20,6 +20,8 @@ import static glide.api.models.commands.WeightAggregateOptions.AGGREGATE_REDIS_API; import static glide.api.models.commands.WeightAggregateOptions.WEIGHTS_REDIS_API; import static glide.api.models.commands.ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.geospatial.GeoAddOptions.CHANGED_REDIS_API; import static glide.api.models.commands.stream.StreamRange.MAXIMUM_RANGE_REDIS_API; import static glide.api.models.commands.stream.StreamRange.MINIMUM_RANGE_REDIS_API; @@ -55,6 +57,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; import static redis_request.RedisRequestOuterClass.RequestType.GeoDist; @@ -839,6 +842,10 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), results.add(Pair.of(FunctionLoad, buildArgs("pewpew"))); results.add(Pair.of(FunctionLoad, buildArgs("REPLACE", "ololo"))); + transaction.functionList(true).functionList("*", false); + results.add(Pair.of(FunctionList, buildArgs(WITH_CODE_REDIS_API))); + results.add(Pair.of(FunctionList, buildArgs(LIBRARY_NAME_REDIS_API, "*"))); + transaction.geodist("key", "Place", "Place2"); results.add(Pair.of(GeoDist, buildArgs("key", "Place", "Place2"))); transaction.geodist("key", "Place", "Place2", GeoUnit.KILOMETERS); diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 4f5f910b0c..3b77497fcd 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import lombok.experimental.UtilityClass; @@ -123,4 +124,43 @@ public static void assertDeepEquals(Object expected, Object actual) { assertEquals(expected, actual); } } + + /** + * Validate whether `FUNCTION LIST` response contains required info. + * + * @param response The response from redis. + * @param libName Expected library name. + * @param functionDescriptions Expected function descriptions. Key - function name, value - + * description. + * @param functionFlags Expected function flags. Key - function name, value - flags set. + * @param libCode Expected library to check if given. + */ + @SuppressWarnings("unchecked") + public static void checkFunctionListResponse( + Map[] response, + String libName, + Map functionDescriptions, + Map> functionFlags, + Optional libCode) { + assertTrue(response.length > 0); + boolean hasLib = false; + for (var lib : response) { + hasLib = lib.containsValue(libName); + if (hasLib) { + var functions = (Object[]) lib.get("functions"); + assertEquals(functionDescriptions.size(), functions.length); + for (var functionInfo : functions) { + var function = (Map) functionInfo; + var functionName = (String) function.get("name"); + assertEquals(functionDescriptions.get(functionName), function.get("description")); + assertEquals(functionFlags.get(functionName), function.get("flags")); + } + if (libCode.isPresent()) { + assertEquals(libCode.get(), lib.get("library_code")); + } + break; + } + } + assertTrue(hasLib); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 6acdaf5215..b847d39635 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -35,6 +35,7 @@ import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamRange.IdBound; import glide.api.models.commands.stream.StreamTrimOptions.MinId; +import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -674,12 +675,47 @@ private static Object[] scriptingAndFunctionsCommands(BaseTransaction transac final String code = "#!lua name=mylib1T \n" + " redis.register_function('myfunc1T', function(keys, args) return args[1] end)"; + var expectedFuncData = + new HashMap() { + { + put("name", "myfunc1T"); + put("description", null); + put("flags", Set.of()); + } + }; + + var expectedLibData = + new Map[] { + Map.of( + "library_name", + "mylib1T", + "engine", + "LUA", + "functions", + new Object[] {expectedFuncData}, + "library_code", + code) + }; - transaction.functionLoad(code, false).functionLoad(code, true); + transaction + .customCommand(new String[] {"function", "flush", "sync"}) + .functionList(false) + .functionList(true) + .functionLoad(code, false) + .functionLoad(code, true) + .functionList("otherLib", false) + .functionList("mylib1T", true) + .customCommand(new String[] {"function", "flush", "sync"}); return new Object[] { + OK, // customCommand("function", "flush", "sync") + new Map[0], // functionList(false) + new Map[0], // functionList(true) "mylib1T", // functionLoad(code, false) "mylib1T", // functionLoad(code, true) + new Map[0], // functionList("otherLib", false) + expectedLibData, // functionList("mylib1T", true) + OK, // customCommand("function", "flush", "sync") }; } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 0998a7a3bd..4d2779b8bc 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -3,6 +3,7 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestUtilities.checkFunctionListResponse; import static glide.TestUtilities.getFirstEntryFromMultiValue; import static glide.TestUtilities.getValueFromInfo; import static glide.TestUtilities.parseInfoResponseToMap; @@ -49,8 +50,11 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -779,53 +783,192 @@ public void flushall() { } @SneakyThrows - @ParameterizedTest + @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") @ValueSource(booleans = {true, false}) - public void functionLoad(boolean withRoute) { + public void functionLoad_and_functionList(boolean singleNodeRoute) { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); - String libName = "mylib1C" + withRoute; + + // TODO use FUNCTION FLUSH + assertEquals( + OK, + clusterClient + .customCommand(new String[] {"FUNCTION", "FLUSH", "SYNC"}) + .get() + .getSingleValue()); + + String libName = "mylib1c_" + singleNodeRoute; + String funcName = "myfunc1c_" + singleNodeRoute; String code = "#!lua name=" + libName - + " \n redis.register_function('myfunc1c" - + withRoute - + "', function(keys, args) return args[1] end)"; - Route route = new SlotKeyRoute("1", PRIMARY); - - var promise = - withRoute - ? clusterClient.functionLoad(code, false, route) - : clusterClient.functionLoad(code, false); - assertEquals(libName, promise.get()); + + " \n redis.register_function('" + + funcName + + "', function(keys, args) return args[1] end)"; // function returns first argument + Route route = singleNodeRoute ? new SlotKeyRoute("1", PRIMARY) : ALL_PRIMARIES; + + assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); // TODO test function with FCALL when fixed in redis-rs and implemented - // TODO test with FUNCTION LIST + + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of()); + } + }; + + var response = clusterClient.functionList(false, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } + } + + response = clusterClient.functionList(true, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + } + } // re-load library without overwriting - promise = - withRoute - ? clusterClient.functionLoad(code, false, route) - : clusterClient.functionLoad(code, false); - var executionException = assertThrows(ExecutionException.class, promise::get); + var executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionLoad(code, false, route).get()); assertInstanceOf(RequestException.class, executionException.getCause()); assertTrue( executionException.getMessage().contains("Library '" + libName + "' already exists")); // re-load library with overwriting - var promise2 = - withRoute - ? clusterClient.functionLoad(code, true, route) - : clusterClient.functionLoad(code, true); - assertEquals(libName, promise2.get()); + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + String newFuncName = "myfunc2c_" + singleNodeRoute; String newCode = code - + "\n redis.register_function('myfunc2c" - + withRoute - + "', function(keys, args) return #args end)"; - promise2 = - withRoute - ? clusterClient.functionLoad(newCode, true, route) - : clusterClient.functionLoad(newCode, true); - assertEquals(libName, promise2.get()); + + "\n redis.register_function('" + + newFuncName + + "', function(keys, args) return #args end)"; // function returns argument count + + assertEquals(libName, clusterClient.functionLoad(newCode, true, route).get()); + + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of()); + + response = clusterClient.functionList(false, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } + } + + response = clusterClient.functionList(true, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + } + } + // TODO test with FCALL + + // TODO FUNCTION FLUSH at the end + } + + @SneakyThrows + @Test + public void functionLoad_and_functionList_without_route() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + // TODO use FUNCTION FLUSH + assertEquals( + OK, + clusterClient + .customCommand(new String[] {"FUNCTION", "FLUSH", "SYNC"}) + .get() + .getSingleValue()); + + String libName = "mylib1c"; + String funcName = "myfunc1c"; + String code = + "#!lua name=" + + libName + + " \n redis.register_function('" + + funcName + + "', function(keys, args) return args[1] end)"; // function returns first argument + + assertEquals(libName, clusterClient.functionLoad(code, false).get()); + // TODO test function with FCALL when fixed in redis-rs and implemented + + var flist = clusterClient.functionList(false).get(); + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of()); + } + }; + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = clusterClient.functionList(true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + + // re-load library without overwriting + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.functionLoad(code, false).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("Library '" + libName + "' already exists")); + + // re-load library with overwriting + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + String newFuncName = "myfunc2c"; + String newCode = + code + + "\n redis.register_function('" + + newFuncName + + "', function(keys, args) return #args end)"; // function returns argument count + assertEquals(libName, clusterClient.functionLoad(newCode, true).get()); + + flist = clusterClient.functionList(libName, false).get(); + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of()); + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = clusterClient.functionList(libName, true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + + // TODO test with FCALL + + // TODO FUNCTION FLUSH at the end } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 19fbf1e107..adafed5406 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -3,6 +3,7 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; +import static glide.TestUtilities.checkFunctionListResponse; import static glide.TestUtilities.getValueFromInfo; import static glide.TestUtilities.parseInfoResponseToMap; import static glide.api.BaseClient.OK; @@ -30,7 +31,10 @@ import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; @@ -369,16 +373,41 @@ public void flushall() { @SneakyThrows @Test - public void functionLoad() { + public void functionLoad_and_functionList() { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); - String libName = "mylib1C"; + + // TODO use FUNCTION FLUSH + assertEquals(OK, regularClient.customCommand(new String[] {"FUNCTION", "FLUSH", "SYNC"}).get()); + + String libName = "mylib1c"; + String funcName = "myfunc1c"; String code = "#!lua name=" + libName - + " \n redis.register_function('myfunc1c', function(keys, args) return args[1] end)"; + + " \n redis.register_function('" + + funcName + + "', function(keys, args) return args[1] end)"; // function returns first argument assertEquals(libName, regularClient.functionLoad(code, false).get()); // TODO test function with FCALL when fixed in redis-rs and implemented - // TODO test with FUNCTION LIST + + var flist = regularClient.functionList(false).get(); + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of()); + } + }; + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = regularClient.functionList(true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); // re-load library without overwriting var executionException = @@ -389,9 +418,24 @@ public void functionLoad() { // re-load library with overwriting assertEquals(libName, regularClient.functionLoad(code, true).get()); + String newFuncName = "myfunc2c"; String newCode = - code + "\n redis.register_function('myfunc2c', function(keys, args) return #args end)"; + code + + "\n redis.register_function('" + + newFuncName + + "', function(keys, args) return #args end)"; // function returns argument count assertEquals(libName, regularClient.functionLoad(newCode, true).get()); + + flist = regularClient.functionList(libName, false).get(); + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of()); + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = regularClient.functionList(libName, true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + // TODO test with FCALL + // TODO FUNCTION FLUSH at the end } }