diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0a7a8960e..bd35fa2ff 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,6 +8,7 @@ on: env: CARGO_TERM_COLOR: always + REDIS_RS_REDIS_JSON_PATH: "/tmp/librejson.so" jobs: build: @@ -54,8 +55,38 @@ jobs: - uses: Swatinem/rust-cache@v1 - uses: actions/checkout@v2 + - name: Checkout RedisJSON + uses: actions/checkout@v2 + with: + repository: "RedisJSON/RedisJSON" + path: "./__ci/redis-json" + set-safe-directory: false + + # When cargo is invoked, it'll go up many directories to see if it can find a workspace + # This will avoid this issue in what is admittedly a bit of a janky but still fully functional way + # + # 1. Copy the untouched file (into Cargo.toml.actual) + # 2. Exclude ./__ci/redis-json from the workspace + # (preventing it from being compiled as a workspace module) + # 3. Build RedisJSON + # 4. Move the built RedisJSON Module (librejson.so) to /tmp + # 5. Restore Cargo.toml to its untouched state + # 6. Remove the RedisJSON Source code so it doesn't interfere with tests + # + # This shouldn't cause issues in the future so long as no profiles or patches + # are applied to the workspace Cargo.toml file + - name: Compile RedisJSON + run: | + cp ./Cargo.toml ./Cargo.toml.actual + echo $'\nexclude = [\"./__ci/redis-json\"]' >> Cargo.toml + cargo +stable build --release --manifest-path ./__ci/redis-json/Cargo.toml + mv ./__ci/redis-json/target/release/librejson.so /tmp/librejson.so + rm ./Cargo.toml; mv ./Cargo.toml.actual ./Cargo.toml + rm -rf ./__ci/redis-json + - name: Run tests run: make test + - name: Check features run: | cargo check --benches --all-features diff --git a/README.md b/README.md index 3c46570d0..681483982 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,44 @@ fn fetch_an_integer() -> String { } ``` +## JSON Support + +Support for the RedisJSON Module can be enabled by specifying "json" as a feature in your Cargo.toml. + +`redis = { version = "0.17.0", features = ["json"] }` + +Then you can simply import the `JsonCommands` trait which will add the `json` commands to all Redis Connections (not to be confused with just `Commands` which only adds the default commands) + +```rust +use redis::Client; +use redis::JsonCommands; +use redis::RedisResult; +use redis::ToRedisArgs; + +// Result returns Ok(true) if the value was set +// Result returns Err(e) if there was an error with the server itself OR serde_json was unable to serialize the boolean +fn set_json_bool(key: P, path: P, b: bool) -> RedisResult { + let client = Client::open("redis://127.0.0.1").unwrap(); + let connection = client.get_connection().unwrap(); + + // runs `JSON.SET {key} {path} {b}` + connection.json_set(key, path, b)? + + // you'll need to use serde_json (or some other json lib) to deserialize the results from the bytes + // It will always be a Vec, if no results were found at the path it'll be an empty Vec +} + +``` + ## Development +To test `redis` you're going to need to be able to test with the Redis Modules, to do this +you must set the following envornment variables before running the test script + +- `REDIS_RS_REDIS_JSON_PATH` = The absolute path to the RedisJSON module (Usually called `librejson.so`). + + + If you want to develop on the library there are a few commands provided by the makefile: diff --git a/redis/Cargo.toml b/redis/Cargo.toml index 62b63c99f..fac14a446 100644 --- a/redis/Cargo.toml +++ b/redis/Cargo.toml @@ -58,6 +58,10 @@ native-tls = { version = "0.2", optional = true } tokio-native-tls = { version = "0.3", optional = true } async-native-tls = { version = "0.4", optional = true } +# Only needed for RedisJSON Support +serde = { version = "1.0.82", optional = true } +serde_json = { version = "1.0.82", optional = true } + # Optional aHash support ahash = { version = "0.7.6", optional = true } @@ -66,6 +70,7 @@ default = ["acl", "streams", "geospatial", "script"] acl = [] aio = ["bytes", "pin-project-lite", "futures-util", "futures-util/alloc", "futures-util/sink", "tokio/io-util", "tokio-util", "tokio-util/codec", "tokio/sync", "combine/tokio", "async-trait"] geospatial = [] +json = ["serde", "serde_json"] cluster = ["crc16", "rand"] script = ["sha1_smol"] tls = ["native-tls"] @@ -104,6 +109,10 @@ required-features = ["aio"] [[test]] name = "test_acl" +[[test]] +name = "test_json" +required-features = ["json", "serde/derive"] + [[bench]] name = "bench_basic" harness = false diff --git a/redis/src/commands/json.rs b/redis/src/commands/json.rs new file mode 100644 index 000000000..2ee5f9a29 --- /dev/null +++ b/redis/src/commands/json.rs @@ -0,0 +1,373 @@ +// can't use rustfmt here because it screws up the file. +#![cfg_attr(rustfmt, rustfmt_skip)] +use crate::cmd::{cmd, Cmd}; +use crate::connection::ConnectionLike; +use crate::pipeline::Pipeline; +use crate::types::{FromRedisValue, RedisResult, ToRedisArgs}; +use crate::RedisError; + +#[cfg(feature = "cluster")] +use crate::commands::ClusterPipeline; + +use serde::ser::Serialize; + +macro_rules! implement_json_commands { + ( + $lifetime: lifetime + $( + $(#[$attr:meta])+ + fn $name:ident<$($tyargs:ident : $ty:ident),*>( + $($argname:ident: $argty:ty),*) $body:block + )* + ) => ( + + /// Implements RedisJSON commands for connection like objects. This + /// allows you to send commands straight to a connection or client. It + /// is also implemented for redis results of clients which makes for + /// very convenient access in some basic cases. + /// + /// This allows you to use nicer syntax for some common operations. + /// For instance this code: + /// + /// ```rust,no_run + /// # fn do_something() -> redis::RedisResult<()> { + /// let client = redis::Client::open("redis://127.0.0.1/")?; + /// let mut con = client.get_connection()?; + /// redis::cmd("SET").arg("my_key").arg(42).execute(&mut con); + /// assert_eq!(redis::cmd("GET").arg("my_key").query(&mut con), Ok(42)); + /// # Ok(()) } + /// ``` + /// + /// Will become this: + /// + /// ```rust,no_run + /// # fn do_something() -> redis::RedisResult<()> { + /// use redis::Commands; + /// let client = redis::Client::open("redis://127.0.0.1/")?; + /// let mut con = client.get_connection()?; + /// con.set("my_key", 42)?; + /// assert_eq!(con.get("my_key"), Ok(42)); + /// # Ok(()) } + /// ``` + pub trait JsonCommands : ConnectionLike + Sized { + $( + $(#[$attr])* + #[inline] + #[allow(clippy::extra_unused_lifetimes, clippy::needless_lifetimes)] + fn $name<$lifetime, $($tyargs: $ty, )* RV: FromRedisValue>( + &mut self $(, $argname: $argty)*) -> RedisResult + { Cmd::$name($($argname),*)?.query(self) } + )* + } + + impl Cmd { + $( + $(#[$attr])* + #[allow(clippy::extra_unused_lifetimes, clippy::needless_lifetimes)] + pub fn $name<$lifetime, $($tyargs: $ty),*>($($argname: $argty),*) -> RedisResult { + $body + } + )* + } + + /// Implements RedisJSON commands over asynchronous connections. This + /// allows you to send commands straight to a connection or client. + /// + /// This allows you to use nicer syntax for some common operations. + /// For instance this code: + /// + /// ```rust,no_run + /// use redis::JsonAsyncCommands; + /// # async fn do_something() -> redis::RedisResult<()> { + /// let client = redis::Client::open("redis://127.0.0.1/")?; + /// let mut con = client.get_async_connection().await?; + /// redis::cmd("SET").arg("my_key").arg(42i32).query_async(&mut con).await?; + /// assert_eq!(redis::cmd("GET").arg("my_key").query_async(&mut con).await, Ok(42i32)); + /// # Ok(()) } + /// ``` + /// + /// Will become this: + /// + /// ```rust,no_run + /// use redis::JsonAsyncCommands; + /// use serde_json::json; + /// # async fn do_something() -> redis::RedisResult<()> { + /// use redis::Commands; + /// let client = redis::Client::open("redis://127.0.0.1/")?; + /// let mut con = client.get_async_connection().await?; + /// con.json_set("my_key", "$", &json!({"item": 42i32})).await?; + /// assert_eq!(con.json_get("my_key", "$").await, Ok(String::from(r#"[{"item":42}]"#))); + /// # Ok(()) } + /// ``` + #[cfg(feature = "aio")] + pub trait JsonAsyncCommands : crate::aio::ConnectionLike + Send + Sized { + $( + $(#[$attr])* + #[inline] + #[allow(clippy::extra_unused_lifetimes, clippy::needless_lifetimes)] + fn $name<$lifetime, $($tyargs: $ty + Send + Sync + $lifetime,)* RV>( + & $lifetime mut self + $(, $argname: $argty)* + ) -> $crate::types::RedisFuture<'a, RV> + where + RV: FromRedisValue, + { + Box::pin(async move { + $body?.query_async(self).await + }) + } + )* + } + + /// Implements RedisJSON commands for pipelines. Unlike the regular + /// commands trait, this returns the pipeline rather than a result + /// directly. Other than that it works the same however. + impl Pipeline { + $( + $(#[$attr])* + #[inline] + #[allow(clippy::extra_unused_lifetimes, clippy::needless_lifetimes)] + pub fn $name<$lifetime, $($tyargs: $ty),*>( + &mut self $(, $argname: $argty)* + ) -> RedisResult<&mut Self> { + self.add_command($body?); + Ok(self) + } + )* + } + + /// Implements RedisJSON commands for cluster pipelines. Unlike the regular + /// commands trait, this returns the cluster pipeline rather than a result + /// directly. Other than that it works the same however. + #[cfg(feature = "cluster")] + impl ClusterPipeline { + $( + $(#[$attr])* + #[inline] + #[allow(clippy::extra_unused_lifetimes, clippy::needless_lifetimes)] + pub fn $name<$lifetime, $($tyargs: $ty),*>( + &mut self $(, $argname: $argty)* + ) -> RedisResult<&mut Self> { + self.add_command($body?); + Ok(self) + } + )* + } + + ) +} + +implement_json_commands! { + 'a + + /// Append the JSON `value` to the array at `path` after the last element in it. + fn json_arr_append(key: K, path: P, value: &'a V) { + let mut cmd = cmd("JSON.ARRAPPEND"); + + cmd.arg(key) + .arg(path) + .arg(serde_json::to_string(value)?); + + Ok::<_, RedisError>(cmd) + } + + /// Index array at `path`, returns first occurance of `value` + fn json_arr_index(key: K, path: P, value: &'a V) { + let mut cmd = cmd("JSON.ARRINDEX"); + + cmd.arg(key) + .arg(path) + .arg(serde_json::to_string(value)?); + + Ok::<_, RedisError>(cmd) + } + + /// Same as `json_arr_index` except takes a `start` and a `stop` value, setting these to `0` will mean + /// they make no effect on the query + /// + /// The default values for `start` and `stop` are `0`, so pass those in if you want them to take no effect + fn json_arr_index_ss(key: K, path: P, value: &'a V, start: &'a isize, stop: &'a isize) { + let mut cmd = cmd("JSON.ARRINDEX"); + + cmd.arg(key) + .arg(path) + .arg(serde_json::to_string(value)?) + .arg(start) + .arg(stop); + + Ok::<_, RedisError>(cmd) + } + + /// Inserts the JSON `value` in the array at `path` before the `index` (shifts to the right). + /// + /// `index` must be withing the array's range. + fn json_arr_insert(key: K, path: P, index: i64, value: &'a V) { + let mut cmd = cmd("JSON.ARRINSERT"); + + cmd.arg(key) + .arg(path) + .arg(index) + .arg(serde_json::to_string(value)?); + + Ok::<_, RedisError>(cmd) + + } + + /// Reports the length of the JSON Array at `path` in `key`. + fn json_arr_len(key: K, path: P) { + let mut cmd = cmd("JSON.ARRLEN"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Removes and returns an element from the `index` in the array. + /// + /// `index` defaults to `-1` (the end of the array). + fn json_arr_pop(key: K, path: P, index: i64) { + let mut cmd = cmd("JSON.ARRPOP"); + + cmd.arg(key) + .arg(path) + .arg(index); + + Ok::<_, RedisError>(cmd) + } + + /// Trims an array so that it contains only the specified inclusive range of elements. + /// + /// This command is extremely forgiving and using it with out-of-range indexes will not produce an error. + /// There are a few differences between how RedisJSON v2.0 and legacy versions handle out-of-range indexes. + fn json_arr_trim(key: K, path: P, start: i64, stop: i64) { + let mut cmd = cmd("JSON.ARRTRIM"); + + cmd.arg(key) + .arg(path) + .arg(start) + .arg(stop); + + Ok::<_, RedisError>(cmd) + } + + /// Clears container values (Arrays/Objects), and sets numeric values to 0. + fn json_clear(key: K, path: P) { + let mut cmd = cmd("JSON.CLEAR"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Deletes a value at `path`. + fn json_del(key: K, path: P) { + let mut cmd = cmd("JSON.DEL"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Gets JSON Value(s) at `path`. + /// + /// Runs `JSON.GET` is key is singular, `JSON.MGET` if there are multiple keys. + fn json_get(key: K, path: P) { + let mut cmd = cmd(if key.is_single_arg() { "JSON.GET" } else { "JSON.MGET" }); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Increments the number value stored at `path` by `number`. + fn json_num_incr_by(key: K, path: P, value: i64) { + let mut cmd = cmd("JSON.NUMINCRBY"); + + cmd.arg(key) + .arg(path) + .arg(value); + + Ok::<_, RedisError>(cmd) + } + + /// Returns the keys in the object that's referenced by `path`. + fn json_obj_keys(key: K, path: P) { + let mut cmd = cmd("JSON.OBJKEYS"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Reports the number of keys in the JSON Object at `path` in `key`. + fn json_obj_len(key: K, path: P) { + let mut cmd = cmd("JSON.OBJLEN"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Sets the JSON Value at `path` in `key`. + fn json_set(key: K, path: P, value: &'a V) { + let mut cmd = cmd("JSON.SET"); + + cmd.arg(key) + .arg(path) + .arg(serde_json::to_string(value)?); + + Ok::<_, RedisError>(cmd) + } + + /// Appends the `json-string` values to the string at `path`. + fn json_str_append(key: K, path: P, value: V) { + let mut cmd = cmd("JSON.STRAPPEND"); + + cmd.arg(key) + .arg(path) + .arg(value); + + Ok::<_, RedisError>(cmd) + } + + /// Reports the length of the JSON String at `path` in `key`. + fn json_str_len(key: K, path: P) { + let mut cmd = cmd("JSON.STRLEN"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Toggle a `boolean` value stored at `path`. + fn json_toggle(key: K, path: P) { + let mut cmd = cmd("JSON.TOGGLE"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } + + /// Reports the type of JSON value at `path`. + fn json_type(key: K, path: P) { + let mut cmd = cmd("JSON.TYPE"); + + cmd.arg(key) + .arg(path); + + Ok::<_, RedisError>(cmd) + } +} + +impl JsonCommands for T where T: ConnectionLike {} + +#[cfg(feature = "aio")] +impl JsonAsyncCommands for T where T: crate::aio::ConnectionLike + Send + Sized {} diff --git a/redis/src/commands/mod.rs b/redis/src/commands/mod.rs index 542c464ba..64bbdf82c 100644 --- a/redis/src/commands/mod.rs +++ b/redis/src/commands/mod.rs @@ -8,6 +8,16 @@ use crate::types::{FromRedisValue, NumericBehavior, RedisResult, ToRedisArgs, Re #[macro_use] mod macros; +#[cfg(feature = "json")] +#[cfg_attr(docsrs, doc(cfg(feature = "json")))] +mod json; + +#[cfg(feature = "json")] +pub use json::JsonCommands; + +#[cfg(all(feature = "json", feature = "aio"))] +pub use json::JsonAsyncCommands; + #[cfg(feature = "cluster")] use crate::cluster_pipeline::ClusterPipeline; diff --git a/redis/src/lib.rs b/redis/src/lib.rs index 635e4a530..09ab61df8 100644 --- a/redis/src/lib.rs +++ b/redis/src/lib.rs @@ -417,6 +417,12 @@ pub mod acl; #[cfg_attr(docsrs, doc(cfg(feature = "aio")))] pub mod aio; +#[cfg(feature = "json")] +pub use crate::commands::JsonCommands; + +#[cfg(all(feature = "json", feature = "aio"))] +pub use crate::commands::JsonAsyncCommands; + #[cfg(feature = "geospatial")] #[cfg_attr(docsrs, doc(cfg(feature = "geospatial")))] pub mod geo; diff --git a/redis/src/types.rs b/redis/src/types.rs index 5f99602b8..395764e6f 100644 --- a/redis/src/types.rs +++ b/redis/src/types.rs @@ -98,6 +98,10 @@ pub enum ErrorKind { ExtensionError, /// Attempt to write to a read-only server ReadOnly, + + #[cfg(feature = "json")] + /// Error Serializing a struct to JSON form + Serialize, } /// Internal low-level redis value enum. @@ -224,6 +228,17 @@ pub struct RedisError { repr: ErrorRepr, } +#[cfg(feature = "json")] +impl From for RedisError { + fn from(serde_err: serde_json::Error) -> RedisError { + RedisError::from(( + ErrorKind::Serialize, + "Serialization Error", + format!("{}", serde_err), + )) + } +} + #[derive(Debug)] enum ErrorRepr { WithDescription(ErrorKind, &'static str), @@ -421,6 +436,8 @@ impl RedisError { ErrorKind::ExtensionError => "extension error", ErrorKind::ClientError => "client error", ErrorKind::ReadOnly => "read-only", + #[cfg(feature = "json")] + ErrorKind::Serialize => "serializing", } } diff --git a/redis/tests/support/cluster.rs b/redis/tests/support/cluster.rs index a40ca49c2..6093980fb 100644 --- a/redis/tests/support/cluster.rs +++ b/redis/tests/support/cluster.rs @@ -11,6 +11,7 @@ use tempfile::TempDir; use crate::support::build_keys_and_certs_for_tls; +use super::Module; use super::RedisServer; const LOCALHOST: &str = "127.0.0.1"; @@ -62,6 +63,10 @@ impl RedisCluster { } pub fn new(nodes: u16, replicas: u16) -> RedisCluster { + RedisCluster::with_modules(nodes, replicas, &[]) + } + + pub fn with_modules(nodes: u16, replicas: u16, modules: &[Module]) -> RedisCluster { let mut servers = vec![]; let mut folders = vec![]; let mut addrs = vec![]; @@ -88,6 +93,7 @@ impl RedisCluster { servers.push(RedisServer::new_with_addr( ClusterType::build_addr(port), tls_paths.clone(), + modules, |cmd| { let tempdir = tempfile::Builder::new() .prefix("redis") diff --git a/redis/tests/support/mod.rs b/redis/tests/support/mod.rs index e870924f6..5d3a73ac9 100644 --- a/redis/tests/support/mod.rs +++ b/redis/tests/support/mod.rs @@ -45,6 +45,10 @@ enum ServerType { Unix, } +pub enum Module { + Json, +} + pub struct RedisServer { pub process: process::Child, tempdir: Option, @@ -70,6 +74,10 @@ impl ServerType { impl RedisServer { pub fn new() -> RedisServer { + RedisServer::with_modules(&[]) + } + + pub fn with_modules(modules: &[Module]) -> RedisServer { let server_type = ServerType::get_intended(); let addr = match server_type { ServerType::Tcp { tls } => { @@ -98,7 +106,7 @@ impl RedisServer { redis::ConnectionAddr::Unix(PathBuf::from(&path)) } }; - RedisServer::new_with_addr(addr, None, |cmd| { + RedisServer::new_with_addr(addr, None, modules, |cmd| { cmd.spawn() .unwrap_or_else(|err| panic!("Failed to run {:?}: {}", cmd, err)) }) @@ -107,9 +115,24 @@ impl RedisServer { pub fn new_with_addr process::Child>( addr: redis::ConnectionAddr, tls_paths: Option, + modules: &[Module], spawner: F, ) -> RedisServer { let mut redis_cmd = process::Command::new("redis-server"); + + // Load Redis Modules + for module in modules { + match module { + Module::Json => { + redis_cmd + .arg("--loadmodule") + .arg(env::var("REDIS_RS_REDIS_JSON_PATH").expect( + "Unable to find path to RedisJSON at REDIS_RS_REDIS_JSON_PATH, is it set?", + )); + } + }; + } + redis_cmd .stdout(process::Stdio::null()) .stderr(process::Stdio::null()); @@ -204,7 +227,11 @@ pub struct TestContext { impl TestContext { pub fn new() -> TestContext { - let server = RedisServer::new(); + TestContext::with_modules(&[]) + } + + pub fn with_modules(modules: &[Module]) -> TestContext { + let server = RedisServer::with_modules(modules); let client = redis::Client::open(redis::ConnectionInfo { addr: server.get_client_addr().clone(), diff --git a/redis/tests/test_json.rs b/redis/tests/test_json.rs new file mode 100644 index 000000000..09fed8979 --- /dev/null +++ b/redis/tests/test_json.rs @@ -0,0 +1,501 @@ +#![cfg(feature = "json")] + +use std::assert_eq; +use std::collections::HashMap; + +use redis::JsonCommands; + +use redis::{ + ErrorKind, RedisError, RedisResult, + Value::{self, *}, +}; + +use crate::support::*; +mod support; + +use serde::Serialize; +// adds json! macro for quick json generation on the fly. +use serde_json::{self, json}; + +const TEST_KEY: &str = "my_json"; + +#[test] +fn test_json_serialize_error() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + #[derive(Debug, Serialize)] + struct InvalidSerializedStruct { + // Maps in serde_json must have string-like keys + // so numbers and strings, anything else will cause the serialization to fail + // this is basically the only way to make a serialization fail at runtime + // since rust doesnt provide the necessary ability to enforce this + pub invalid_json: HashMap, + } + + let mut test_invalid_value: InvalidSerializedStruct = InvalidSerializedStruct { + invalid_json: HashMap::new(), + }; + + test_invalid_value.invalid_json.insert(true, 2i64); + + let set_invalid: RedisResult = con.json_set(TEST_KEY, "$", &test_invalid_value); + + assert_eq!( + set_invalid, + Err(RedisError::from(( + ErrorKind::Serialize, + "Serialization Error", + String::from("key must be string") + ))) + ); +} + +#[test] +fn test_json_arr_append() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":[1i64], "nested": {"a": [1i64, 2i64]}, "nested2": {"a": 42i64}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_append: RedisResult = con.json_arr_append(TEST_KEY, "$..a", &3i64); + + assert_eq!(json_append, Ok(Bulk(vec![Int(2i64), Int(3i64), Nil]))); +} + +#[test] +fn test_json_arr_index() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":[1i64, 2i64, 3i64, 2i64], "nested": {"a": [3i64, 4i64]}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_arrindex: RedisResult = con.json_arr_index(TEST_KEY, "$..a", &2i64); + + assert_eq!(json_arrindex, Ok(Bulk(vec![Int(1i64), Int(-1i64)]))); + + let update_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":[1i64, 2i64, 3i64, 2i64], "nested": {"a": false}}), + ); + + assert_eq!(update_initial, Ok(true)); + + let json_arrindex_2: RedisResult = con.json_arr_index_ss(TEST_KEY, "$..a", &2i64, 0, 0); + + assert_eq!(json_arrindex_2, Ok(Bulk(vec![Int(1i64), Nil]))); +} + +#[test] +fn test_json_arr_insert() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":[3i64], "nested": {"a": [3i64 ,4i64]}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_arrinsert: RedisResult = con.json_arr_insert(TEST_KEY, "$..a", 0, &1i64); + + assert_eq!(json_arrinsert, Ok(Bulk(vec![Int(2), Int(3)]))); + + let update_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":[1i64 ,2i64 ,3i64 ,2i64], "nested": {"a": false}}), + ); + + assert_eq!(update_initial, Ok(true)); + + let json_arrinsert_2: RedisResult = con.json_arr_insert(TEST_KEY, "$..a", 0, &1i64); + + assert_eq!(json_arrinsert_2, Ok(Bulk(vec![Int(5), Nil]))); +} + +#[test] +fn test_json_arr_len() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a": [3i64], "nested": {"a": [3i64, 4i64]}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_arrlen: RedisResult = con.json_arr_len(TEST_KEY, "$..a"); + + assert_eq!(json_arrlen, Ok(Bulk(vec![Int(1), Int(2)]))); + + let update_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a": [1i64, 2i64, 3i64, 2i64], "nested": {"a": false}}), + ); + + assert_eq!(update_initial, Ok(true)); + + let json_arrlen_2: RedisResult = con.json_arr_len(TEST_KEY, "$..a"); + + assert_eq!(json_arrlen_2, Ok(Bulk(vec![Int(4), Nil]))); +} + +#[test] +fn test_json_arr_pop() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a": [3i64], "nested": {"a": [3i64, 4i64]}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_arrpop: RedisResult = con.json_arr_pop(TEST_KEY, "$..a", -1); + + assert_eq!( + json_arrpop, + Ok(Bulk(vec![ + // convert string 3 to its ascii value as bytes + Data(Vec::from("3".as_bytes())), + Data(Vec::from("4".as_bytes())) + ])) + ); + + let update_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":["foo", "bar"], "nested": {"a": false}, "nested2": {"a":[]}}), + ); + + assert_eq!(update_initial, Ok(true)); + + let json_arrpop_2: RedisResult = con.json_arr_pop(TEST_KEY, "$..a", -1); + + assert_eq!( + json_arrpop_2, + Ok(Bulk(vec![Data(Vec::from("\"bar\"".as_bytes())), Nil, Nil])) + ); +} + +#[test] +fn test_json_arr_trim() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a": [], "nested": {"a": [1i64, 4u64]}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_arrtrim: RedisResult = con.json_arr_trim(TEST_KEY, "$..a", 1, 1); + + assert_eq!(json_arrtrim, Ok(Bulk(vec![Int(0), Int(1)]))); + + let update_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a": [1i64, 2i64, 3i64, 4i64], "nested": {"a": false}}), + ); + + assert_eq!(update_initial, Ok(true)); + + let json_arrtrim_2: RedisResult = con.json_arr_trim(TEST_KEY, "$..a", 1, 1); + + assert_eq!(json_arrtrim_2, Ok(Bulk(vec![Int(1), Nil]))); +} + +#[test] +fn test_json_clear() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set(TEST_KEY, "$", &json!({"obj": {"a": 1i64, "b": 2i64}, "arr": [1i64, 2i64, 3i64], "str": "foo", "bool": true, "int": 42i64, "float": std::f64::consts::PI})); + + assert_eq!(set_initial, Ok(true)); + + let json_clear: RedisResult = con.json_clear(TEST_KEY, "$.*"); + + assert_eq!(json_clear, Ok(4)); + + let checking_value: RedisResult = con.json_get(TEST_KEY, "$"); + + // float is set to 0 and serde_json serializes 0f64 to 0.0, which is a different string + assert_eq!( + checking_value, + // i found it changes the order? + // its not reallt a problem if you're just deserializing it anyway but still + // kinda weird + Ok("[{\"arr\":[],\"bool\":true,\"float\":0,\"int\":0,\"obj\":{},\"str\":\"foo\"}]".into()) + ); +} + +#[test] +fn test_json_del() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a": 1i64, "nested": {"a": 2i64, "b": 3i64}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_del: RedisResult = con.json_del(TEST_KEY, "$..a"); + + assert_eq!(json_del, Ok(2)); +} + +#[test] +fn test_json_get() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":2i64, "b": 3i64, "nested": {"a": 4i64, "b": null}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_get: RedisResult = con.json_get(TEST_KEY, "$..b"); + + assert_eq!(json_get, Ok("[3,null]".into())); + + let json_get_multi: RedisResult = con.json_get(TEST_KEY, "..a $..b"); + + assert_eq!(json_get_multi, Ok("2".into())); +} + +#[test] +fn test_json_mget() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial_a: RedisResult = con.json_set( + format!("{}-a", TEST_KEY), + "$", + &json!({"a":1i64, "b": 2i64, "nested": {"a": 3i64, "b": null}}), + ); + let set_initial_b: RedisResult = con.json_set( + format!("{}-b", TEST_KEY), + "$", + &json!({"a":4i64, "b": 5i64, "nested": {"a": 6i64, "b": null}}), + ); + + assert_eq!(set_initial_a, Ok(true)); + assert_eq!(set_initial_b, Ok(true)); + + let json_mget: RedisResult = con.json_mget( + vec![format!("{}-a", TEST_KEY), format!("{}-b", TEST_KEY)], + "$..a", + ); + + assert_eq!( + json_mget, + Ok(Bulk(vec![ + Data(Vec::from("[1,3]".as_bytes())), + Data(Vec::from("[4,6]".as_bytes())) + ])) + ); +} + +#[test] +fn test_json_numincrby() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":"b","b":[{"a":2i64}, {"a":5i64}, {"a":"c"}]}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_numincrby_a: RedisResult = con.json_numincrby(TEST_KEY, "$.a", 2); + + // cannot increment a string + assert_eq!(json_numincrby_a, Ok("[null]".into())); + + let json_numincrby_b: RedisResult = con.json_numincrby(TEST_KEY, "$..a", 2); + + // however numbers can be incremented + assert_eq!(json_numincrby_b, Ok("[null,4,7,null]".into())); +} + +#[test] +fn test_json_objkeys() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":[3i64], "nested": {"a": {"b":2i64, "c": 1i64}}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_objkeys: RedisResult = con.json_objkeys(TEST_KEY, "$..a"); + + assert_eq!( + json_objkeys, + Ok(Bulk(vec![ + Nil, + Bulk(vec![ + Data(Vec::from("b".as_bytes())), + Data(Vec::from("c".as_bytes())) + ]) + ])) + ); +} + +#[test] +fn test_json_objlen() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":[3i64], "nested": {"a": {"b":2i64, "c": 1i64}}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_objlen: RedisResult = con.json_objlen(TEST_KEY, "$..a"); + + assert_eq!(json_objlen, Ok(Bulk(vec![Nil, Int(2)]))); +} + +#[test] +fn test_json_set() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set: RedisResult = con.json_set(TEST_KEY, "$", &json!({"key": "value"})); + + assert_eq!(set, Ok(true)); +} + +#[test] +fn test_json_strappend() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31i64}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_strappend: RedisResult = con.json_strappend(TEST_KEY, "$..a", "\"baz\""); + + assert_eq!(json_strappend, Ok(Bulk(vec![Int(6), Int(8), Nil]))); + + let json_get_check: RedisResult = con.json_get(TEST_KEY, "$"); + + assert_eq!( + json_get_check, + Ok("[{\"a\":\"foobaz\",\"nested\":{\"a\":\"hellobaz\"},\"nested2\":{\"a\":31}}]".into()) + ); +} + +#[test] +fn test_json_strlen() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31i32}}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_strlen: RedisResult = con.json_strlen(TEST_KEY, "$..a"); + + assert_eq!(json_strlen, Ok(Bulk(vec![Int(3), Int(5), Nil]))); +} + +#[test] +fn test_json_toggle() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set(TEST_KEY, "$", &json!({"bool": true})); + + assert_eq!(set_initial, Ok(true)); + + let json_toggle_a: RedisResult = con.json_toggle(TEST_KEY, "$.bool"); + assert_eq!(json_toggle_a, Ok(Bulk(vec![Int(0)]))); + + let json_toggle_b: RedisResult = con.json_toggle(TEST_KEY, "$.bool"); + assert_eq!(json_toggle_b, Ok(Bulk(vec![Int(1)]))); +} + +#[test] +fn test_json_type() { + let ctx = TestContext::with_modules(&[Module::Json]); + let mut con = ctx.connection(); + + let set_initial: RedisResult = con.json_set( + TEST_KEY, + "$", + &json!({"a":2i64, "nested": {"a": true}, "foo": "bar"}), + ); + + assert_eq!(set_initial, Ok(true)); + + let json_type_a: RedisResult = con.json_type(TEST_KEY, "$..foo"); + + assert_eq!( + json_type_a, + Ok(Bulk(vec![Data(Vec::from("string".as_bytes()))])) + ); + + let json_type_b: RedisResult = con.json_type(TEST_KEY, "$..a"); + + assert_eq!( + json_type_b, + Ok(Bulk(vec![ + Data(Vec::from("integer".as_bytes())), + Data(Vec::from("boolean".as_bytes())) + ])) + ); + + let json_type_c: RedisResult = con.json_type(TEST_KEY, "$..dummy"); + + assert_eq!(json_type_c, Ok(Bulk(vec![]))); +}