diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 3d513c512c..2e6a1a9241 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -4,9 +4,14 @@ import static glide.ffi.resolvers.SocketListenerResolver.getSocket; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.commands.ConnectionManagementCommands; +import glide.api.commands.SetCommands; import glide.api.commands.StringCommands; import glide.api.models.commands.SetOptions; import glide.api.models.configuration.BaseClientConfiguration; @@ -20,6 +25,7 @@ import glide.managers.BaseCommandResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; @@ -32,7 +38,7 @@ /** Base Client class for Redis */ @AllArgsConstructor public abstract class BaseClient - implements AutoCloseable, ConnectionManagementCommands, StringCommands { + implements AutoCloseable, ConnectionManagementCommands, StringCommands, SetCommands { /** Redis simple string response with "OK" */ public static final String OK = ConstantResponse.OK.toString(); @@ -149,10 +155,18 @@ protected String handleStringOrNullResponse(Response response) throws RedisExcep return handleRedisResponse(String.class, true, response); } + protected Long handleLongResponse(Response response) throws RedisException { + return handleRedisResponse(Long.class, false, response); + } + protected Object[] handleArrayResponse(Response response) { return handleRedisResponse(Object[].class, true, response); } + protected Set handleSetResponse(Response response) { + return handleRedisResponse(Set.class, false, response); + } + @Override public CompletableFuture ping() { return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); @@ -181,4 +195,26 @@ public CompletableFuture set( String[] arguments = ArrayUtils.addAll(new String[] {key, value}, options.toArgs()); return commandManager.submitNewCommand(SetString, arguments, this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sadd(String key, String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture srem(String key, String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SRem, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture> smembers(String key) { + return commandManager.submitNewCommand(SMembers, new String[] {key}, this::handleSetResponse); + } + + @Override + public CompletableFuture scard(String key) { + return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/SetCommands.java b/java/client/src/main/java/glide/api/commands/SetCommands.java new file mode 100644 index 0000000000..f2098c9b08 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/SetCommands.java @@ -0,0 +1,77 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +/** + * Set Commands interface. + * + * @see Set Commands + */ +public interface SetCommands { + /** + * Add specified members to the set stored at key. Specified members that are already + * a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key where members will be added to its set. + * @param members A list of members to add to the set stored at key. + * @return The number of members that were added to the set, excluding members already present. + * @remarks If key does not exist, a new set is created before adding members + * . + * @example + *

+ * int result = client.sadd("my_set", new String[]{"member1", "member2"}).get(); + * // result: 2 + * + */ + CompletableFuture sadd(String key, String[] members); + + /** + * Remove specified members from the set stored at key. Specified members that are + * not a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key from which members will be removed. + * @param members A list of members to remove from the set stored at key. + * @return The number of members that were removed from the set, excluding non-existing members. + * @remarks If key does not exist, it is treated as an empty set and this command + * returns 0. + * @example + *

+ * int result = client.srem("my_set", new String[]{"member1", "member2"}).get(); + * // result: 2 + * + */ + CompletableFuture srem(String key, String[] members); + + /** + * Retrieve all the members of the set value stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the set members. + * @return A Set of all members of the set. + * @remarks If key does not exist an empty set will be returned. + * @example + *

+ * {@literal Set} result = client.smembers("my_set").get(); + * // result: {"member1", "member2", "member3"} + * + */ + CompletableFuture> smembers(String key); + + /** + * Retrieve the set cardinality (number of elements) of the set stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the number of set members. + * @return The cardinality (number of elements) of the set, or 0 if the key does not exist. + * @example + *

+ * int result = client.scard("my_set").get(); + * // result: 3 + * + */ + CompletableFuture scard(String key); +} 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 92248e5047..6d40cc5ce2 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -5,6 +5,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -165,6 +169,74 @@ public T set(String key, String value, SetOptions options) { return getThis(); } + /** + * Add specified members to the set stored at key. Specified members that are already + * a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key where members will be added to its set. + * @param members A list of members to add to the set stored at key. + * @return Command Response - The number of members that were added to the set, excluding members + * already present. + * @remarks If key does not exist, a new set is created before adding members + * . + */ + public T sadd(String key, String[] members) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); + + protobufTransaction.addCommands(buildCommand(SAdd, commandArgs)); + return getThis(); + } + + /** + * Remove specified members from the set stored at key. Specified members that are + * not a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key from which members will be removed. + * @param members A list of members to remove from the set stored at key. + * @return Command Response - The number of members that were removed from the set, excluding + * non-existing members. + * @remarks If key does not exist, it is treated as an empty set and this command + * returns 0. + */ + public T srem(String key, String[] members) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); + + protobufTransaction.addCommands(buildCommand(SRem, commandArgs)); + return getThis(); + } + + /** + * Retrieve all the members of the set value stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the set members. + * @return Command Response - A Set of all members of the set. + * @remarks If key does not exist an empty set will be returned. + */ + public T smembers(String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(SMembers, commandArgs)); + return getThis(); + } + + /** + * Retrieve the set cardinality (number of elements) of the set stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the number of set members. + * @return Command Response - The cardinality (number of elements) of the set, or 0 if the key + * does not exist. + */ + public T scard(String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(SCard, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index a77111b082..29405685dd 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -15,6 +15,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -22,8 +26,10 @@ import glide.api.models.commands.SetOptions.Expiry; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Set; import java.util.concurrent.CompletableFuture; import lombok.SneakyThrows; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -267,4 +273,100 @@ public void info_with_empty_InfoOptions_returns_success() { assertEquals(testResponse, response); assertEquals(testPayload, payload); } + + @SneakyThrows + @Test + public void sadd_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"testMember1", "testMember2"}; + String[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sadd(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void srem_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"testMember1", "testMember2"}; + String[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void smembers_returns_success() { + // setup + String key = "testKey"; + Set value = Set.of("testMember"); + + CompletableFuture> testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SMembers), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.smembers(key); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scard_returns_success() { + // setup + String key = "testKey"; + Long value = 2L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SCard), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java index cbdcd4632c..745f5b124a 100644 --- a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -6,6 +6,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -57,6 +61,18 @@ public void transaction_builds_protobuf_request() { Info, ArgsArray.newBuilder().addArgs(InfoOptions.Section.EVERYTHING.toString()).build())); + transaction.sadd("key", new String[] {"value"}); + results.add(Pair.of(SAdd, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.srem("key", new String[] {"value"}); + results.add(Pair.of(SRem, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.smembers("key"); + results.add(Pair.of(SMembers, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.scard("key"); + results.add(Pair.of(SCard, ArgsArray.newBuilder().addArgs("key").build())); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { 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 5cd4c52df4..591fcbd855 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -6,6 +6,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -56,6 +60,18 @@ public void transaction_builds_protobuf_request() { Info, ArgsArray.newBuilder().addArgs(InfoOptions.Section.EVERYTHING.toString()).build())); + transaction.sadd("key", new String[] {"value"}); + results.add(Pair.of(SAdd, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.srem("key", new String[] {"value"}); + results.add(Pair.of(SRem, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.smembers("key"); + results.add(Pair.of(SMembers, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.scard("key"); + results.add(Pair.of(SCard, ArgsArray.newBuilder().addArgs("key").build())); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 4f33ca36db..4bddc118e1 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import glide.api.BaseClient; import glide.api.RedisClient; @@ -18,7 +19,11 @@ import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.configuration.RedisClusterClientConfiguration; +import glide.api.models.exceptions.RequestException; import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; import lombok.Getter; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; @@ -247,4 +252,49 @@ public void set_missing_value_and_returnOldValue_is_null(BaseClient client) { String data = client.set("another", ANOTHER_VALUE, options).get(); assertNull(data); } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void sadd_srem_scard_smembers_existing_set(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals( + 4, client.sadd(key, new String[] {"member1", "member2", "member3", "member4"}).get()); + assertEquals(1, client.srem(key, new String[] {"member3", "nonExistingMember"}).get()); + + Set expectedMembers = Set.of("member1", "member2", "member4"); + assertEquals(expectedMembers, client.smembers(key).get()); + assertEquals(1, client.srem(key, new String[] {"member1"}).get()); + assertEquals(2, client.scard(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void srem_scard_smembers_non_existing_key(BaseClient client) { + assertEquals(0, client.srem("nonExistingKey", new String[] {"member"}).get()); + assertEquals(0, client.scard("nonExistingKey").get()); + assertEquals(Set.of(), client.smembers("nonExistingKey").get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void sadd_srem_scard_smembers_key_with_non_set_value(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "foo").get()); + + Exception e = + assertThrows(ExecutionException.class, () -> client.sadd(key, new String[] {"baz"}).get()); + assertTrue(e.getCause() instanceof RequestException); + + e = assertThrows(ExecutionException.class, () -> client.srem(key, new String[] {"baz"}).get()); + assertTrue(e.getCause() instanceof RequestException); + + e = assertThrows(ExecutionException.class, () -> client.scard(key).get()); + assertTrue(e.getCause() instanceof RequestException); + + e = assertThrows(ExecutionException.class, () -> client.smembers(key).get()); + assertTrue(e.getCause() instanceof RequestException); + } } diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 2b01067cc2..f891dc757b 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -3,6 +3,7 @@ import glide.api.models.BaseTransaction; import glide.api.models.commands.SetOptions; +import java.util.Set; import java.util.UUID; public class TestUtilities { @@ -10,15 +11,21 @@ public class TestUtilities { public static BaseTransaction transactionTest(BaseTransaction baseTransaction) { String key1 = "{key}" + UUID.randomUUID(); String key2 = "{key}" + UUID.randomUUID(); + String key3 = "{key}" + UUID.randomUUID(); baseTransaction.set(key1, "bar"); baseTransaction.set(key2, "baz", SetOptions.builder().returnOldValue(true).build()); baseTransaction.customCommand("MGET", key1, key2); + baseTransaction.sadd(key3, new String[] {"baz", "foo"}); + baseTransaction.srem(key3, new String[] {"foo"}); + baseTransaction.scard(key3); + baseTransaction.smembers(key3); + return baseTransaction; } public static Object[] transactionTestResult() { - return new Object[] {"OK", null, new String[] {"bar", "baz"}}; + return new Object[] {"OK", null, new String[] {"bar", "baz"}, 2L, 1L, 1L, Set.of("baz")}; } }