Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: Add FUNCTION LIST command. #1452

Merged
merged 15 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 241 additions & 1 deletion glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use redis::{
cluster_routing::Routable, from_owned_redis_value, Cmd, ErrorKind, RedisResult, Value,
};

#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is required for debug logging. I decided to keep it.

pub(crate) enum ExpectedReturnType {
Map,
MapOfStringToDouble,
Expand All @@ -20,6 +20,9 @@ pub(crate) enum ExpectedReturnType {
ArrayOfDoubleOrNull,
Lolwut,
ArrayOfArraysOfDoubleOrNull,
ArrayOfMapsRecursive,
ArrayOfMaps,
StringOrSet,
ArrayOfPairs,
ArrayOfMemberScorePairs,
ZMPopReturnType,
Expand Down Expand Up @@ -346,6 +349,66 @@ pub(crate) fn convert_to_expected_type(
)
.into()),
},
// `FUNCTION LIST` returns an array of maps with nested list of maps.
// In RESP2 these maps are represented by arrays - we're going to convert them.
/* RESP2 response
1) 1) "library_name"
2) "mylib"
3) "engine"
4) "LUA"
5) "functions"
6) 1) 1) "name"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a set?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flags is a set

2) "myfunc2"
3) "description"
4) (nil)
5) "flags"
6) (empty array)
2) 1) "name"
...
2) 1) "library_name"
...

RESP3 response
1) 1# "library_name" => "mylib"
2# "engine" => "LUA"
3# "functions" =>
1) 1# "name" => "myfunc2"
2# "description" => (nil)
3# "flags" => (empty set)
2) 1# "name" => "myfunc"
...
2) 1# "library_name" => "mylib1"
...
*/
ExpectedReturnType::ArrayOfMapsRecursive => match value {
// empty array, or it is already contains a map (RESP3 response) - no conversion needed
Value::Array(ref array) if array.is_empty() || matches!(array[0], Value::Map(_)) => {
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
Ok(value)
}
Value::Array(array) => {
convert_array_of_flat_maps(array, Some(ExpectedReturnType::ArrayOfMaps))
}
_ => Err((ErrorKind::TypeError, "Response couldn't be converted").into()),
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
},
// Not used for a command, but used as a helper for `ArrayOfMapsRecursive` to process the inner map.
ExpectedReturnType::ArrayOfMaps => match value {
Value::Array(array) => {
convert_array_of_flat_maps(array, Some(ExpectedReturnType::StringOrSet))
}
// Due to recursion, it would be called to convert every map value, including simple strings, do nothing with them
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
Value::BulkString(_) | Value::SimpleString(_) | Value::VerbatimString { .. } => {
Ok(value)
}
_ => Err((ErrorKind::TypeError, "Response couldn't be converted").into()),
},
// Not used for a command, but used as a helper for `ArrayOfMaps` to process the inner map.
// It may contain a string (name, description) or set (flags), or nil (description).
// The set is stored as array. See comment for `ArrayOfMapsRecursive`.
ExpectedReturnType::StringOrSet => match value {
Value::Nil => Ok(value),
Value::Array(_) => convert_to_expected_type(value, Some(ExpectedReturnType::Set)),
_ => convert_to_expected_type(value, Some(ExpectedReturnType::BulkString)),
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
},
}
}

Expand Down Expand Up @@ -378,6 +441,48 @@ fn convert_array_elements(
Ok(Value::Array(converted_array))
}

/// Converts an array of flatten maps into an array of maps.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
/// Input:
/// ```text
/// 1) 1) "map 1 key 1"
/// 2) "map 1 value 1"
/// 3) "map 1 key 2"
/// 4) "map 1 value 2"
/// ...
/// 1) 1) "map 2 key 1"
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
/// 2) "map 2 value 1"
/// ...
/// ```
/// Output:
/// ```text
/// 1) 1# "map 1 key 1" => "map 1 value 1"
/// 2# "map 1 key 2" => "map 1 value 2"
/// ...
/// 2) 1# "map 2 key 1" => "map 2 value 1"
/// ...
/// ```
///
/// `array` is an input.
/// `value_expected_return_type` is the type of the values stored in the maps to convert to.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
fn convert_array_of_flat_maps(
array: Vec<Value>,
value_expected_return_type: Option<ExpectedReturnType>,
) -> RedisResult<Value> {
let mut result: Vec<Value> = Vec::with_capacity(array.len());
for entry in array {
let Value::Array(entry_as_array) = entry else {
return Err((ErrorKind::TypeError, "Incorrect value type received").into());
};
let map = convert_array_to_map(
entry_as_array,
Some(ExpectedReturnType::BulkString),
value_expected_return_type,
)?;
result.push(map);
}
Ok(Value::Array(result))
}

fn convert_array_to_map(
array: Vec<Value>,
key_expected_return_type: Option<ExpectedReturnType>,
Expand Down Expand Up @@ -532,6 +637,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
}
}
b"LOLWUT" => Some(ExpectedReturnType::Lolwut),
b"FUNCTION LIST" => Some(ExpectedReturnType::ArrayOfMapsRecursive),
_ => None,
}
}
Expand All @@ -540,6 +646,140 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
mod tests {
use super::*;

#[test]
fn convert_function_list() {
assert!(matches!(
expected_type_for_cmd(redis::cmd("FUNCTION").arg("LIST")),
Some(ExpectedReturnType::ArrayOfMapsRecursive)
));

let resp2_response = Value::Array(vec![
Value::Array(vec![
Value::BulkString("library_name".to_string().into_bytes()),
Value::BulkString("mylib1".to_string().into_bytes()),
Value::BulkString("engine".to_string().into_bytes()),
Value::BulkString("LUA".to_string().into_bytes()),
Value::BulkString("functions".to_string().into_bytes()),
Value::Array(vec![
Value::Array(vec![
Value::BulkString("name".to_string().into_bytes()),
Value::BulkString("myfunc1".to_string().into_bytes()),
Value::BulkString("description".to_string().into_bytes()),
Value::Nil,
Value::BulkString("flags".to_string().into_bytes()),
Value::Array(vec![
Value::BulkString("read".to_string().into_bytes()),
Value::BulkString("write".to_string().into_bytes()),
]),
]),
Value::Array(vec![
Value::BulkString("name".to_string().into_bytes()),
Value::BulkString("myfunc2".to_string().into_bytes()),
Value::BulkString("description".to_string().into_bytes()),
Value::BulkString("blahblah".to_string().into_bytes()),
Value::BulkString("flags".to_string().into_bytes()),
Value::Array(vec![]),
]),
]),
]),
Value::Array(vec![
Value::BulkString("library_name".to_string().into_bytes()),
Value::BulkString("mylib2".to_string().into_bytes()),
Value::BulkString("engine".to_string().into_bytes()),
Value::BulkString("LUA".to_string().into_bytes()),
Value::BulkString("functions".to_string().into_bytes()),
Value::Array(vec![]),
Value::BulkString("library_code".to_string().into_bytes()),
Value::BulkString("<code>".to_string().into_bytes()),
]),
]);

let resp3_response = Value::Array(vec![
Value::Map(vec![
(
Value::BulkString("library_name".to_string().into_bytes()),
Value::BulkString("mylib1".to_string().into_bytes()),
),
(
Value::BulkString("engine".to_string().into_bytes()),
Value::BulkString("LUA".to_string().into_bytes()),
),
(
Value::BulkString("functions".to_string().into_bytes()),
Value::Array(vec![
Value::Map(vec![
(
Value::BulkString("name".to_string().into_bytes()),
Value::BulkString("myfunc1".to_string().into_bytes()),
),
(
Value::BulkString("description".to_string().into_bytes()),
Value::Nil,
),
(
Value::BulkString("flags".to_string().into_bytes()),
Value::Set(vec![
Value::BulkString("read".to_string().into_bytes()),
Value::BulkString("write".to_string().into_bytes()),
]),
),
]),
Value::Map(vec![
(
Value::BulkString("name".to_string().into_bytes()),
Value::BulkString("myfunc2".to_string().into_bytes()),
),
(
Value::BulkString("description".to_string().into_bytes()),
Value::BulkString("blahblah".to_string().into_bytes()),
),
(
Value::BulkString("flags".to_string().into_bytes()),
Value::Set(vec![]),
),
]),
]),
),
]),
Value::Map(vec![
(
Value::BulkString("library_name".to_string().into_bytes()),
Value::BulkString("mylib2".to_string().into_bytes()),
),
(
Value::BulkString("engine".to_string().into_bytes()),
Value::BulkString("LUA".to_string().into_bytes()),
),
(
Value::BulkString("functions".to_string().into_bytes()),
Value::Array(vec![]),
),
(
Value::BulkString("library_code".to_string().into_bytes()),
Value::BulkString("<code>".to_string().into_bytes()),
),
]),
]);

assert_eq!(
convert_to_expected_type(
resp2_response.clone(),
Some(ExpectedReturnType::ArrayOfMapsRecursive)
)
.unwrap(),
resp3_response.clone()
);

assert_eq!(
convert_to_expected_type(
resp3_response.clone(),
Some(ExpectedReturnType::ArrayOfMapsRecursive)
)
.unwrap(),
resp3_response.clone()
);
}

#[test]
fn convert_lolwut() {
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 @@ -187,6 +187,7 @@ enum RequestType {
GetBit = 145;
ZInter = 146;
FunctionLoad = 150;
FunctionList = 151;
}

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 @@ -157,6 +157,7 @@ pub enum RequestType {
GetBit = 145,
ZInter = 146,
FunctionLoad = 150,
FunctionList = 151,
}

fn get_two_word_command(first: &str, second: &str) -> Cmd {
Expand Down Expand Up @@ -317,6 +318,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType {
ProtobufRequestType::GetBit => RequestType::GetBit,
ProtobufRequestType::ZInter => RequestType::ZInter,
ProtobufRequestType::FunctionLoad => RequestType::FunctionLoad,
ProtobufRequestType::FunctionList => RequestType::FunctionList,
}
}
}
Expand Down Expand Up @@ -473,6 +475,7 @@ impl RequestType {
RequestType::GetBit => Some(cmd("GETBIT")),
RequestType::ZInter => Some(cmd("ZINTER")),
RequestType::FunctionLoad => Some(get_two_word_command("FUNCTION", "LOAD")),
RequestType::FunctionList => Some(get_two_word_command("FUNCTION", "LIST")),
}
}
}
40 changes: 40 additions & 0 deletions java/client/src/main/java/glide/api/RedisClient.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api;

import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API;
import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API;
import static glide.utils.ArrayTransformUtils.castArray;
import static glide.utils.ArrayTransformUtils.concatenateArrays;
import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray;
Expand All @@ -13,6 +15,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand;
import static redis_request.RedisRequestOuterClass.RequestType.Echo;
import static redis_request.RedisRequestOuterClass.RequestType.FlushAll;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionList;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad;
import static redis_request.RedisRequestOuterClass.RequestType.Info;
import static redis_request.RedisRequestOuterClass.RequestType.LastSave;
Expand Down Expand Up @@ -203,4 +206,41 @@ public CompletableFuture<String> functionLoadReplace(@NonNull String libraryCode
new String[] {FunctionLoadOptions.REPLACE.toString(), libraryCode},
this::handleStringResponse);
}

@Override
@SuppressWarnings("unchecked")
public CompletableFuture<Map<String, Object>[]> functionListWithCode(
@NonNull String libNamePattern) {
return commandManager.submitNewCommand(
FunctionList,
new String[] {LIBRARY_NAME_REDIS_API, libNamePattern, WITH_CODE_REDIS_API},
response -> castArray(handleArrayResponse(response), Map.class));
}

@Override
@SuppressWarnings("unchecked")
public CompletableFuture<Map<String, Object>[]> functionList(@NonNull String libNamePattern) {
return commandManager.submitNewCommand(
FunctionList,
new String[] {LIBRARY_NAME_REDIS_API, libNamePattern},
response -> castArray(handleArrayResponse(response), Map.class));
}

@Override
@SuppressWarnings("unchecked")
public CompletableFuture<Map<String, Object>[]> functionListWithCode() {
return commandManager.submitNewCommand(
FunctionList,
new String[] {WITH_CODE_REDIS_API},
response -> castArray(handleArrayResponse(response), Map.class));
}

@Override
@SuppressWarnings("unchecked")
public CompletableFuture<Map<String, Object>[]> functionList() {
return commandManager.submitNewCommand(
FunctionList,
new String[0],
response -> castArray(handleArrayResponse(response), Map.class));
}
}
Loading
Loading