Skip to content

Commit

Permalink
Java: Adding command LMPOP (#292) (valkey-io#1457)
Browse files Browse the repository at this point in the history
* Java: Adding command `LMPOP` (#292)

* Java: Adding command LMPOP

- interface & client implementation
- new map type conversion
- renaming LmPop to LMPop

* address review comments

* comment typo

* fixing examples

---------

Co-authored-by: TJ Zhang <tj.zhang@improving.com>
  • Loading branch information
tjzhang-BQ and TJ Zhang authored May 24, 2024
1 parent 201f551 commit d880b87
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 19 deletions.
56 changes: 56 additions & 0 deletions glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ pub(crate) enum ExpectedReturnType {
DoubleOrNull,
ZRankReturnType,
JsonToggleReturnType,
ArrayOfStrings,
ArrayOfBools,
ArrayOfDoubleOrNull,
Lolwut,
ArrayOfStringAndArrays,
ArrayOfArraysOfDoubleOrNull,
ArrayOfPairs,
ArrayOfMemberScorePairs,
Expand Down Expand Up @@ -176,6 +178,14 @@ pub(crate) fn convert_to_expected_type(
)
.into()),
},
ExpectedReturnType::ArrayOfStrings => match value {
Value::Array(array) => convert_array_elements(array, ExpectedReturnType::BulkString),
_ => Err((
ErrorKind::TypeError,
"Response couldn't be converted to an array of bulk strings",
)
.into()),
},
ExpectedReturnType::ArrayOfDoubleOrNull => match value {
Value::Array(array) => convert_array_elements(array, ExpectedReturnType::DoubleOrNull),
_ => Err((
Expand Down Expand Up @@ -324,6 +334,29 @@ pub(crate) fn convert_to_expected_type(
// RESP2 returns scores as strings, but we want scores as type double.
convert_to_array_of_pairs(value, Some(ExpectedReturnType::Double))
}
// Used by LMPOP and BLMPOP
// The server response can be an array or null
//
// Example:
// let input = ["key", "val1", "val2"]
// let expected =("key", vec!["val1", "val2"])
ExpectedReturnType::ArrayOfStringAndArrays => match value {
Value::Nil => Ok(value),
Value::Array(array) if array.len() == 2 && matches!(array[1], Value::Array(_)) => {
// convert the array to a map of string to string-array
let map = convert_array_to_map(
array,
Some(ExpectedReturnType::BulkString),
Some(ExpectedReturnType::ArrayOfStrings),
)?;
Ok(map)
}
_ => Err((
ErrorKind::TypeError,
"Response couldn't be converted to a pair of String/String-Array return type",
)
.into()),
},
// Used by BZPOPMIN/BZPOPMAX, which return an array consisting of the key of the sorted set that was popped, the popped member, and its score.
// RESP2 returns the score as a string, but RESP3 returns the score as a double. Here we convert string scores into type double.
ExpectedReturnType::KeyWithMemberAndScore => match value {
Expand Down Expand Up @@ -508,6 +541,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
b"BZMPOP" | b"ZMPOP" => Some(ExpectedReturnType::ZMPopReturnType),
b"JSON.TOGGLE" => Some(ExpectedReturnType::JsonToggleReturnType),
b"GEOPOS" => Some(ExpectedReturnType::ArrayOfArraysOfDoubleOrNull),
b"LMPOP" => Some(ExpectedReturnType::ArrayOfStringAndArrays),
b"HRANDFIELD" => cmd
.position(b"WITHVALUES")
.map(|_| ExpectedReturnType::ArrayOfPairs),
Expand Down Expand Up @@ -752,6 +786,28 @@ mod tests {
assert_eq!(expected_response, converted_flat_array);
}

#[test]
fn convert_to_array_of_string_and_array_return_type() {
assert!(matches!(
expected_type_for_cmd(redis::cmd("LMPOP").arg("1").arg("key").arg("LEFT")),
Some(ExpectedReturnType::ArrayOfStringAndArrays)
));

// testing value conversion
let flat_array = Value::Array(vec![
Value::BulkString(b"1".to_vec()),
Value::Array(vec![Value::BulkString(b"one".to_vec())]),
]);
let expected_response = Value::Map(vec![(
Value::BulkString("1".into()),
Value::Array(vec![Value::BulkString(b"one".to_vec())]),
)]);
let converted_flat_array =
convert_to_expected_type(flat_array, Some(ExpectedReturnType::ArrayOfStringAndArrays))
.unwrap();
assert_eq!(expected_response, converted_flat_array);
}

#[test]
fn convert_zadd_only_if_incr_is_included() {
assert!(matches!(
Expand Down
1 change: 1 addition & 0 deletions glide-core/src/protobuf/redis_request.proto
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ enum RequestType {
BitPos = 147;
BitOp = 148;
FunctionLoad = 150;
LMPop = 155;
}

message Command {
Expand Down
3 changes: 3 additions & 0 deletions glide-core/src/request_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ pub enum RequestType {
BitPos = 147,
BitOp = 148,
FunctionLoad = 150,
LMPop = 155,
}

fn get_two_word_command(first: &str, second: &str) -> Cmd {
Expand Down Expand Up @@ -314,6 +315,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType {
ProtobufRequestType::BitCount => RequestType::BitCount,
ProtobufRequestType::BZMPop => RequestType::BZMPop,
ProtobufRequestType::SetBit => RequestType::SetBit,
ProtobufRequestType::LMPop => RequestType::LMPop,
ProtobufRequestType::ZInterCard => RequestType::ZInterCard,
ProtobufRequestType::ZMPop => RequestType::ZMPop,
ProtobufRequestType::GetBit => RequestType::GetBit,
Expand Down Expand Up @@ -471,6 +473,7 @@ impl RequestType {
RequestType::ZRandMember => Some(cmd("ZRANDMEMBER")),
RequestType::BitCount => Some(cmd("BITCOUNT")),
RequestType::BZMPop => Some(cmd("BZMPOP")),
RequestType::LMPop => Some(cmd("LMPOP")),
RequestType::SetBit => Some(cmd("SETBIT")),
RequestType::ZInterCard => Some(cmd("ZINTERCARD")),
RequestType::ZMPop => Some(cmd("ZMPOP")),
Expand Down
39 changes: 39 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static glide.ffi.resolvers.SocketListenerResolver.getSocket;
import static glide.utils.ArrayTransformUtils.castArray;
import static glide.utils.ArrayTransformUtils.castArrayofArrays;
import static glide.utils.ArrayTransformUtils.castMapOfArrays;
import static glide.utils.ArrayTransformUtils.concatenateArrays;
import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray;
import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray;
Expand Down Expand Up @@ -49,6 +50,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.LIndex;
import static redis_request.RedisRequestOuterClass.RequestType.LInsert;
import static redis_request.RedisRequestOuterClass.RequestType.LLen;
import static redis_request.RedisRequestOuterClass.RequestType.LMPop;
import static redis_request.RedisRequestOuterClass.RequestType.LPop;
import static redis_request.RedisRequestOuterClass.RequestType.LPush;
import static redis_request.RedisRequestOuterClass.RequestType.LPushX;
Expand Down Expand Up @@ -134,6 +136,7 @@
import glide.api.models.Script;
import glide.api.models.commands.ExpireOptions;
import glide.api.models.commands.LInsertOptions.InsertPosition;
import glide.api.models.commands.PopDirection;
import glide.api.models.commands.RangeOptions;
import glide.api.models.commands.RangeOptions.LexRange;
import glide.api.models.commands.RangeOptions.RangeQuery;
Expand Down Expand Up @@ -340,6 +343,16 @@ protected <V> Map<String, V> handleMapResponse(Response response) throws RedisEx
return handleRedisResponse(Map.class, false, response);
}

/**
* @param response A Protobuf response
* @return A map of <code>String</code> to <code>V</code> or <code>null</code>
* @param <V> Value type.
*/
@SuppressWarnings("unchecked") // raw Map cast to Map<String, V>
protected <V> Map<String, V> handleMapOrNullResponse(Response response) throws RedisException {
return handleRedisResponse(Map.class, true, response);
}

@SuppressWarnings("unchecked") // raw Set cast to Set<String>
protected Set<String> handleSetResponse(Response response) throws RedisException {
return handleRedisResponse(Set.class, false, response);
Expand Down Expand Up @@ -1454,4 +1467,30 @@ public CompletableFuture<Long> bitop(
concatenateArrays(new String[] {bitwiseOperation.toString(), destination}, keys);
return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse);
}

@Override
public CompletableFuture<Map<String, String[]>> lmpop(
@NonNull String[] keys, @NonNull PopDirection direction, long count) {
String[] arguments =
concatenateArrays(
new String[] {Long.toString(keys.length)},
keys,
new String[] {direction.toString(), COUNT_FOR_LIST_REDIS_API, Long.toString(count)});
return commandManager.submitNewCommand(
LMPop,
arguments,
response -> castMapOfArrays(handleMapOrNullResponse(response), String.class));
}

@Override
public CompletableFuture<Map<String, String[]>> lmpop(
@NonNull String[] keys, @NonNull PopDirection direction) {
String[] arguments =
concatenateArrays(
new String[] {Long.toString(keys.length)}, keys, new String[] {direction.toString()});
return commandManager.submitNewCommand(
LMPop,
arguments,
response -> castMapOfArrays(handleMapOrNullResponse(response), String.class));
}
}
46 changes: 46 additions & 0 deletions java/client/src/main/java/glide/api/commands/ListBaseCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
package glide.api.commands;

import glide.api.models.commands.LInsertOptions.InsertPosition;
import glide.api.models.commands.PopDirection;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
Expand All @@ -11,6 +13,8 @@
* @see <a href="https://redis.io/commands/?group=list">List Commands</a>
*/
public interface ListBaseCommands {
/** Redis API keyword used to extract specific count of members from a sorted set. */
String COUNT_FOR_LIST_REDIS_API = "COUNT";

/**
* Inserts all the specified values at the head of the list stored at <code>key</code>. <code>
Expand Down Expand Up @@ -369,4 +373,46 @@ CompletableFuture<Long> linsert(
* }</pre>
*/
CompletableFuture<Long> lpushx(String key, String[] elements);

/**
* Pops one or more elements from the first non-empty list from the provided <code>keys
* </code>.
*
* @since Redis 7.0 and above.
* @apiNote When in cluster mode, all <code>keys</code> must map to the same hash slot.
* @see <a href="https://valkey.io/commands/lmpop/">valkey.io</a> for details.
* @param keys An array of keys to lists.
* @param direction The direction based on which elements are popped from - see {@link
* PopDirection}.
* @param count The maximum number of popped elements.
* @return A <code>Map</code> of <code>key</code> name mapped array of popped elements.
* @example
* <pre>{@code
* client.lpush("testKey", new String[] {"one", "two", "three"}).get();
* Map<String, String[]> result = client.lmpop(new String[] {"testKey"}, PopDirection.LEFT, 1L).get();
* String[] resultValue = result.get("testKey");
* assertArrayEquals(new String[] {"three"}, resultValue);
* }</pre>
*/
CompletableFuture<Map<String, String[]>> lmpop(String[] keys, PopDirection direction, long count);

/**
* Pops one element from the first non-empty list from the provided <code>keys</code>.
*
* @since Redis 7.0 and above.
* @apiNote When in cluster mode, all <code>keys</code> must map to the same hash slot.
* @see <a href="https://valkey.io/commands/lmpop/">valkey.io</a> for details.
* @param keys An array of keys to lists.
* @param direction The direction based on which elements are popped from - see {@link
* PopDirection}.
* @return A <code>Map</code> of <code>key</code> name mapped array of the popped element.
* @example
* <pre>{@code
* client.lpush("testKey", new String[] {"one", "two", "three"}).get();
* Map<String, String[]> result = client.lmpop(new String[] {"testKey"}, PopDirection.LEFT).get();
* String[] resultValue = result.get("testKey");
* assertArrayEquals(new String[] {"three"}, resultValue);
* }</pre>
*/
CompletableFuture<Map<String, String[]>> lmpop(String[] keys, PopDirection direction);
}
51 changes: 51 additions & 0 deletions java/client/src/main/java/glide/api/models/BaseTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package glide.api.models;

import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API;
import static glide.api.commands.ListBaseCommands.COUNT_FOR_LIST_REDIS_API;
import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.COUNT_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API;
Expand Down Expand Up @@ -64,6 +65,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.LIndex;
import static redis_request.RedisRequestOuterClass.RequestType.LInsert;
import static redis_request.RedisRequestOuterClass.RequestType.LLen;
import static redis_request.RedisRequestOuterClass.RequestType.LMPop;
import static redis_request.RedisRequestOuterClass.RequestType.LPop;
import static redis_request.RedisRequestOuterClass.RequestType.LPush;
import static redis_request.RedisRequestOuterClass.RequestType.LPushX;
Expand Down Expand Up @@ -145,6 +147,7 @@
import glide.api.models.commands.InfoOptions;
import glide.api.models.commands.InfoOptions.Section;
import glide.api.models.commands.LInsertOptions.InsertPosition;
import glide.api.models.commands.PopDirection;
import glide.api.models.commands.RangeOptions;
import glide.api.models.commands.RangeOptions.InfLexBound;
import glide.api.models.commands.RangeOptions.InfScoreBound;
Expand Down Expand Up @@ -3552,6 +3555,54 @@ public T bitop(
return getThis();
}

/**
* Pops one or more elements from the first non-empty list from the provided <code>keys
* </code>.
*
* @since Redis 7.0 and above.
* @see <a href="https://valkey.io/commands/lmpop/">valkey.io</a> for details.
* @param keys An array of keys to lists.
* @param direction The direction based on which elements are popped from - see {@link
* PopDirection}.
* @param count The maximum number of popped elements.
* @return Command Response - A <code>Map</code> of <code>key</code> name mapped arrays of popped
* elements.
*/
public T lmpop(@NonNull String[] keys, @NonNull PopDirection direction, @NonNull Long count) {
ArgsArray commandArgs =
buildArgs(
concatenateArrays(
new String[] {Long.toString(keys.length)},
keys,
new String[] {
direction.toString(), COUNT_FOR_LIST_REDIS_API, Long.toString(count)
}));
protobufTransaction.addCommands(buildCommand(LMPop, commandArgs));
return getThis();
}

/**
* Pops one element from the first non-empty list from the provided <code>keys</code>.
*
* @since Redis 7.0 and above.
* @see <a href="https://valkey.io/commands/lmpop/">valkey.io</a> for details.
* @param keys An array of keys to lists.
* @param direction The direction based on which elements are popped from - see {@link
* PopDirection}.
* @return Command Response - A <code>Map</code> of <code>key</code> name mapped array of the
* popped element.
*/
public T lmpop(@NonNull String[] keys, @NonNull PopDirection direction) {
ArgsArray commandArgs =
buildArgs(
concatenateArrays(
new String[] {Long.toString(keys.length)},
keys,
new String[] {direction.toString()}));
protobufTransaction.addCommands(buildCommand(LMPop, commandArgs));
return getThis();
}

/** Build protobuf {@link Command} object for given command and arguments. */
protected Command buildCommand(RequestType requestType) {
return buildCommand(requestType, buildArgs());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.models.commands;

import glide.api.commands.ListBaseCommands;

/**
* Enumeration representing element pop direction for the {@link ListBaseCommands#lmpop} command.
*/
public enum PopDirection {
/** Represents the option that elements should be popped from the left side of a list. */
LEFT,
/** Represents the option that elements should be popped from the right side of a list. */
RIGHT
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ public static <T, U extends T> U[][] castArrayofArrays(T[] outerObjectArr, Class
@SuppressWarnings("unchecked")
public static <T, U extends T> Map<String, U[]> castMapOfArrays(
Map<String, T[]> mapOfArrays, Class<U> clazz) {
if (mapOfArrays == null) {
return null;
}
return mapOfArrays.entrySet().stream()
.collect(Collectors.toMap(k -> k.getKey(), e -> castArray(e.getValue(), clazz)));
}
Expand Down
Loading

0 comments on commit d880b87

Please sign in to comment.