Skip to content

Commit

Permalink
Implements RedisValue derive proc macro. (#335)
Browse files Browse the repository at this point in the history
`RedisValue` derive proc macro allows to automatically generate the `From` trait of a given struct and convert it to `RedisValue`.

Example:

```rust
#[derive(RedisValue)]
struct RedisValueDeriveInner {
    i: i64,
}

#[derive(RedisValue)]
struct RedisValueDerive {
    i: i64,
    f: f64,
    s: String,
    u: usize,
    v: Vec<i64>,
    v2: Vec<RedisValueDeriveInner>,
    hash_map: HashMap<String, String>,
    hash_set: HashSet<String>,
    ordered_map: BTreeMap<String, RedisValueDeriveInner>,
    ordered_set: BTreeSet<String>,
}

#[command(
    {
        flags: [ReadOnly, NoMandatoryKeys],
        arity: -1,
        key_spec: [
            {
                notes: "test redis value derive macro",
                flags: [ReadOnly, Access],
                begin_search: Index({ index : 0 }),
                find_keys: Range({ last_key : 0, steps : 0, limit : 0 }),
            }
        ]
    }
)]
fn redis_value_derive(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
    Ok(RedisValueDerive {
        i: 10,
        f: 1.1,
        s: "s".to_owned(),
        u: 20,
        v: vec![1, 2, 3],
        v2: vec![
            RedisValueDeriveInner { i: 1 },
            RedisValueDeriveInner { i: 2 },
        ],
        hash_map: HashMap::from([("key".to_owned(), "val`".to_owned())]),
        hash_set: HashSet::from(["key".to_owned()]),
        ordered_map: BTreeMap::from([("key".to_owned(), RedisValueDeriveInner { i: 10 })]),
        ordered_set: BTreeSet::from(["key".to_owned()]),
    }
    .into())
}
```
The `From` implementation generates a `RedisValue::OrderMap` such that the fields names are the map keys and the values are the result of running `Into` function on the field value and convert it into a `RedisValue`.
The code above will generate the following reply (in resp3):

```
127.0.0.1:6379> redis_value_derive
1# "f" => (double) 1.1
2# "hash_map" => 1# "key" => "val"
3# "hash_set" => 1~ "key"
4# "i" => (integer) 10
5# "ordered_map" => 1# "key" => 1# "i" => (integer) 10
6# "ordered_set" => 1~ "key"
7# "s" => "s"
8# "u" => (integer) 20
9# "v" =>
   1) (integer) 1
   2) (integer) 2
   3) (integer) 3
10# "v2" =>
   1) 1# "i" => (integer) 1
   2) 1# "i" => (integer) 2
```
  • Loading branch information
MeirShpilraien authored May 22, 2023
1 parent 4306b65 commit b0939e7
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 3 deletions.
59 changes: 58 additions & 1 deletion examples/proc_macro_commands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

use redis_module::RedisError;
use redis_module::{redis_module, Context, RedisResult, RedisString, RedisValue};
use redis_module_macros::command;
use redis_module_macros::{command, RedisValue};

#[derive(RedisValue)]
struct RedisValueDeriveInner {
i: i64,
}

#[derive(RedisValue)]
struct RedisValueDerive {
i: i64,
f: f64,
s: String,
u: usize,
v: Vec<i64>,
v2: Vec<RedisValueDeriveInner>,
hash_map: HashMap<String, String>,
hash_set: HashSet<String>,
ordered_map: BTreeMap<String, RedisValueDeriveInner>,
ordered_set: BTreeSet<String>,
}

#[command(
{
flags: [ReadOnly, NoMandatoryKeys],
arity: -1,
key_spec: [
{
notes: "test redis value derive macro",
flags: [ReadOnly, Access],
begin_search: Index({ index : 0 }),
find_keys: Range({ last_key : 0, steps : 0, limit : 0 }),
}
]
}
)]
fn redis_value_derive(
_ctx: &Context,
_args: Vec<RedisString>,
) -> Result<RedisValueDerive, RedisError> {
Ok(RedisValueDerive {
i: 10,
f: 1.1,
s: "s".to_owned(),
u: 20,
v: vec![1, 2, 3],
v2: vec![
RedisValueDeriveInner { i: 1 },
RedisValueDeriveInner { i: 2 },
],
hash_map: HashMap::from([("key".to_owned(), "val".to_owned())]),
hash_set: HashSet::from(["key".to_owned()]),
ordered_map: BTreeMap::from([("key".to_owned(), RedisValueDeriveInner { i: 10 })]),
ordered_set: BTreeSet::from(["key".to_owned()]),
})
}

#[command(
{
Expand Down
2 changes: 1 addition & 1 deletion redismodule-rs-macros/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ pub(crate) fn redis_command(attr: TokenStream, item: TokenStream) -> TokenStream

let args = redis_module::decode_args(ctx, argv, argc);
let response = #original_function_name(&context, args);
context.reply(response) as i32
context.reply(response.map(|v| v.into())) as i32
}

#[linkme::distributed_slice(redis_module::commands::COMMNADS_LIST)]
Expand Down
89 changes: 89 additions & 0 deletions redismodule-rs-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use quote::quote;
use syn::ItemFn;

mod command;
mod redis_value;

/// This proc macro allow to specify that the follow function is a Redis command.
/// The macro accept the following arguments that discribe the command properties:
Expand Down Expand Up @@ -134,3 +135,91 @@ pub fn module_changed_event_handler(_attr: TokenStream, item: TokenStream) -> To
};
gen.into()
}

/// The macro auto generate a [From] implementation that can convert the struct into [RedisValue].
///
/// Example:
///
/// ```rust,no_run,ignore
/// #[derive(RedisValue)]
/// struct RedisValueDeriveInner {
/// i: i64,
/// }
///
/// #[derive(RedisValue)]
/// struct RedisValueDerive {
/// i: i64,
/// f: f64,
/// s: String,
/// u: usize,
/// v: Vec<i64>,
/// v2: Vec<RedisValueDeriveInner>,
/// hash_map: HashMap<String, String>,
/// hash_set: HashSet<String>,
/// ordered_map: BTreeMap<String, RedisValueDeriveInner>,
/// ordered_set: BTreeSet<String>,
/// }
///
/// #[command(
/// {
/// flags: [ReadOnly, NoMandatoryKeys],
/// arity: -1,
/// key_spec: [
/// {
/// notes: "test redis value derive macro",
/// flags: [ReadOnly, Access],
/// begin_search: Index({ index : 0 }),
/// find_keys: Range({ last_key : 0, steps : 0, limit : 0 }),
/// }
/// ]
/// }
/// )]
/// fn redis_value_derive(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
/// Ok(RedisValueDerive {
/// i: 10,
/// f: 1.1,
/// s: "s".to_owned(),
/// u: 20,
/// v: vec![1, 2, 3],
/// v2: vec![
/// RedisValueDeriveInner { i: 1 },
/// RedisValueDeriveInner { i: 2 },
/// ],
/// hash_map: HashMap::from([("key".to_owned(), "val`".to_owned())]),
/// hash_set: HashSet::from(["key".to_owned()]),
/// ordered_map: BTreeMap::from([("key".to_owned(), RedisValueDeriveInner { i: 10 })]),
/// ordered_set: BTreeSet::from(["key".to_owned()]),
/// }
/// .into())
/// }
/// ```
///
/// The [From] implementation generates a [RedisValue::OrderMap] such that the fields names
/// are the map keys and the values are the result of running [Into] function on the field
/// value and convert it into a [RedisValue].
///
/// The code above will generate the following reply (in resp3):
///
/// ```bash
/// 127.0.0.1:6379> redis_value_derive
/// 1# "f" => (double) 1.1
/// 2# "hash_map" => 1# "key" => "val"
/// 3# "hash_set" => 1~ "key"
/// 4# "i" => (integer) 10
/// 5# "ordered_map" => 1# "key" => 1# "i" => (integer) 10
/// 6# "ordered_set" => 1~ "key"
/// 7# "s" => "s"
/// 8# "u" => (integer) 20
/// 9# "v" =>
/// 1) (integer) 1
/// 2) (integer) 2
/// 3) (integer) 3
/// 10# "v2" =>
/// 1) 1# "i" => (integer) 1
/// 2) 1# "i" => (integer) 2
/// ```
///
#[proc_macro_derive(RedisValue)]
pub fn redis_value(item: TokenStream) -> TokenStream {
redis_value::redis_value(item)
}
52 changes: 52 additions & 0 deletions redismodule-rs-macros/src/redis_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

pub fn redis_value(item: TokenStream) -> TokenStream {
let struct_input: DeriveInput = parse_macro_input!(item);
let struct_data = match struct_input.data {
Data::Struct(s) => s,
_ => {
return quote! {compile_error!("RedisValue derive can only be apply on struct.")}.into()
}
};

let struct_name = struct_input.ident;

let fields = match struct_data.fields {
Fields::Named(f) => f,
_ => {
return quote! {compile_error!("RedisValue derive can only be apply on struct with named fields.")}.into()
}
};

let fields = fields
.named
.into_iter()
.map(|v| {
let name = v.ident.ok_or("Field without a name is not supported.")?;
Ok(name)
})
.collect::<Result<Vec<_>, &str>>();

let fields = match fields {
Ok(f) => f,
Err(e) => return quote! {compile_error!(#e)}.into(),
};

let fields_names: Vec<_> = fields.iter().map(|v| v.to_string()).collect();

let res = quote! {
impl From<#struct_name> for redis_module::redisvalue::RedisValue {
fn from(val: #struct_name) -> redis_module::redisvalue::RedisValue {
redis_module::redisvalue::RedisValue::OrderedMap(std::collections::BTreeMap::from([
#((
redis_module::redisvalue::RedisValueKey::String(#fields_names.to_owned()),
val.#fields.into()
), )*
]))
}
}
};
res.into()
}
2 changes: 1 addition & 1 deletion src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ macro_rules! redis_command {

let args = $crate::decode_args(ctx, argv, argc);
let response = $command_handler(&context, args);
context.reply(response) as c_int
context.reply(response.map(|v| v.into())) as c_int
}
/////////////////////

Expand Down
76 changes: 76 additions & 0 deletions src/redisvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,48 @@ impl TryFrom<RedisValue> for String {
}
}

impl From<String> for RedisValueKey {
fn from(s: String) -> Self {
Self::String(s)
}
}

impl From<i64> for RedisValueKey {
fn from(i: i64) -> Self {
Self::Integer(i)
}
}

impl From<RedisString> for RedisValueKey {
fn from(rs: RedisString) -> Self {
Self::BulkRedisString(rs)
}
}

impl From<Vec<u8>> for RedisValueKey {
fn from(s: Vec<u8>) -> Self {
Self::BulkString(s)
}
}

impl From<&str> for RedisValueKey {
fn from(s: &str) -> Self {
s.to_owned().into()
}
}

impl From<&String> for RedisValueKey {
fn from(s: &String) -> Self {
s.clone().into()
}
}

impl From<bool> for RedisValueKey {
fn from(b: bool) -> Self {
Self::Bool(b)
}
}

impl From<()> for RedisValue {
fn from(_: ()) -> Self {
Self::Null
Expand Down Expand Up @@ -124,6 +166,40 @@ impl<T: Into<Self>> From<Vec<T>> for RedisValue {
}
}

impl<K: Into<RedisValueKey>, V: Into<RedisValue>> From<HashMap<K, V>> for RedisValue {
fn from(items: HashMap<K, V>) -> Self {
Self::Map(
items
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}

impl<K: Into<RedisValueKey>, V: Into<RedisValue>> From<BTreeMap<K, V>> for RedisValue {
fn from(items: BTreeMap<K, V>) -> Self {
Self::OrderedMap(
items
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}

impl<K: Into<RedisValueKey>> From<HashSet<K>> for RedisValue {
fn from(items: HashSet<K>) -> Self {
Self::Set(items.into_iter().map(Into::into).collect())
}
}

impl<K: Into<RedisValueKey>> From<BTreeSet<K>> for RedisValue {
fn from(items: BTreeSet<K>) -> Self {
Self::OrderedSet(items.into_iter().map(Into::into).collect())
}
}

impl<'root> TryFrom<&CallReply<'root>> for RedisValueKey {
type Error = RedisError;
fn try_from(reply: &CallReply<'root>) -> Result<Self, Self::Error> {
Expand Down
18 changes: 18 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::utils::{get_redis_connection, start_redis_server_with_module};
use anyhow::Context;
use anyhow::Result;
use redis::Value;
use redis::{RedisError, RedisResult};

mod utils;
Expand Down Expand Up @@ -538,3 +539,20 @@ fn test_command_proc_macro() -> Result<()> {

Ok(())
}

#[test]
fn test_redis_value_derive() -> Result<()> {
let port: u16 = 6497;
let _guards = vec![start_redis_server_with_module("proc_macro_commands", port)
.with_context(|| "failed to start redis server")?];
let mut con =
get_redis_connection(port).with_context(|| "failed to connect to redis server")?;

let res: Value = redis::cmd("redis_value_derive")
.query(&mut con)
.with_context(|| "failed to run string.set")?;

assert_eq!(res.as_sequence().unwrap().len(), 20);

Ok(())
}

0 comments on commit b0939e7

Please sign in to comment.