From b9de0e3ea7c06bb86ad43179ef1045925fd28859 Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Mon, 11 Sep 2023 23:58:11 -0700 Subject: [PATCH] Feature: Triggers and functions commands --- .../redis/clients/jedis/BuilderFactory.java | 9 + .../redis/clients/jedis/CommandObjects.java | 52 ++ .../redis/clients/jedis/UnifiedJedis.java | 42 ++ .../jedis/commands/RedisModuleCommands.java | 4 +- .../jedis/gears/RedisGearsCommands.java | 15 + .../jedis/gears/RedisGearsProtocol.java | 44 ++ .../jedis/gears/TFunctionListParams.java | 50 ++ .../jedis/gears/TFunctionLoadParams.java | 36 ++ .../jedis/gears/resps/FunctionInfo.java | 97 ++++ .../jedis/gears/resps/FunctionStreamInfo.java | 88 +++ .../jedis/gears/resps/GearsLibraryInfo.java | 172 ++++++ .../jedis/gears/resps/StreamTriggerInfo.java | 145 +++++ .../jedis/gears/resps/TriggerInfo.java | 162 ++++++ .../jedis/modules/gears/GearsTest.java | 538 ++++++++++++++++++ .../resources/functions/keyspaceTriggers.js | 12 + src/test/resources/functions/pingpong.js | 7 + .../resources/functions/streamTriggers.js | 14 + src/test/resources/functions/withConfig.js | 16 + src/test/resources/functions/withFlags.js | 9 + .../resources/functions/workingWIthHashes.js | 8 + 20 files changed, 1519 insertions(+), 1 deletion(-) create mode 100644 src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java create mode 100644 src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java create mode 100644 src/main/java/redis/clients/jedis/gears/TFunctionListParams.java create mode 100644 src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java create mode 100644 src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java create mode 100644 src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java create mode 100644 src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java create mode 100644 src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java create mode 100644 src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java create mode 100644 src/test/java/redis/clients/jedis/modules/gears/GearsTest.java create mode 100644 src/test/resources/functions/keyspaceTriggers.js create mode 100644 src/test/resources/functions/pingpong.js create mode 100644 src/test/resources/functions/streamTriggers.js create mode 100644 src/test/resources/functions/withConfig.js create mode 100644 src/test/resources/functions/withFlags.js create mode 100644 src/test/resources/functions/workingWIthHashes.js diff --git a/src/main/java/redis/clients/jedis/BuilderFactory.java b/src/main/java/redis/clients/jedis/BuilderFactory.java index 46ce255c354..379e94445d0 100644 --- a/src/main/java/redis/clients/jedis/BuilderFactory.java +++ b/src/main/java/redis/clients/jedis/BuilderFactory.java @@ -5,6 +5,7 @@ import java.util.stream.Collectors; import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.gears.resps.GearsLibraryInfo; import redis.clients.jedis.resps.*; import redis.clients.jedis.resps.LCSMatchResult.MatchedPosition; import redis.clients.jedis.resps.LCSMatchResult.Position; @@ -1730,6 +1731,14 @@ public List build(Object data) { } }; + public static final Builder> GEARS_LIBRARY_LIST = new Builder>() { + @Override + public List build(Object data) { + List list = (List) data; + return list.stream().map(o -> GearsLibraryInfo.LIBRARY_BUILDER.build(o)).collect(Collectors.toList()); + } + }; + public static final Builder>> STRING_LIST_LIST = new Builder>>() { @Override @SuppressWarnings("unchecked") diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java index d3d77db3b45..cfccfb536c5 100644 --- a/src/main/java/redis/clients/jedis/CommandObjects.java +++ b/src/main/java/redis/clients/jedis/CommandObjects.java @@ -2,6 +2,8 @@ import static redis.clients.jedis.Protocol.Command.*; import static redis.clients.jedis.Protocol.Keyword.*; +import static redis.clients.jedis.gears.RedisGearsProtocol.GearsCommand.TFCALL; +import static redis.clients.jedis.gears.RedisGearsProtocol.GearsCommand.TFCALLASYNC; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -15,6 +17,11 @@ import redis.clients.jedis.bloom.*; import redis.clients.jedis.bloom.RedisBloomProtocol.*; import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.gears.RedisGearsProtocol.GearsKeyword; +import redis.clients.jedis.gears.RedisGearsProtocol.GearsCommand; +import redis.clients.jedis.gears.TFunctionListParams; +import redis.clients.jedis.gears.TFunctionLoadParams; +import redis.clients.jedis.gears.resps.GearsLibraryInfo; import redis.clients.jedis.graph.GraphProtocol.*; import redis.clients.jedis.json.*; import redis.clients.jedis.json.JsonProtocol.JsonCommand; @@ -4163,6 +4170,51 @@ public final CommandObject> graphConfigGet(String configName } // RedisGraph commands + // RedisGears commands + + public final CommandObject tFunctionLoad(String libraryCode, TFunctionLoadParams params) { + CommandArguments args = commandArguments(GearsCommand.TFUNCTION); + args.add(GearsKeyword.LOAD.getValue()); + params.addParams(args); + args.add(libraryCode); + + return new CommandObject<>(args, BuilderFactory.STRING); + } + + public final CommandObject tFunctionDelete(String libraryName) { + CommandArguments args = commandArguments(GearsCommand.TFUNCTION); + args.add(GearsKeyword.DELETE.getValue()); + args.add(libraryName); + + return new CommandObject<>(args, BuilderFactory.STRING); + } + + public final CommandObject> tFunctionList(TFunctionListParams params) { + CommandArguments args = commandArguments(GearsCommand.TFUNCTION); + args.add(GearsKeyword.LIST.getValue()); + params.addParams(args); + + return new CommandObject<>(args, BuilderFactory.GEARS_LIBRARY_LIST); + } + + public final CommandObject tFunctionCall(String library, String function, List keys, List args) { + String[] keysArray = keys.toArray(new String[keys.size()]); + String[] argsArray = args.toArray(new String[args.size()]); + return new CommandObject<>(commandArguments(TFCALL).add(library+"."+function).add(keysArray.length) + .keys((Object[]) keysArray).addObjects((Object[]) argsArray), + BuilderFactory.ENCODED_OBJECT); + } + + public final CommandObject tFunctionCallAsync(String library, String function, List keys, List args) { + String[] keysArray = keys.toArray(new String[keys.size()]); + String[] argsArray = args.toArray(new String[args.size()]); + return new CommandObject<>(commandArguments(TFCALLASYNC).add(library+"."+function).add(keysArray.length) + .keys((Object[]) keysArray).addObjects((Object[]) argsArray), + BuilderFactory.ENCODED_OBJECT); + } + + // RedisGears commands + /** * Get the instance for JsonObjectMapper if not null, otherwise a new instance reference with * default implementation will be created and returned. diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index acf6ad7dfd0..ff5cc83fc4f 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -19,6 +19,9 @@ import redis.clients.jedis.commands.RedisModuleCommands; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.executors.*; +import redis.clients.jedis.gears.TFunctionListParams; +import redis.clients.jedis.gears.TFunctionLoadParams; +import redis.clients.jedis.gears.resps.GearsLibraryInfo; import redis.clients.jedis.graph.GraphCommandObjects; import redis.clients.jedis.graph.ResultSet; import redis.clients.jedis.json.JsonSetParams; @@ -4807,4 +4810,43 @@ public void setJsonObjectMapper(JsonObjectMapper jsonObjectMapper) { public void setDefaultSearchDialect(int dialect) { this.commandObjects.setDefaultSearchDialect(dialect); } + + // RedisGears commands + + @Override + public String tFunctionLoad(String libraryCode) { + return executeCommand(commandObjects.tFunctionLoad(libraryCode, TFunctionLoadParams.loadParams())); + } + + @Override + public String tFunctionLoad(String libraryCode, TFunctionLoadParams params) { + return executeCommand(commandObjects.tFunctionLoad(libraryCode, params)); + } + + @Override + public String tFunctionDelete(String libraryName) { + return executeCommand(commandObjects.tFunctionDelete(libraryName)); + } + + @Override + public List tFunctionList() { + return executeCommand(commandObjects.tFunctionList(TFunctionListParams.listParams())); + } + + @Override + public List tFunctionList(TFunctionListParams params) { + return executeCommand(commandObjects.tFunctionList(params)); + } + + @Override + public Object tFunctionCall(String library, String function, List keys, List args) { + return executeCommand(commandObjects.tFunctionCall(library, function, keys, args)); + } + + @Override + public Object tFunctionCallAsync(String library, String function, List keys, List args) { + return executeCommand(commandObjects.tFunctionCallAsync(library, function, keys, args)); + } + + // RedisGears commands } diff --git a/src/main/java/redis/clients/jedis/commands/RedisModuleCommands.java b/src/main/java/redis/clients/jedis/commands/RedisModuleCommands.java index c4b785fc700..5e678387207 100644 --- a/src/main/java/redis/clients/jedis/commands/RedisModuleCommands.java +++ b/src/main/java/redis/clients/jedis/commands/RedisModuleCommands.java @@ -1,6 +1,7 @@ package redis.clients.jedis.commands; import redis.clients.jedis.bloom.commands.RedisBloomCommands; +import redis.clients.jedis.gears.RedisGearsCommands; import redis.clients.jedis.graph.RedisGraphCommands; import redis.clients.jedis.json.commands.RedisJsonCommands; import redis.clients.jedis.search.RediSearchCommands; @@ -11,6 +12,7 @@ public interface RedisModuleCommands extends RedisJsonCommands, RedisTimeSeriesCommands, RedisBloomCommands, - RedisGraphCommands { + RedisGraphCommands, + RedisGearsCommands { } diff --git a/src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java b/src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java new file mode 100644 index 00000000000..d8099a12e15 --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java @@ -0,0 +1,15 @@ +package redis.clients.jedis.gears; + +import redis.clients.jedis.gears.resps.GearsLibraryInfo; + +import java.util.List; + +public interface RedisGearsCommands { + String tFunctionLoad(String libraryCode); + String tFunctionLoad(String libraryCode, TFunctionLoadParams params); + List tFunctionList(TFunctionListParams params); + List tFunctionList(); + String tFunctionDelete(String libraryName); + Object tFunctionCall(String library, String function, List keys, List args); + Object tFunctionCallAsync(String library, String function, List keys, List args); +} diff --git a/src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java b/src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java new file mode 100644 index 00000000000..7a918d61525 --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java @@ -0,0 +1,44 @@ +package redis.clients.jedis.gears; + +import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.util.SafeEncoder; + +public class RedisGearsProtocol { + public enum GearsCommand implements ProtocolCommand { + TFUNCTION("TFUNCTION"), + TFCALL("TFCALL"), + TFCALLASYNC("TFCALLASYNC"); + + private final byte[] raw; + + GearsCommand(String alt) { + raw = SafeEncoder.encode(alt); + } + + @Override + public byte[] getRaw() { + return raw; + } + } + + public enum GearsKeyword { + CONFIG("CONFIG"), + REPLACE("REPLACE"), + LOAD("LOAD"), + DELETE("DELETE"), + LIST("LIST"), + WITHCODE("WITHCODE"), + LIBRARY("LIBRARY"), + VERBOSE("VERBOSE"); + + private final String value; + + GearsKeyword(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/src/main/java/redis/clients/jedis/gears/TFunctionListParams.java b/src/main/java/redis/clients/jedis/gears/TFunctionListParams.java new file mode 100644 index 00000000000..82b3b2d1286 --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/TFunctionListParams.java @@ -0,0 +1,50 @@ +package redis.clients.jedis.gears; + +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.gears.RedisGearsProtocol.GearsKeyword; +import redis.clients.jedis.params.IParams; + +import java.util.Collections; + +public class TFunctionListParams implements IParams { + private boolean withCode = false; + private int verbose; + private String libraryName; + + public static TFunctionListParams listParams() { + return new TFunctionListParams(); + } + + @Override + public void addParams(CommandArguments args) { + if (withCode) { + args.add(GearsKeyword.WITHCODE.getValue()); + } + + if (verbose > 0 && verbose < 4) { + args.add(String.join("", Collections.nCopies(verbose, "v"))); + } else if (verbose != 0) { // verbose == 0 is the default, so we don't need to throw an error + throw new IllegalArgumentException("verbose must be between 1 and 3"); + } + + if (libraryName != null) { + args.add(GearsKeyword.LIBRARY); + args.add(libraryName); + } + } + + public TFunctionListParams withCode() { + this.withCode = true; + return this; + } + + public TFunctionListParams verbose(int verbose) { + this.verbose = verbose; + return this; + } + + public TFunctionListParams library(String libraryName) { + this.libraryName = libraryName; + return this; + } +} diff --git a/src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java b/src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java new file mode 100644 index 00000000000..087069e1423 --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java @@ -0,0 +1,36 @@ +package redis.clients.jedis.gears; + +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.gears.RedisGearsProtocol.GearsKeyword; +import redis.clients.jedis.params.IParams; + +public class TFunctionLoadParams implements IParams { + private boolean replace = false; + private String config; + + public static TFunctionLoadParams loadParams() { + return new TFunctionLoadParams(); + } + + @Override + public void addParams(CommandArguments args) { + if (replace) { + args.add(GearsKeyword.REPLACE.getValue()); + } + + if (config != null && !config.isEmpty()) { + args.add(GearsKeyword.CONFIG.getValue()); + args.add(config); + } + } + + public TFunctionLoadParams replace() { + this.replace = true; + return this; + } + + public TFunctionLoadParams withConfig(String config) { + this.config = config; + return this; + } +} diff --git a/src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java b/src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java new file mode 100644 index 00000000000..ccb8785812b --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java @@ -0,0 +1,97 @@ +package redis.clients.jedis.gears.resps; + +import redis.clients.jedis.Builder; +import redis.clients.jedis.util.KeyValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static redis.clients.jedis.BuilderFactory.*; + +public class FunctionInfo { + private final String name; + private final String description; + private final boolean isAsync; + + private final List flags; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public boolean isAsync() { + return isAsync; + } + + public List getFlags() { + return flags; + } + + public FunctionInfo(String name, String description, boolean isAsync, List flags) { + this.name = name; + this.description = description; + this.isAsync = isAsync; + this.flags = flags; + } + + public static final Builder> FUNCTION_INFO_LIST = new Builder>() { + @Override + public List build(Object data) { + List dataAsList = (List) data; + if (!dataAsList.isEmpty()) { + boolean isListOfList = dataAsList.get(0).getClass().isAssignableFrom(ArrayList.class); + + if (isListOfList) { + if (((List>)data).get(0).get(0) instanceof KeyValue) { + List> dataAsKeyValues = (List>)data; + return dataAsKeyValues.stream().map(keyValues -> { + String name = null; + String description = null; + List flags = Collections.emptyList(); + boolean isAsync = false; + for (KeyValue kv : keyValues) { + switch (STRING.build(kv.getKey())) { + case "name": + name = STRING.build(kv.getValue()); + break; + case "description": + description = STRING.build(kv.getValue()); + break; + case "raw-arguments": + flags = STRING_LIST.build(kv.getValue()); + break; + case "is_async": + isAsync = BOOLEAN.build(kv.getValue()); + break; + } + } + return new FunctionInfo(name, description, isAsync, flags); + }).collect(Collectors.toList()); + } else { + return dataAsList.stream().map((pairObject) -> (List) pairObject) + .map((pairList) -> new FunctionInfo( // + STRING.build(pairList.get(7)), // name + STRING.build(pairList.get(1)), // description + BOOLEAN.build(pairList.get(5)), // is_async + STRING_LIST.build(pairList.get(3)) // flags + )).collect(Collectors.toList()); + } + } else { + return dataAsList.stream() // + .map(STRING::build) // + .map((name) -> new FunctionInfo(name, null, false, null)) // + .collect(Collectors.toList()); + } + } else { + return Collections.emptyList(); + } + } + }; +} + diff --git a/src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java b/src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java new file mode 100644 index 00000000000..f4b607d6a32 --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java @@ -0,0 +1,88 @@ +package redis.clients.jedis.gears.resps; + +import redis.clients.jedis.Builder; +import redis.clients.jedis.BuilderFactory; + +import java.util.List; +import java.util.stream.Collectors; + +public class FunctionStreamInfo { + private final String name; + private final String idToReadFrom; + private final String lastError; + private final long lastLag; + private final long lastProcessedTime; + private final long totalLag; + private final long totalProcessedTime; + private final long totalRecordProcessed; + private final List pendingIds; + + public String getName() { + return name; + } + + public String getIdToReadFrom() { + return idToReadFrom; + } + + public String getLastError() { + return lastError; + } + + public long getLastLag() { + return lastLag; + } + + public long getLastProcessedTime() { + return lastProcessedTime; + } + + public long getTotalLag() { + return totalLag; + } + + public long getTotalProcessedTime() { + return totalProcessedTime; + } + + public long getTotalRecordProcessed() { + return totalRecordProcessed; + } + + public List getPendingIds() { + return pendingIds; + } + + public FunctionStreamInfo(String name, String idToReadFrom, String lastError, + long lastProcessedTime, long lastLag, long totalLag, long totalProcessedTime, long totalRecordProcessed, + List pendingIds) { + this.name = name; + this.idToReadFrom = idToReadFrom; + this.lastError = lastError; + this.lastProcessedTime = lastProcessedTime; + this.lastLag = lastLag; + this.totalLag = totalLag; + this.totalProcessedTime = totalProcessedTime; + this.totalRecordProcessed = totalRecordProcessed; + this.pendingIds = pendingIds; + } + + public static final Builder> STREAM_INFO_LIST = new Builder>() { + @Override + public List build(Object data) { + return ((List) data).stream().map((pairObject) -> (List) pairObject) + .map((pairList) -> new FunctionStreamInfo( + BuilderFactory.STRING.build(pairList.get(9)), // name + BuilderFactory.STRING.build(pairList.get(1)), // id_to_read_from + BuilderFactory.STRING.build(pairList.get(3)), // last_error + BuilderFactory.LONG.build(pairList.get(7)), // last_processed_time + BuilderFactory.LONG.build(pairList.get(5)), // last_lag + BuilderFactory.LONG.build(pairList.get(13)), // total_lag + BuilderFactory.LONG.build(pairList.get(15)), // total_processed_time + BuilderFactory.LONG.build(pairList.get(17)), // total_record_processed + BuilderFactory.STRING_LIST.build(pairList.get(11)) // pending_ids + ))// + .collect(Collectors.toList()); + } + }; +} diff --git a/src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java b/src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java new file mode 100644 index 00000000000..5dfb86b88ad --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java @@ -0,0 +1,172 @@ +package redis.clients.jedis.gears.resps; + +import redis.clients.jedis.Builder; +import redis.clients.jedis.util.KeyValue; + +import java.util.Collections; +import java.util.List; + +import static redis.clients.jedis.BuilderFactory.*; +import static redis.clients.jedis.gears.resps.FunctionInfo.FUNCTION_INFO_LIST; +import static redis.clients.jedis.gears.resps.StreamTriggerInfo.STREAM_TRIGGER_INFO_LIST; +import static redis.clients.jedis.gears.resps.TriggerInfo.KEYSPACE_TRIGGER_INFO_LIST; + +public class GearsLibraryInfo { + private final String apiVersion; + private final List clusterFunctions; + private final String code; + private final String configuration; + private final String engine; + private final List functions; + private final List keyspaceTriggers; + private final String name; + private final List pendingAsyncCalls; + private final long pendingJobs; + private final List streamTriggers; + private final String user; + + public GearsLibraryInfo(String apiVersion, List clusterFunctions, String code, String configuration, + String engine, List functions, List keyspaceTriggers, String name, + List pendingAsyncCalls, long pendingJobs, List streamTriggers, String user) { + this.apiVersion = apiVersion; + this.clusterFunctions = clusterFunctions; + this.code = code; + this.configuration = configuration; + this.engine = engine; + this.functions = functions; + this.keyspaceTriggers = keyspaceTriggers; + this.name = name; + this.pendingAsyncCalls = pendingAsyncCalls; + this.pendingJobs = pendingJobs; + this.streamTriggers = streamTriggers; + this.user = user; + } + public String getApiVersion() { + return apiVersion; + } + + public List getClusterFunctions() { + return clusterFunctions; + } + + public String getCode() { + return code; + } + + public String getConfiguration() { + return configuration; + } + + public String getEngine() { + return engine; + } + + public List getFunctions() { + return functions; + } + + public List getKeyspaceTriggers() { + return keyspaceTriggers; + } + + public String getName() { + return name; + } + + public List getPendingAsyncCalls() { + return pendingAsyncCalls; + } + + public long getPendingJobs() { + return pendingJobs; + } + + public List getStreamTriggers() { + return streamTriggers; + } + + public String getUser() { + return user; + } + + public static final Builder LIBRARY_BUILDER = new Builder() { + @Override + public GearsLibraryInfo build(Object data) { + if (data == null) return null; + List list = (List) data; + if (list.isEmpty()) return null; + + String apiVersion = null; + List clusterFunctions = Collections.emptyList(); + String code = ""; + String configuration = null; + String engine = null; + List functions = Collections.emptyList(); + List keyspaceTriggers = Collections.emptyList(); + String name = null; + List pendingAsyncCalls = null; + long pendingJobs = 0; + List streamTriggers = Collections.emptyList(); + String user = null; + + if (list.get(0) instanceof KeyValue) { + for (KeyValue kv : (List) list) { + switch (STRING.build(kv.getKey())) { + case "api_version": + apiVersion = STRING.build(kv.getValue()); + break; + case "cluster_functions": + clusterFunctions = STRING_LIST.build(kv.getValue()); + break; + case "configuration": + configuration = STRING.build(kv.getValue()); + break; + case "engine": + engine = STRING.build(kv.getValue()); + break; + case "functions": + functions = FUNCTION_INFO_LIST.build(kv.getValue()); + break; + case "keyspace_triggers": + keyspaceTriggers = KEYSPACE_TRIGGER_INFO_LIST.build(kv.getValue()); + break; + case "name": + name = STRING.build(kv.getValue()); + break; + case "pending_async_calls": + pendingAsyncCalls = STRING_LIST.build(kv.getValue()); + break; + case "pending_jobs": + pendingJobs = LONG.build(kv.getValue()); + break; + case "stream_triggers": + streamTriggers = STREAM_TRIGGER_INFO_LIST.build(kv.getValue()); + break; + case "user": + user = STRING.build(kv.getValue()); + break; + case "code": + code = STRING.build(kv.getValue()); + break; + } + } + } else { + boolean withCode = list.size() > 23; + int offset = withCode ? 2 : 0; + apiVersion = STRING.build(list.get(1)); + clusterFunctions = STRING_LIST.build(list.get(3)); + code = withCode ? STRING.build(list.get(5)) : ""; + configuration = STRING.build(list.get(5 + offset)); + engine = STRING.build(list.get(7 + offset)); + functions = FUNCTION_INFO_LIST.build(list.get(9 + offset)); + keyspaceTriggers = KEYSPACE_TRIGGER_INFO_LIST.build(list.get(11 + offset)); + name = STRING.build(list.get(13 + offset)); + pendingAsyncCalls = STRING_LIST.build(list.get(15 + offset)); + pendingJobs = LONG.build(list.get(17 + offset)); + streamTriggers = STREAM_TRIGGER_INFO_LIST.build(list.get(19 + offset)); + user = STRING.build(list.get(21 + offset)); + } + return new GearsLibraryInfo(apiVersion, clusterFunctions, code, configuration, engine, functions, keyspaceTriggers, name, pendingAsyncCalls, pendingJobs, streamTriggers, user); + } + }; +} \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java b/src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java new file mode 100644 index 00000000000..be526e0e714 --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java @@ -0,0 +1,145 @@ +package redis.clients.jedis.gears.resps; + +import redis.clients.jedis.Builder; +import redis.clients.jedis.util.KeyValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static redis.clients.jedis.BuilderFactory.*; +import static redis.clients.jedis.gears.resps.FunctionStreamInfo.STREAM_INFO_LIST; + +public class StreamTriggerInfo { + private final String name; + private final String description; + private final String prefix; + private final boolean trim; + private final long window; + private final List streams; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getPrefix() { + return prefix; + } + public boolean isTrim() { + return trim; + } + + public long getWindow() { + return window; + } + + public List getStreams() { + return streams; + } + + public StreamTriggerInfo(String name, String description, String prefix, + long window, boolean trim, List streams) { + this.name = name; + this.description = description; + this.prefix = prefix; + this.window = window; + this.trim = trim; + this.streams = streams; + } + public StreamTriggerInfo(String name) { + this(name, null, null, 0, false, Collections.emptyList()); + } + + public StreamTriggerInfo(String name, String description, String prefix, + long window, boolean trim) { + this(name, description, prefix, window, trim, Collections.emptyList()); + } + + public static final Builder> STREAM_TRIGGER_INFO_LIST = new Builder>() { + @Override + public List build(Object data) { + List dataAsList = (List) data; + if (!dataAsList.isEmpty()) { + boolean isListOfList = dataAsList.get(0).getClass().isAssignableFrom(ArrayList.class); + if (isListOfList) { + if (((List>)data).get(0).get(0) instanceof KeyValue) { + List> dataAsKeyValues = (List>)data; + return dataAsKeyValues.stream().map(keyValues -> { + String name = null; + String description = null; + String prefix = null; + long window = 0; + boolean trim = false; + List streams = null; + + for (KeyValue kv : keyValues) { + switch (STRING.build(kv.getKey())) { + case "name": + name = STRING.build(kv.getValue()); + break; + case "description": + description = STRING.build(kv.getValue()); + break; + case "prefix": + prefix = STRING.build(kv.getValue()); + break; + case "window": + window = LONG.build(kv.getValue()); + break; + case "trim": + trim = BOOLEAN.build(kv.getValue()); + break; + case "streams": + streams = STREAM_INFO_LIST.build(kv.getValue()); + break; + } + } + return new StreamTriggerInfo(name, description, prefix, window, trim, streams); + }).collect(Collectors.toList()); + } else { + return dataAsList.stream().map((pairObject) -> (List) pairObject).map((pairList) -> { + StreamTriggerInfo result = null; + switch (pairList.size()) { + case 1: + result = new StreamTriggerInfo(STRING.build(pairList.get(0))); + break; + case 10: + result = new StreamTriggerInfo( // + STRING.build(pairList.get(3)), // name + STRING.build(pairList.get(1)), // description + STRING.build(pairList.get(5)), // prefix + LONG.build(pairList.get(9)), // window + BOOLEAN.build(pairList.get(7)) // trim + ); + break; + case 12: + result = new StreamTriggerInfo( // + STRING.build(pairList.get(3)), // name + STRING.build(pairList.get(1)), // description + STRING.build(pairList.get(5)), // prefix + LONG.build(pairList.get(11)), // window + BOOLEAN.build(pairList.get(9)), // trim + STREAM_INFO_LIST.build(pairList.get(7)) // streams + ); + break; + } + return result; + }) // + .collect(Collectors.toList()); + } + } else { + return dataAsList.stream() // + .map(STRING::build).map((name) -> new StreamTriggerInfo(name, null, null, 0, false)) // + .collect(Collectors.toList()); + } + } else { + return Collections.emptyList(); + } + } + }; +} diff --git a/src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java b/src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java new file mode 100644 index 00000000000..8a5b4704842 --- /dev/null +++ b/src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java @@ -0,0 +1,162 @@ +package redis.clients.jedis.gears.resps; + +import redis.clients.jedis.Builder; +import redis.clients.jedis.util.KeyValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static redis.clients.jedis.BuilderFactory.LONG; +import static redis.clients.jedis.BuilderFactory.STRING; + +public class TriggerInfo { + private final String name; + private final String description; + + private final String lastError; + + private final long lastExecutionTime; + + private final long numFailed; + + private final long numFinished; + + private final long numSuccess; + + private final long numTrigger; + + private final long totalExecutionTime; + + + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getLastError() { + return lastError; + } + + public long getLastExecutionTime() { + return lastExecutionTime; + } + + public long getNumFailed() { + return numFailed; + } + + public long getNumFinished() { + return numFinished; + } + + public long getNumSuccess() { + return numSuccess; + } + + public long getNumTrigger() { + return numTrigger; + } + + public long getTotalExecutionTime() { + return totalExecutionTime; + } + + public TriggerInfo(String name, String description, String lastError, long numFinished, long numSuccess, + long numFailed, long numTrigger, long lastExecutionTime, long totalExecutionTime) { + this.name = name; + this.description = description; + this.lastError = lastError; + this.numFinished = numFinished; + this.numSuccess = numSuccess; + this.numFailed = numFailed; + this.numTrigger = numTrigger; + this.lastExecutionTime = lastExecutionTime; + this.totalExecutionTime = totalExecutionTime; + } + + public static final Builder> KEYSPACE_TRIGGER_INFO_LIST = new Builder>() { + @Override + public List build(Object data) { + List dataAsList = (List) data; + if (!dataAsList.isEmpty()) { + boolean isListOfList = dataAsList.get(0).getClass().isAssignableFrom(ArrayList.class); + if (isListOfList) { + if (((List>)data).get(0).get(0) instanceof KeyValue) { + List> dataAsKeyValues = (List>)data; + return dataAsKeyValues.stream().map(keyValues -> { + String name = null; + String description = null; + String lastError = null; + long lastExecutionTime = 0; + long numFailed = 0; + long numFinished = 0; + long numSuccess = 0; + long numTrigger = 0; + long totalExecutionTime = 0; + + for (KeyValue kv : keyValues) { + switch (STRING.build(kv.getKey())) { + case "name": + name = STRING.build(kv.getValue()); + break; + case "description": + description = STRING.build(kv.getValue()); + break; + case "last_error": + lastError = STRING.build(kv.getValue()); + break; + case "last_execution_time": + lastExecutionTime = LONG.build(kv.getValue()); + break; + case "num_failed": + numFailed = LONG.build(kv.getValue()); + break; + case "num_finished": + numFinished = LONG.build(kv.getValue()); + break; + case "num_success": + numSuccess = LONG.build(kv.getValue()); + break; + case "num_trigger": + numTrigger = LONG.build(kv.getValue()); + break; + case "total_execution_time": + totalExecutionTime = LONG.build(kv.getValue()); + break; + } + } + return new TriggerInfo(name, description, lastError, numFinished, numSuccess, numFailed, numTrigger, + lastExecutionTime, totalExecutionTime); + }).collect(Collectors.toList()); + } else { + return dataAsList.stream().map((pairObject) -> (List) pairObject) + .map((pairList) -> new TriggerInfo(STRING.build(pairList.get(7)), // name + STRING.build(pairList.get(1)), // description + STRING.build(pairList.get(3)), // last_error + LONG.build(pairList.get(11)), // num_finished + LONG.build(pairList.get(13)), // num_success + LONG.build(pairList.get(9)), // num_failed + LONG.build(pairList.get(15)), // num_trigger + LONG.build(pairList.get(5)), // last_execution_time + LONG.build(pairList.get(17)) // total_execution_time + ))// + .collect(Collectors.toList()); + } + } else { + return dataAsList.stream() // + .map(STRING::build)// + .map((name) -> new TriggerInfo(name, null, null, 0,0,0,0,0,0)) // + .collect(Collectors.toList()); + } + } else { + return Collections.emptyList(); + } + } + }; +} diff --git a/src/test/java/redis/clients/jedis/modules/gears/GearsTest.java b/src/test/java/redis/clients/jedis/modules/gears/GearsTest.java new file mode 100644 index 00000000000..9abd42b5e4a --- /dev/null +++ b/src/test/java/redis/clients/jedis/modules/gears/GearsTest.java @@ -0,0 +1,538 @@ +package redis.clients.jedis.modules.gears; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import redis.clients.jedis.RedisProtocol; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.gears.TFunctionListParams; +import redis.clients.jedis.gears.TFunctionLoadParams; +import redis.clients.jedis.modules.RedisModuleCommandsTestBase; +import redis.clients.jedis.gears.resps.GearsLibraryInfo; +import redis.clients.jedis.util.KeyValue; +import redis.clients.jedis.util.RedisProtocolUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.*; + +public class GearsTest extends RedisModuleCommandsTestBase { + private static final String BAD_FUNCTION = "All Your Base Are Belong to Us"; + private static final int NUMBER_OF_LIBS = 6; + private static final List LOADED_LIBS = Arrays.asList("streamTriggers", "withFlags", "pingpong", "keyspaceTriggers", "hashitout", "withConfig"); + + @BeforeClass + public static void prepare() { + RedisModuleCommandsTestBase.prepare(); + } + + @Before + public void deleteFunctions() { + List libraries = client.tFunctionList(); + libraries.stream().map(GearsLibraryInfo::getName).forEach(library -> client.tFunctionDelete(library)); + } + + @Test + public void testFunctionLoad() throws IOException { + client.tFunctionLoad(readLibrary("pingpong.js")); + + List libraries = client.tFunctionList(); + assertTrue(libraries.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()).contains("pingpong")); + } + + @Test(expected = JedisDataException.class) + public void testFunctionLoadAlreadyLoadedFails() throws IOException { + client.tFunctionLoad(readLibrary("pingpong.js")); + client.tFunctionLoad(readLibrary("pingpong.js")); + + List libraries = client.tFunctionList(); + assertTrue(libraries.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()).contains("pingpong")); + } + + @Test + public void testFunctionLoadWithReplace() throws IOException { + client.tFunctionLoad(readLibrary("pingpong.js")); + client.tFunctionLoad(readLibrary("pingpong.js"), TFunctionLoadParams.loadParams().replace()); + + List libraries = client.tFunctionList(); + assertTrue(libraries.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()).contains("pingpong")); + } + + @Test(expected = JedisDataException.class) + public void testBadFunctionLoad() { + client.tFunctionLoad(BAD_FUNCTION); + } + + @Test + public void testFunctionListNoCodeVerboseZero() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(); + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionListNoCodeVerboseOne() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().verbose(1)); + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "stream".equalsIgnoreCase(trigger.getPrefix()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> func.getFlags().contains("raw-arguments"))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionListNoCodeVerboseTwo() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().verbose(2)); + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "stream".equalsIgnoreCase(trigger.getPrefix()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> func.getFlags().contains("raw-arguments"))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionListNoCodeVerboseThree() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().verbose(3)); + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "stream".equalsIgnoreCase(trigger.getPrefix()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> func.getFlags().contains("raw-arguments"))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionListWithCodeVerboseZero() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().withCode().verbose(0)); + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + List sources = libraryInfos.stream().map(GearsLibraryInfo::getCode).collect(Collectors.toList()); + assertTrue(sources.stream().allMatch(Objects::nonNull)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionListWithCodeVerboseOne() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().withCode().verbose(1)); + + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + List sources = libraryInfos.stream().map(GearsLibraryInfo::getCode).collect(Collectors.toList()); + assertTrue(sources.stream().allMatch(Objects::nonNull)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "stream".equalsIgnoreCase(trigger.getPrefix()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> func.getFlags().contains("raw-arguments"))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionListWithCodeVerboseTwo() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().withCode().verbose(2)); + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + List sources = libraryInfos.stream().map(GearsLibraryInfo::getCode).collect(Collectors.toList()); + assertTrue(sources.stream().allMatch(Objects::nonNull)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "stream".equalsIgnoreCase(trigger.getPrefix()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> func.getFlags().contains("raw-arguments"))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionListWithCodeVerboseThree() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().withCode().verbose(3)); + assertEquals(NUMBER_OF_LIBS, libraryInfos.size()); + + List libraryNames = libraryInfos.stream().map(GearsLibraryInfo::getName).collect(Collectors.toList()); + assertTrue(libraryNames.containsAll(LOADED_LIBS)); + + List sources = libraryInfos.stream().map(GearsLibraryInfo::getCode).collect(Collectors.toList()); + assertTrue(sources.stream().allMatch(Objects::nonNull)); + + Map>> libraryConditions = initializeTestLibraryConditions(); + + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("streamTriggers").add(lib -> lib.getStreamTriggers().stream().anyMatch(trigger -> "stream".equalsIgnoreCase(trigger.getPrefix()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> "my_set".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withFlags").add(lib -> lib.getFunctions().stream().anyMatch(func -> func.getFlags().contains("raw-arguments"))); + libraryConditions.get("pingpong").add(lib -> lib.getFunctions().stream().anyMatch(func -> "playPingPong".equalsIgnoreCase(func.getName()))); + libraryConditions.get("keyspaceTriggers").add(lib -> lib.getKeyspaceTriggers().stream().anyMatch(trigger -> "consumer".equalsIgnoreCase(trigger.getName()))); + libraryConditions.get("hashitout").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hashy".equalsIgnoreCase(func.getName()))); + libraryConditions.get("withConfig").add(lib -> lib.getFunctions().stream().anyMatch(func -> "hset".equalsIgnoreCase(func.getName()))); + + for (GearsLibraryInfo libraryInfo : libraryInfos) { + List> conditions = libraryConditions.get(libraryInfo.getName()); + if (conditions != null && !conditions.isEmpty()) { + conditions.forEach(c -> c.test(libraryInfo)); + } + } + } + + @Test + public void testFunctionLibraryListNoCodeVerboseZero() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong")); + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertNull(libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertTrue(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testFunctionLibraryListNoCodeVerboseOne() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong").verbose(1)); + + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertEquals("You PING, we PONG", libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertTrue(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testFunctionLibraryListNoCodeVerboseTwo() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong").verbose(2)); + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertEquals("You PING, we PONG", libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertTrue(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testFunctionLibraryListNoCodeVerboseThree() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong").verbose(3)); + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertEquals("You PING, we PONG", libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertTrue(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testFunctionLibraryListWithCodeVerboseZero() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong").withCode()); + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertNull(libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertFalse(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testFunctionLibraryListWithCodeVerboseOne() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong").withCode().verbose(1)); + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertEquals("You PING, we PONG", libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertFalse(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testFunctionLibraryListWithCodeVerboseTwo() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong").withCode().verbose(2)); + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertEquals("You PING, we PONG", libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertFalse(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testFunctionLibraryListWithCodeVerboseThree() throws IOException { + loadAllLibraries(); + List libraryInfos = client.tFunctionList(TFunctionListParams.listParams().library("pingpong").withCode().verbose(3)); + assertEquals(1, libraryInfos.size()); + assertEquals("pingpong", libraryInfos.get(0).getName()); + assertEquals("You PING, we PONG", libraryInfos.get(0).getFunctions().get(0).getDescription()); + assertFalse(libraryInfos.get(0).getCode().isEmpty()); + } + + @Test + public void testLibraryDelete() throws IOException { + loadAllLibraries(); + Object result = client.tFunctionDelete("pingpong"); + assertEquals("OK", result); + List libraryInfos = client.tFunctionList(); + assertEquals(NUMBER_OF_LIBS - 1, libraryInfos.size()); + } + + @Test + public void testLibraryCallStringResult() throws IOException { + loadAllLibraries(); + Object result = client.tFunctionCall("pingpong", "playPingPong", Collections.emptyList(), + Collections.emptyList()); + assertEquals(String.class, result.getClass()); + assertEquals("PONG", result); + } + + @Test + public void testLibraryCallSetValueResult() throws IOException { + loadAllLibraries(); + Object result = client.tFunctionCall("withFlags", "my_set", Collections.singletonList("MY_KEY"), + Collections.singletonList("MY_VALUE")); + assertEquals(String.class, result.getClass()); + assertEquals("OK", result); + assertEquals("MY_VALUE", client.get("MY_KEY")); + } + + @Test + public void testLibraryCallHashResult() throws IOException { + loadAllLibraries(); + Map payload = new HashMap<>(); + payload.put("C", "Dennis Ritchie"); + payload.put("Python", "Guido van Rossum"); + payload.put("C++", "Bjarne Stroustrup"); + payload.put("JavaScript", "Brendan Eich"); + payload.put("Java", "James Gosling"); + payload.put("Ruby", "Yukihiro Matsumoto"); + + client.hmset("hash1", payload); + + Object result = client.tFunctionCall("hashitout", "hashy", Collections.singletonList("hash1"), + Collections.emptyList()); + assertEquals(ArrayList.class, result.getClass()); + List asList = (List)result; + + assertEquals(RedisProtocolUtil.getRedisProtocol() == RedisProtocol.RESP2 ? 14 : 7, asList.size()); + + if (RedisProtocolUtil.getRedisProtocol() == RedisProtocol.RESP2) { + int indexOfJava = asList.indexOf("Java"); + assertTrue(indexOfJava >= 0); + assertEquals("James Gosling", asList.get(indexOfJava+1)); + int indexOfJavaScript = asList.indexOf("JavaScript"); + assertTrue(indexOfJavaScript >= 0); + assertEquals("Brendan Eich", asList.get(indexOfJavaScript+1)); + int indexOfC = asList.indexOf("C"); + assertTrue(indexOfC >= 0); + assertEquals("Dennis Ritchie", asList.get(indexOfC+1)); + int indexOfRuby = asList.indexOf("Ruby"); + assertTrue(indexOfRuby >= 0); + assertEquals("Yukihiro Matsumoto", asList.get(indexOfRuby+1)); + int indexOfPython = asList.indexOf("Python"); + assertTrue(indexOfPython >= 0); + assertEquals("Guido van Rossum", asList.get(indexOfPython+1)); + int indexOfCPP = asList.indexOf("C++"); + assertTrue(indexOfCPP >= 0); + assertEquals("Bjarne Stroustrup", asList.get(indexOfCPP+1)); + int indexOfLastUpdated = asList.indexOf("__last_updated__"); + assertTrue(indexOfLastUpdated >= 0); + assertTrue(Integer.parseInt(asList.get(indexOfLastUpdated+1)) > 0); + } else { + for (KeyValue kv : (List) result) { + if (!kv.getKey().toString().equalsIgnoreCase("__last_updated__")) { + assertTrue(payload.containsKey(kv.getKey())); + assertEquals(payload.get(kv.getKey()), kv.getValue()); + } + } + } + } + + @Test + public void testFunctionLoadWithConfig() throws IOException { + loadAllLibraries(); + List argsBefore = Arrays.asList("Dictionary1", "Pollito", "Chicken"); + client.tFunctionCall("withConfig", "hset", Collections.emptyList(), argsBefore); + + String config = "{\"last_modified_field_name\":\"changed_on\"}"; + client.tFunctionLoad(readLibrary("withConfig.js"), TFunctionLoadParams.loadParams().replace().withConfig(config)); + + List argsAfter = Arrays.asList("Dictionary2", "Gallina", "Hen"); + Object result = client.tFunctionCall("withConfig", "hset", Collections.emptyList(), argsAfter); + System.out.println(result); + + Map dict1 = client.hgetAll("Dictionary1"); + Map dict2 = client.hgetAll("Dictionary2"); + + assertTrue(dict1.containsKey("Pollito")); + assertTrue(dict1.containsKey("__last_modified__")); + assertFalse(dict1.containsKey("changed_on")); + + assertTrue(dict2.containsKey("Gallina")); + assertTrue(dict2.containsKey("changed_on")); + assertFalse(dict2.containsKey("__last_modified__")); + } + + @Test + public void testLibraryCallSetValueResultAsync() throws IOException { + loadAllLibraries(); + Object result = client.tFunctionCallAsync("withFlags", "my_set", Collections.singletonList("KEY_TWO"), + Collections.singletonList("KEY_TWO_VALUE")); + assertEquals(String.class, result.getClass()); + assertEquals("OK", result); + assertEquals("KEY_TWO_VALUE", client.get("KEY_TWO")); + } + + private static String readLibrary(String filename) throws IOException { + Path path = Paths.get("src/test/resources/functions/" + filename); + return String.join("\n", Files.readAllLines(path)); + } + + private void loadAllLibraries() throws IOException { + try (Stream walk = Files.walk(Paths.get("src/test/resources/functions/"))) { + List libs = walk + .filter(p -> !Files.isDirectory(p)) // + .map(Path::toString) // + .filter(f -> f.endsWith(".js")) // + .collect(Collectors.toList()); + + libs.forEach(lib -> { + String code; + try { + code = String.join("\n", Files.readAllLines(Paths.get(lib))); + } catch (IOException e) { + throw new RuntimeException(e); + } + client.tFunctionLoad(code, TFunctionLoadParams.loadParams().replace()); + }); + } + } + + private Map>> initializeTestLibraryConditions() { + Map>> libraryConditions = new HashMap<>(); + libraryConditions.put("streamTriggers", new ArrayList<>()); + libraryConditions.put("withFlags", new ArrayList<>()); + libraryConditions.put("pingpong", new ArrayList<>()); + libraryConditions.put("keyspaceTriggers", new ArrayList<>()); + libraryConditions.put("hashitout", new ArrayList<>()); + libraryConditions.put("withConfig", new ArrayList<>()); + + return libraryConditions; + } +} diff --git a/src/test/resources/functions/keyspaceTriggers.js b/src/test/resources/functions/keyspaceTriggers.js new file mode 100644 index 00000000000..2bca56b4dc5 --- /dev/null +++ b/src/test/resources/functions/keyspaceTriggers.js @@ -0,0 +1,12 @@ +#!js api_version=1.0 name=keyspaceTriggers + +redis.registerKeySpaceTrigger("consumer", "", function(client, data){ + if (client.call("type", data.key) != "hash") { + // key is not a hash, do not touch it. + return; + } + // get the current time in ms + var curr_time = client.call("time")[0]; + // set '__last_updated__' with the current time value + client.call('hset', data.key, '__last_updated__', curr_time); +}); \ No newline at end of file diff --git a/src/test/resources/functions/pingpong.js b/src/test/resources/functions/pingpong.js new file mode 100644 index 00000000000..511013ce16b --- /dev/null +++ b/src/test/resources/functions/pingpong.js @@ -0,0 +1,7 @@ +#!js api_version=1.0 name=pingpong + +function answer(client, data) { + return client.call('ping'); +} + +redis.registerFunction('playPingPong', answer, {description: 'You PING, we PONG'}); \ No newline at end of file diff --git a/src/test/resources/functions/streamTriggers.js b/src/test/resources/functions/streamTriggers.js new file mode 100644 index 00000000000..9d39020d1e0 --- /dev/null +++ b/src/test/resources/functions/streamTriggers.js @@ -0,0 +1,14 @@ +#!js api_version=1.0 name=streamTriggers + +redis.registerStreamTrigger( + "consumer", // consumer name + "stream", // streams prefix + function(c, data) { + // callback to run on each element added to the stream + redis.log(JSON.stringify(data, (key, value) => + typeof value === 'bigint' + ? value.toString() + : value // return everything else unchanged + )); + } +); \ No newline at end of file diff --git a/src/test/resources/functions/withConfig.js b/src/test/resources/functions/withConfig.js new file mode 100644 index 00000000000..c06944864fc --- /dev/null +++ b/src/test/resources/functions/withConfig.js @@ -0,0 +1,16 @@ +#!js api_version=1.0 name=withConfig + +var last_modified_field_name = "__last_modified__" + +if (redis.config.last_modified_field_name !== undefined) { + if (typeof redis.config.last_modified_field_name != 'string') { + throw "last_modified_field_name must be a string"; + } + last_modified_field_name = redis.config.last_modified_field_name +} + +redis.registerFunction("hset", function(client, key, field, val){ + // get the current time in ms + var curr_time = client.call("time")[0]; + return client.call('hset', key, field, val, last_modified_field_name, curr_time); +}); \ No newline at end of file diff --git a/src/test/resources/functions/withFlags.js b/src/test/resources/functions/withFlags.js new file mode 100644 index 00000000000..f4f9f05e1f7 --- /dev/null +++ b/src/test/resources/functions/withFlags.js @@ -0,0 +1,9 @@ +#!js api_version=1.0 name=withFlags +redis.registerFunction("my_set", + (c, key, val) => { + return c.call("set", key, val); + }, + { + flags: [redis.functionFlags.RAW_ARGUMENTS] + } +); \ No newline at end of file diff --git a/src/test/resources/functions/workingWIthHashes.js b/src/test/resources/functions/workingWIthHashes.js new file mode 100644 index 00000000000..6b99c516559 --- /dev/null +++ b/src/test/resources/functions/workingWIthHashes.js @@ -0,0 +1,8 @@ +#!js api_version=1.0 name=hashitout + +redis.registerFunction('hashy', function(client, key_name){ + if (client.call('type', key_name) == 'hash') { + return client.call('hgetall', key_name); + } + throw "Oops, that wasn't a Hash!"; +}); \ No newline at end of file