Skip to content

Commit

Permalink
Add setoptions to set command
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Carbonetto <[email protected]>
  • Loading branch information
acarbonetto committed Dec 19, 2023
1 parent 98e6e10 commit 0d9b2b4
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 2 deletions.
50 changes: 49 additions & 1 deletion java/client/src/main/java/babushka/api/RedisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
import static babushka.api.commands.Command.RequestType.CUSTOM_COMMAND;
import static babushka.api.commands.Command.RequestType.GETSTRING;
import static babushka.api.commands.Command.RequestType.SETSTRING;
import static babushka.api.models.commands.SetOptions.createSetOptions;

import babushka.api.commands.BaseCommands;
import babushka.api.commands.Command;
import babushka.api.commands.StringCommands;
import babushka.api.commands.Transaction;
import babushka.api.commands.VoidCommands;
import babushka.api.models.commands.SetOptions;
import babushka.api.models.configuration.RedisClientConfiguration;
import babushka.managers.CommandManager;
import connection_request.ConnectionRequestOuterClass;
import java.lang.reflect.Array;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import response.ResponseOuterClass;
Expand Down Expand Up @@ -45,7 +49,7 @@ public RedisClient(CommandManager commandManager) {
}

/**
* Close the client if it's open
* Close the Redis client
*
* @return
*/
Expand All @@ -68,6 +72,14 @@ public CompletableFuture<?> exec(Transaction transaction) {
return new CompletableFuture<>();
}

/**
* Executes a single custom command, without checking inputs. Every part of the command, including subcommands,
* should be added as a separate value in args.
*
* @param cmd to be executed
* @param args arguments for the command
* @return CompletableFuture with the response
*/
public CompletableFuture<Object> customCommand(String cmd, String[] args) {
String[] commandArguments = (String[]) Array.newInstance(String.class, args.length + 1);
commandArguments[0] = cmd;
Expand All @@ -78,15 +90,51 @@ public CompletableFuture<Object> customCommand(String cmd, String[] args) {
return exec(command, BaseCommands::handleResponse);
}

/**
* Get the value associated with the given key, or null if no such value exists.
* See https://redis.io/commands/set/ for details.
*
* @param key - The key to retrieve from the database.
* @return If `key` exists, returns the value of `key` as a string. Otherwise, return null
*/
public CompletableFuture<String> get(String key) {
Command command =
Command.builder().requestType(GETSTRING).arguments(new String[] {key}).build();
return exec(command, StringCommands::handleStringResponse);
}

/**
* Set the given key with the given value. Return value is dependent on the passed options.
* See https://redis.io/commands/set/ for details.
*
* @param key - The key to store.
* @param value - The value to store with the given key.
* @return null
*/
public CompletableFuture<Void> set(String key, String value) {
Command command =
Command.builder().requestType(SETSTRING).arguments(new String[] {key, value}).build();
return exec(command, VoidCommands::handleVoidResponse);
}

/**
* Set the given key with the given value. Return value is dependent on the passed options.
* See https://redis.io/commands/set/ for details.
*
* @param key - The key to store.
* @param value - The value to store with the given key.
* @param options - The Set options
* @return string or null
* If value isn't set because of `onlyIfExists` or `onlyIfDoesNotExist` conditions, return null.
* If `returnOldValue` is set, return the old value as a string.
*/
public CompletableFuture<String> set(String key, String value, SetOptions options) {
LinkedList<String> args = new LinkedList<>();
args.add(key);
args.add(value);
args.addAll(createSetOptions(options));
Command command =
Command.builder().requestType(SETSTRING).arguments(args.toArray(new String[0])).build();
return exec(command, StringCommands::handleStringResponse);
}
}
16 changes: 16 additions & 0 deletions java/client/src/main/java/babushka/api/commands/Command.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package babushka.api.commands;

import java.util.Arrays;
import lombok.Builder;

@Builder
Expand All @@ -13,4 +14,19 @@ public enum RequestType {
GETSTRING,
SETSTRING
}

@Override
public boolean equals(Object o) {
if (o instanceof Command) {
Command otherCommand = (Command) o;
if (this.requestType != otherCommand.requestType)
return false;

if (!Arrays.equals(this.arguments, otherCommand.arguments))
return false;

return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public interface StringCommands {
public static String handleStringResponse(Response response) {
// return function to convert protobuf.Response into the response object by
// calling valueFromPointer
return (String) BaseCommands.handleResponse(response);
Object value = BaseCommands.handleResponse(response);
return value == null ? null : (String) value;
}

CompletableFuture<?> get(String key);
Expand Down
104 changes: 104 additions & 0 deletions java/client/src/main/java/babushka/api/models/commands/SetOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package babushka.api.models.commands;

import java.util.LinkedList;
import java.util.List;
import lombok.Builder;
import lombok.NonNull;

@Builder
@NonNull
public class SetOptions {

/**
* `onlyIfDoesNotExist` - Only set the key if it does not already exist. Equivalent to `NX` in the Redis API.
* `onlyIfExists` - Only set the key if it already exist. Equivalent to `EX` in the Redis API.
* if `conditional` is not set the value will be set regardless of prior value existence.
* If value isn't set because of the condition, return null.
*/
private ConditionalSet conditionalSet;

/**
* Return the old string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value stored at key is not a string.
* Equivalent to `GET` in the Redis API.
*/
private boolean returnOldValue;

/**
* If not set, no expiry time will be set for the value.
*/
private TimeToLive expiry;

public enum ConditionalSet {
ONLY_IF_EXISTS,
ONLY_IF_DOES_NOT_EXIST
}

@Builder
public static class TimeToLive {
private TimeToLiveType type;
private int count;
}

public enum TimeToLiveType {
/**
* Retain the time to live associated with the key. Equivalent to `KEEPTTL` in the Redis API.
*/
KEEP_EXISTING,
/**
* Set the specified expire time, in seconds. Equivalent to `EX` in the Redis API.
*/
SECONDS,
/**
* Set the specified expire time, in milliseconds. Equivalent to `PX` in the Redis API.
*/
MILLISECONDS,
/**
* Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Redis API.
*/
UNIX_SECONDS,
/**
* Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the Redis API.
*/
UNIX_MILLISECONDS
}

public static String CONDITIONAL_SET_ONLY_IF_EXISTS = "XX";
public static String CONDITIONAL_SET_ONLY_IF_DOES_NOT_EXIST = "NX";
public static String RETURN_OLD_VALUE = "GET";
public static String TIME_TO_LIVE_KEEP_EXISTING = "KEEPTTL";
public static String TIME_TO_LIVE_SECONDS = "EX";
public static String TIME_TO_LIVE_MILLISECONDS = "PX";
public static String TIME_TO_LIVE_UNIX_SECONDS = "EXAT";
public static String TIME_TO_LIVE_UNIX_MILLISECONDS = "PXAT";

public static List createSetOptions(SetOptions options) {
List optionArgs = new LinkedList();
if (options.conditionalSet != null) {
if (options.conditionalSet == ConditionalSet.ONLY_IF_EXISTS) {
optionArgs.add(CONDITIONAL_SET_ONLY_IF_EXISTS);
} else if (options.conditionalSet == ConditionalSet.ONLY_IF_DOES_NOT_EXIST) {
optionArgs.add(CONDITIONAL_SET_ONLY_IF_DOES_NOT_EXIST);
}
}

if (options.returnOldValue) {
optionArgs.add(RETURN_OLD_VALUE);
}

if (options.expiry != null) {
if (options.expiry.type == TimeToLiveType.KEEP_EXISTING) {
optionArgs.add(TIME_TO_LIVE_KEEP_EXISTING);
} else if (options.expiry.type == TimeToLiveType.SECONDS) {
optionArgs.add(TIME_TO_LIVE_SECONDS + " " + options.expiry.count);
} else if (options.expiry.type == TimeToLiveType.MILLISECONDS) {
optionArgs.add(TIME_TO_LIVE_MILLISECONDS + " " + options.expiry.count);
} else if (options.expiry.type == TimeToLiveType.UNIX_SECONDS) {
optionArgs.add(TIME_TO_LIVE_UNIX_SECONDS + " " + options.expiry.count);
} else if (options.expiry.type == TimeToLiveType.UNIX_MILLISECONDS) {
optionArgs.add(TIME_TO_LIVE_UNIX_MILLISECONDS + " " + options.expiry.count);
}
}

return optionArgs;
}
}
82 changes: 82 additions & 0 deletions java/client/src/test/java/babushka/api/RedisClientTest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package babushka.api;

import static babushka.api.models.commands.SetOptions.CONDITIONAL_SET_ONLY_IF_DOES_NOT_EXIST;
import static babushka.api.models.commands.SetOptions.CONDITIONAL_SET_ONLY_IF_EXISTS;
import static babushka.api.models.commands.SetOptions.RETURN_OLD_VALUE;
import static babushka.api.models.commands.SetOptions.TIME_TO_LIVE_KEEP_EXISTING;
import static babushka.api.models.commands.SetOptions.TIME_TO_LIVE_UNIX_SECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import babushka.api.commands.Command;
import babushka.api.models.commands.SetOptions;
import babushka.api.models.configuration.RedisClientConfiguration;
import babushka.managers.CommandManager;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -111,6 +119,80 @@ public void set_success() throws ExecutionException, InterruptedException {
// teardown
}

@Test
public void set_withOptionsOnlyIfExists_success() throws ExecutionException, InterruptedException {
// setup
String key = "testKey";
String value = "testValue";
SetOptions setOptions = SetOptions.builder()
.conditionalSet(SetOptions.ConditionalSet.ONLY_IF_EXISTS)
.returnOldValue(false)
.expiry(SetOptions.TimeToLive.builder()
.type(SetOptions.TimeToLiveType.KEEP_EXISTING)
.build())
.build();
Command cmd =
Command.builder()
.requestType(Command.RequestType.SETSTRING)
.arguments(new String[] {
key,
value,
CONDITIONAL_SET_ONLY_IF_EXISTS,
TIME_TO_LIVE_KEEP_EXISTING
})
.build();
CompletableFuture<String> testResponse = mock(CompletableFuture.class);
when(testResponse.get()).thenReturn(null);
when(commandManager.<String>submitNewCommand(eq(cmd), any())).thenReturn(testResponse);

// exercise
CompletableFuture<String> response = service.set(key, value, setOptions);

// verify
assertNotNull(response);
assertNull(response.get());

// teardown
}

@Test
public void set_withOptionsOnlyIfDoesNotExist_success() throws ExecutionException, InterruptedException {
// setup
String key = "testKey";
String value = "testValue";
SetOptions setOptions = SetOptions.builder()
.conditionalSet(SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST)
.returnOldValue(true)
.expiry(SetOptions.TimeToLive.builder()
.type(SetOptions.TimeToLiveType.UNIX_SECONDS)
.count(60)
.build())
.build();
Command cmd =
Command.builder()
.requestType(Command.RequestType.SETSTRING)
.arguments(new String[] {
key,
value,
CONDITIONAL_SET_ONLY_IF_DOES_NOT_EXIST,
RETURN_OLD_VALUE,
TIME_TO_LIVE_UNIX_SECONDS + " 60"
})
.build();
CompletableFuture<String> testResponse = mock(CompletableFuture.class);
when(testResponse.get()).thenReturn(value);
when(commandManager.<String>submitNewCommand(eq(cmd), any())).thenReturn(testResponse);

// exercise
CompletableFuture<String> response = service.set(key, value, setOptions);

// verify
assertNotNull(response);
assertEquals(value, response.get());

// teardown
}

@Test
public void test_ping_success() {
// setup
Expand Down

0 comments on commit 0d9b2b4

Please sign in to comment.