Skip to content

Commit

Permalink
Python: adds JSON.TOGGLE command (valkey-io#1184)
Browse files Browse the repository at this point in the history
  • Loading branch information
shohamazon authored Mar 27, 2024
1 parent 7ed3361 commit faca726
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* Python, Node: Added ZRANK command ([#1065](https://github.com/aws/glide-for-redis/pull/1065), [#1149](https://github.com/aws/glide-for-redis/pull/1149))
* Core: Enabled Cluster Mode periodic checks by default ([#1089](https://github.com/aws/glide-for-redis/pull/1089))
* Node: Added Rename command. ([#1124](https://github.com/aws/glide-for-redis/pull/1124))
* Python: Added JSON.TOGGLE command ([#1184](https://github.com/aws/glide-for-redis/pull/1184))

#### Features

Expand Down
66 changes: 66 additions & 0 deletions glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub(crate) enum ExpectedReturnType {
Set,
DoubleOrNull,
ZrankReturnType,
JsonToggleReturnType,
}

pub(crate) fn convert_to_expected_type(
Expand Down Expand Up @@ -121,6 +122,43 @@ pub(crate) fn convert_to_expected_type(
ExpectedReturnType::BulkString => Ok(Value::BulkString(
from_owned_redis_value::<String>(value)?.into(),
)),
ExpectedReturnType::JsonToggleReturnType => match value {
Value::Array(array) => {
let converted_array: RedisResult<Vec<_>> = array
.into_iter()
.map(|item| match item {
Value::Nil => Ok(Value::Nil),
_ => match from_owned_redis_value::<bool>(item.clone()) {
Ok(boolean_value) => Ok(Value::Boolean(boolean_value)),
_ => Err((
ErrorKind::TypeError,
"Could not convert value to boolean",
format!("(value was {:?})", item),
)
.into()),
},
})
.collect();

converted_array.map(Value::Array)
}
Value::BulkString(bytes) => match std::str::from_utf8(&bytes) {
Ok("true") => Ok(Value::Boolean(true)),
Ok("false") => Ok(Value::Boolean(false)),
_ => Err((
ErrorKind::TypeError,
"Response couldn't be converted to boolean",
format!("(response was {:?})", bytes),
)
.into()),
},
_ => Err((
ErrorKind::TypeError,
"Response couldn't be converted to Json Toggle return type",
format!("(response was {:?})", value),
)
.into()),
},
}
}

Expand Down Expand Up @@ -204,6 +242,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
b"SMEMBERS" => Some(ExpectedReturnType::Set),
b"ZSCORE" => Some(ExpectedReturnType::DoubleOrNull),
b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble),
b"JSON.TOGGLE" => Some(ExpectedReturnType::JsonToggleReturnType),
_ => None,
}
}
Expand Down Expand Up @@ -423,4 +462,31 @@ mod tests {

assert!(convert_to_expected_type(Value::Nil, Some(ExpectedReturnType::Double)).is_err());
}

#[test]
fn test_convert_to_list_of_bool_or_null() {
let array = vec![Value::Nil, Value::Int(0), Value::Int(1)];
let array_result = convert_to_expected_type(
Value::Array(array),
Some(ExpectedReturnType::JsonToggleReturnType),
)
.unwrap();

let array_result = if let Value::Array(array) = array_result {
array
} else {
panic!("Expected an Array, but got {:?}", array_result);
};
assert_eq!(array_result.len(), 3);

assert_eq!(array_result[0], Value::Nil);
assert_eq!(array_result[1], Value::Boolean(false));
assert_eq!(array_result[2], Value::Boolean(true));

assert!(convert_to_expected_type(
Value::Nil,
Some(ExpectedReturnType::JsonToggleReturnType)
)
.is_err());
}
}
43 changes: 42 additions & 1 deletion python/python/glide/async_commands/redis_modules/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from typing import List, Optional, Union, cast

from glide.async_commands.core import ConditionalChange
from glide.constants import TOK
from glide.constants import TOK, TJsonResponse
from glide.protobuf.redis_request_pb2 import RequestType
from glide.redis_client import TRedisClient

Expand Down Expand Up @@ -137,3 +137,44 @@ async def get(
args.extend(paths)

return cast(str, await client.custom_command(args))


async def toggle(
client: TRedisClient,
key: str,
path: str,
) -> TJsonResponse[bool]:
"""
Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`.
See https://redis.io/commands/json.toggle/ for more details.
Args:
client (TRedisClient): The Redis client to execute the command.
key (str): The key of the JSON document.
path (str): The JSONPath to specify.
Returns:
TJsonResponse[bool]: For JSONPath (`path` starts with `$`), returns a list of boolean replies for every possible path, with the toggled boolean value,
or None for JSON values matching the path that are not boolean.
For legacy path (`path` doesn't starts with `$`), returns the value of the toggled boolean in `path`.
Note that when sending legacy path syntax, If `path` doesn't exist or the value at `path` isn't a boolean, an error is raised.
For more information about the returned type, see `TJsonResponse`.
Examples:
>>> from glide import json as redisJson
>>> import json
>>> await redisJson.set(client, "doc", "$", json.dumps({"bool": True, "nested": {"bool": False, "nested": {"bool": 10}}}))
'OK'
>>> await redisJson.toggle(client, "doc", "$.bool")
[False, True, None] # Indicates successful toggling of the Boolean values at path '$.bool' in the key stored at `doc`.
>>> await redisJson.toggle(client, "doc", "bool")
True # Indicates successful toggling of the Boolean value at path 'bool' in the key stored at `doc`.
>>> json.loads(await redisJson.get(client, "doc", "$"))
[{"bool": True, "nested": {"bool": True, "nested": {"bool": 10}}}] # The updated JSON value in the key stored at `doc`.
"""

return cast(
TJsonResponse[bool],
await client.custom_command(["JSON.TOGGLE", key, path]),
)
6 changes: 5 additions & 1 deletion python/python/glide/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0

from typing import Dict, List, Literal, Set, TypeVar, Union
from typing import Dict, List, Literal, Optional, Set, TypeVar, Union

from glide.protobuf.connection_request_pb2 import ConnectionRequest
from glide.protobuf.redis_request_pb2 import RedisRequest
Expand All @@ -27,3 +27,7 @@
# Otherwise, response will be : {Address : response , ... } with type of Dict[str, T].
TClusterResponse = Union[T, Dict[str, T]]
TSingleNodeRoute = Union[RandomNode, SlotKeyRoute, SlotIdRoute, ByAddressRoute]
# When specifying legacy path (path doesn't start with `$`), response will be T
# Otherwise, (when specifying JSONPath), response will be List[Optional[T]].
# For more information, see: https://redis.io/docs/data-types/json/path/ .
TJsonResponse = Union[T, List[Optional[T]]]
18 changes: 18 additions & 0 deletions python/python/tests/tests_redis_modules/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from glide.async_commands.redis_modules.json import JsonGetOptions
from glide.config import ProtocolVersion
from glide.constants import OK
from glide.exceptions import RequestError
from glide.redis_client import TRedisClient
from tests.test_async_client import get_random_string, parse_info_response

Expand Down Expand Up @@ -148,3 +149,20 @@ async def test_json_get_formatting(self, redis_client: TRedisClient):
'[\n~{\n~~"a":*1.0,\n~~"b":*2,\n~~"c":*{\n~~~"d":*3,\n~~~"e":*4\n~~}\n~}\n]'
)
assert result == expected_result

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_json_toggle(self, redis_client: TRedisClient):
key = get_random_string(10)
json_value = {"bool": True, "nested": {"bool": False, "nested": {"bool": 10}}}
assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK

assert await json.toggle(redis_client, key, "$..bool") == [False, True, None]
assert await json.toggle(redis_client, key, "bool") is True

assert await json.toggle(redis_client, key, "$.nested") == [None]
with pytest.raises(RequestError):
assert await json.toggle(redis_client, key, "nested")

with pytest.raises(RequestError):
assert await json.toggle(redis_client, "non_exiting_key", "$")

0 comments on commit faca726

Please sign in to comment.