diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index b5a121b2f3..71d97b3d53 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -16,6 +16,7 @@ pub(crate) enum ExpectedReturnType { DoubleOrNull, ZrankReturnType, JsonToggleReturnType, + ArrayOfBools, } pub(crate) fn convert_to_expected_type( @@ -159,6 +160,21 @@ pub(crate) fn convert_to_expected_type( ) .into()), }, + ExpectedReturnType::ArrayOfBools => match value { + Value::Array(array) => { + let array_of_bools = array + .iter() + .map(|v| Value::Boolean(from_owned_redis_value::(v.clone()).unwrap())) + .collect(); + Ok(Value::Array(array_of_bools)) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array of boolean", + format!("(response was {:?})", value), + ) + .into()), + }, } } @@ -221,6 +237,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { b"INCRBYFLOAT" | b"HINCRBYFLOAT" => Some(ExpectedReturnType::Double), b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" | b"SISMEMBER" | b"PERSIST" | b"SMOVE" => Some(ExpectedReturnType::Boolean), + b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), b"SMEMBERS" | b"SINTER" => Some(ExpectedReturnType::Set), b"ZSCORE" => Some(ExpectedReturnType::DoubleOrNull), b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble), @@ -249,6 +266,21 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { mod tests { use super::*; + #[test] + fn convert_smismember() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("SMISMEMBER").arg("key").arg("elem")), + Some(ExpectedReturnType::ArrayOfBools) + )); + + let redis_response = Value::Array(vec![Value::Int(0), Value::Int(1)]); + let converted_response = + convert_to_expected_type(redis_response, Some(ExpectedReturnType::ArrayOfBools)) + .unwrap(); + let expected_response = Value::Array(vec![Value::Boolean(false), Value::Boolean(true)]); + assert_eq!(expected_response, converted_response); + } + #[test] fn convert_zadd_only_if_incr_is_included() { assert!(matches!( diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 7a6a74a39b..8eaa8d8f68 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -158,6 +158,7 @@ enum RequestType { ZRangeStore = 115; GetRange = 116; SMove = 117; + SMIsMember = 118; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 4870926a9f..61a8a3191c 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -126,6 +126,7 @@ pub enum RequestType { ZRangeStore = 115, GetRange = 116, SMove = 117, + SMIsMember = 118, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -255,6 +256,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::ZRangeStore => RequestType::ZRangeStore, ProtobufRequestType::GetRange => RequestType::GetRange, ProtobufRequestType::SMove => RequestType::SMove, + ProtobufRequestType::SMIsMember => RequestType::SMIsMember, } } } @@ -380,6 +382,7 @@ impl RequestType { RequestType::ZRangeStore => Some(cmd("ZRANGESTORE")), RequestType::GetRange => Some(cmd("GETRANGE")), RequestType::SMove => Some(cmd("SMOVE")), + RequestType::SMIsMember => Some(cmd("SMISMEMBER")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 8d72f62418..809848fab8 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -57,6 +57,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; +import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; import static redis_request.RedisRequestOuterClass.RequestType.SRem; @@ -551,6 +552,13 @@ public CompletableFuture scard(@NonNull String key) { return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture smismember(@NonNull String key, @NonNull String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand( + SMIsMember, arguments, response -> castArray(handleArrayResponse(response), Boolean.class)); + } + @Override public CompletableFuture sdiffstore(@NonNull String destination, @NonNull String[] keys) { String[] arguments = ArrayUtils.addFirst(keys, destination); diff --git a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java index 9c954ee199..a143d2e82f 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -76,6 +76,22 @@ public interface SetBaseCommands { */ CompletableFuture scard(String key); + /** + * Checks whether each member is contained in the members of the set stored at key. + * + * @see redis.io for details. + * @param key The key of the set to check. + * @param members A list of members to check for existence in the set. + * @return An array of Boolean values, each indicating if the respective + * member exists in the set. + * @example + *
{@code
+     * Boolean[] areMembers = client.smismembmer("my_set", new String[] { "a", "b", "c" }).get();
+     * assert areMembers[0] && areMembers[1] && !areMembers[2]; // Only first two elements are present in "my_set"
+     * }
+ */ + CompletableFuture smismember(String key, String[] members); + /** * Moves member from the set at source to the set at destination * , removing it from the source set. Creates a new destination set if needed. The 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 997c2e3d20..2a2d9b735c 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -68,6 +68,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; +import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; import static redis_request.RedisRequestOuterClass.RequestType.SRem; @@ -930,6 +931,21 @@ public T scard(@NonNull String key) { return getThis(); } + /** + * Checks whether each member is contained in the members of the set stored at key. + * + * @see redis.io for details. + * @param key The key of the set to check. + * @param members A list of members to check for existence in the set. + * @return Command Response - An array of Boolean values, each + * indicating if the respective member exists in the set. + */ + public T smismember(@NonNull String key, @NonNull String[] members) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); + protobufTransaction.addCommands(buildCommand(SMIsMember, commandArgs)); + return getThis(); + } + /** * Stores the difference between the first set and all the successive sets in keys * into a new set at destination. diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 793c2efb5a..09614d175a 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -88,6 +88,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; +import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; import static redis_request.RedisRequestOuterClass.RequestType.SRem; @@ -1736,6 +1737,31 @@ public void scard_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void smismember_returns_success() { + // setup + String key = "testKey"; + String[] members = {"1", "2"}; + String[] arguments = {"testKey", "1", "2"}; + Boolean[] value = {true, false}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SMIsMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.smismember(key, members); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void sdiffstore_returns_success() { 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 4de4f17da8..1b95f3ff21 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -73,6 +73,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SInter; import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; +import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; import static redis_request.RedisRequestOuterClass.RequestType.SRem; @@ -286,6 +287,9 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.sinterstore("key", new String[] {"set1", "set2"}); results.add(Pair.of(SInterStore, buildArgs("key", "set1", "set2"))); + transaction.smismember("key", new String[] {"1", "2"}); + results.add(Pair.of(SMIsMember, buildArgs("key", "1", "2"))); + transaction.sunionstore("key", new String[] {"set1", "set2"}); results.add( Pair.of( diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 22703f03c6..449ff122b7 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -1041,6 +1041,29 @@ public void sinterstore(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void smismember(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + assertEquals(2, client.sadd(key1, new String[] {"one", "two"}).get()); + assertArrayEquals( + new Boolean[] {true, false}, client.smismember(key1, new String[] {"one", "three"}).get()); + + // empty set + assertArrayEquals( + new Boolean[] {false, false}, client.smismember(key2, new String[] {"one", "three"}).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.smismember(key2, new String[] {"_"}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index cce8253fa9..ee039abb12 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -105,6 +105,7 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.scard(key7); baseTransaction.sismember(key7, "baz"); baseTransaction.smembers(key7); + baseTransaction.smismember(key7, new String[] {"baz", "foo"}); baseTransaction.sinter(new String[] {key7, key7}); baseTransaction.sadd(setKey2, new String[] {"a", "b"}); @@ -217,6 +218,7 @@ public static Object[] transactionTestResult() { 1L, true, // sismember(key7, "baz") Set.of("baz"), // smembers(key7) + new Boolean[] {true, false}, // smismembmer(key7, new String[] {"baz", "foo"}) Set.of("baz"), // sinter(new String[] { key7, key7 }) 2L, // sadd(setKey2, new String[] { "a", "b" }) 3L, // sunionstore(setKey3, new String[] { setKey2, key7 })