diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index b73a231bc3..bc70afc514 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -3,6 +3,9 @@ import static glide.ffi.resolvers.SocketListenerResolver.getSocket; import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.Incr; +import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; +import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.SetString; @@ -145,6 +148,14 @@ 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 Double handleDoubleResponse(Response response) throws RedisException { + return handleRedisResponse(Double.class, false, response); + } + @Override public CompletableFuture ping() { return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); @@ -173,4 +184,21 @@ public CompletableFuture set( String[] arguments = ArrayUtils.addAll(new String[] {key, value}, options.toArgs()); return commandManager.submitNewCommand(SetString, arguments, this::handleStringOrNullResponse); } + + @Override + public CompletableFuture incr(@NonNull String key) { + return commandManager.submitNewCommand(Incr, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture incrBy(@NonNull String key, long amount) { + return commandManager.submitNewCommand( + IncrBy, new String[] {key, Long.toString(amount)}, this::handleLongResponse); + } + + @Override + public CompletableFuture incrByFloat(@NonNull String key, double amount) { + return commandManager.submitNewCommand( + IncrByFloat, new String[] {key, Double.toString(amount)}, this::handleDoubleResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/StringCommands.java b/java/client/src/main/java/glide/api/commands/StringCommands.java index 8037a4682c..f16e33343e 100644 --- a/java/client/src/main/java/glide/api/commands/StringCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringCommands.java @@ -48,4 +48,44 @@ public interface StringCommands { * is set, return the old value as a String. */ CompletableFuture set(String key, String value, SetOptions options); + + /** + * Increments the number stored at key by one. If key does not exist, it + * is set to 0 before performing the operation. + * + * @see redis.io for details. + * @param key The key to increment its value. + * @return The value of key after the increment. An error is raised if key + * contains a value of the wrong type or contains a string that can not be represented + * as integer. + */ + CompletableFuture incr(String key); + + /** + * Increments the number stored at key by amount. If key + * does not exist, it is set to 0 before performing the operation. + * + * @see redis.io for details. + * @param key The key to increment its value. + * @param amount The amount to increment. + * @return The value of key after the increment, An error is raised if key + * contains a value of the wrong type or contains a string that cannot be represented + * as integer. + */ + CompletableFuture incrBy(String key, long amount); + + /** + * Increment the string representing a floating point number stored at key by + * amount. By using a negative increment value, the result is that the value stored at + * key is decremented. If key does not exist, it is set to 0 before + * performing the operation. + * + * @see redis.io for details. + * @param key The key to increment its value. + * @param amount The amount to increment. + * @return The value of key after the increment. An error is raised if key + * contains a value of the wrong type, or the current key content is not parsable as a + * double precision floating point number. + */ + CompletableFuture incrByFloat(String key, double amount); } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index bf666776e9..a1967ffaa0 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -13,6 +13,9 @@ import static org.mockito.Mockito.when; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.Incr; +import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; +import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.SetString; @@ -201,4 +204,77 @@ public void set_with_SetOptions_OnlyIfDoesNotExist_returns_success() { assertNotNull(response); assertEquals(value, response.get()); } + + @SneakyThrows + @Test + public void incr_returns_success() { + // setup + String key = "testKey"; + Long value = 10L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Incr), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incr(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incrBy_returns_success() { + // setup + String key = "testKey"; + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrBy), eq(new String[] {key, Long.toString(amount)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrBy(key, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incrByFloat_returns_success() { + // setup + String key = "testKey"; + double amount = 1.1; + Double value = 10.1; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrByFloat), eq(new String[] {key, Double.toString(amount)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrByFloat(key, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } } diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index 63c0417247..bdc6dc3702 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -7,6 +7,8 @@ repositories { } dependencies { + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' + // client implementation project(':client') diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 4f33ca36db..51697dbee0 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -7,9 +7,11 @@ import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.Expiry.Milliseconds; +import static java.util.concurrent.TimeUnit.SECONDS; 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,9 +20,12 @@ 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.concurrent.ExecutionException; import lombok.Getter; import lombok.SneakyThrows; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Timeout; @@ -247,4 +252,68 @@ 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 incr_commands_existing_key(BaseClient client) { + String key = RandomStringUtils.randomAlphabetic(10); + + assertEquals(OK, client.set(key, "10").get(10, SECONDS)); + + assertEquals(11, client.incr(key).get(10, SECONDS)); + assertEquals("11", client.get(key).get(10, SECONDS)); + + assertEquals(15, client.incrBy(key, 4).get(10, SECONDS)); + assertEquals("15", client.get(key).get(10, SECONDS)); + + assertEquals(20.5, client.incrByFloat(key, 5.5).get(10, SECONDS)); + assertEquals("20.5", client.get(key).get(10, SECONDS)); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void incr_commands_non_existing_key(BaseClient client) { + String key1 = RandomStringUtils.randomAlphabetic(10); + String key2 = RandomStringUtils.randomAlphabetic(10); + String key3 = RandomStringUtils.randomAlphabetic(10); + + assertNull(client.get(key1).get(10, SECONDS)); + assertEquals(1, client.incr(key1).get(10, SECONDS)); + assertEquals("1", client.get(key1).get(10, SECONDS)); + + assertNull(client.get(key2).get(10, SECONDS)); + assertEquals(3, client.incrBy(key2, 3).get(10, SECONDS)); + assertEquals("3", client.get(key2).get(10, SECONDS)); + + assertNull(client.get(key3).get(10, SECONDS)); + assertEquals(0.5, client.incrByFloat(key3, 0.5).get(10, SECONDS)); + assertEquals("0.5", client.get(key3).get(10, SECONDS)); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void test_incr_commands_type_error(BaseClient client) { + String key1 = RandomStringUtils.randomAlphabetic(10); + + assertEquals(OK, client.set(key1, "foo").get(10, SECONDS)); + + Exception incrException = + assertThrows(ExecutionException.class, () -> client.incr(key1).get(10, SECONDS)); + assertTrue(incrException.getCause() instanceof RequestException); + assertTrue(incrException.getCause().getMessage().contains("value is not an integer")); + + Exception incrByException = + assertThrows(ExecutionException.class, () -> client.incrBy(key1, 3).get(10, SECONDS)); + assertTrue(incrByException.getCause() instanceof RequestException); + assertTrue(incrByException.getCause().getMessage().contains("value is not an integer")); + + Exception incrByFloatException = + assertThrows( + ExecutionException.class, () -> client.incrByFloat(key1, 3.5).get(10, SECONDS)); + assertTrue(incrByFloatException.getCause() instanceof RequestException); + assertTrue(incrByFloatException.getCause().getMessage().contains("value is not a valid float")); + } }