Skip to content

Commit b0939e7

Browse files
author
Meir Shpilraien (Spielrein)
authored
Implements RedisValue derive proc macro. (#335)
`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 ```
1 parent 4306b65 commit b0939e7

File tree

7 files changed

+295
-3
lines changed

7 files changed

+295
-3
lines changed

examples/proc_macro_commands.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,62 @@
1+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2+
3+
use redis_module::RedisError;
14
use redis_module::{redis_module, Context, RedisResult, RedisString, RedisValue};
2-
use redis_module_macros::command;
5+
use redis_module_macros::{command, RedisValue};
6+
7+
#[derive(RedisValue)]
8+
struct RedisValueDeriveInner {
9+
i: i64,
10+
}
11+
12+
#[derive(RedisValue)]
13+
struct RedisValueDerive {
14+
i: i64,
15+
f: f64,
16+
s: String,
17+
u: usize,
18+
v: Vec<i64>,
19+
v2: Vec<RedisValueDeriveInner>,
20+
hash_map: HashMap<String, String>,
21+
hash_set: HashSet<String>,
22+
ordered_map: BTreeMap<String, RedisValueDeriveInner>,
23+
ordered_set: BTreeSet<String>,
24+
}
25+
26+
#[command(
27+
{
28+
flags: [ReadOnly, NoMandatoryKeys],
29+
arity: -1,
30+
key_spec: [
31+
{
32+
notes: "test redis value derive macro",
33+
flags: [ReadOnly, Access],
34+
begin_search: Index({ index : 0 }),
35+
find_keys: Range({ last_key : 0, steps : 0, limit : 0 }),
36+
}
37+
]
38+
}
39+
)]
40+
fn redis_value_derive(
41+
_ctx: &Context,
42+
_args: Vec<RedisString>,
43+
) -> Result<RedisValueDerive, RedisError> {
44+
Ok(RedisValueDerive {
45+
i: 10,
46+
f: 1.1,
47+
s: "s".to_owned(),
48+
u: 20,
49+
v: vec![1, 2, 3],
50+
v2: vec![
51+
RedisValueDeriveInner { i: 1 },
52+
RedisValueDeriveInner { i: 2 },
53+
],
54+
hash_map: HashMap::from([("key".to_owned(), "val".to_owned())]),
55+
hash_set: HashSet::from(["key".to_owned()]),
56+
ordered_map: BTreeMap::from([("key".to_owned(), RedisValueDeriveInner { i: 10 })]),
57+
ordered_set: BTreeSet::from(["key".to_owned()]),
58+
})
59+
}
360

461
#[command(
562
{

redismodule-rs-macros/src/command.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ pub(crate) fn redis_command(attr: TokenStream, item: TokenStream) -> TokenStream
344344

345345
let args = redis_module::decode_args(ctx, argv, argc);
346346
let response = #original_function_name(&context, args);
347-
context.reply(response) as i32
347+
context.reply(response.map(|v| v.into())) as i32
348348
}
349349

350350
#[linkme::distributed_slice(redis_module::commands::COMMNADS_LIST)]

redismodule-rs-macros/src/lib.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use quote::quote;
33
use syn::ItemFn;
44

55
mod command;
6+
mod redis_value;
67

78
/// This proc macro allow to specify that the follow function is a Redis command.
89
/// The macro accept the following arguments that discribe the command properties:
@@ -134,3 +135,91 @@ pub fn module_changed_event_handler(_attr: TokenStream, item: TokenStream) -> To
134135
};
135136
gen.into()
136137
}
138+
139+
/// The macro auto generate a [From] implementation that can convert the struct into [RedisValue].
140+
///
141+
/// Example:
142+
///
143+
/// ```rust,no_run,ignore
144+
/// #[derive(RedisValue)]
145+
/// struct RedisValueDeriveInner {
146+
/// i: i64,
147+
/// }
148+
///
149+
/// #[derive(RedisValue)]
150+
/// struct RedisValueDerive {
151+
/// i: i64,
152+
/// f: f64,
153+
/// s: String,
154+
/// u: usize,
155+
/// v: Vec<i64>,
156+
/// v2: Vec<RedisValueDeriveInner>,
157+
/// hash_map: HashMap<String, String>,
158+
/// hash_set: HashSet<String>,
159+
/// ordered_map: BTreeMap<String, RedisValueDeriveInner>,
160+
/// ordered_set: BTreeSet<String>,
161+
/// }
162+
///
163+
/// #[command(
164+
/// {
165+
/// flags: [ReadOnly, NoMandatoryKeys],
166+
/// arity: -1,
167+
/// key_spec: [
168+
/// {
169+
/// notes: "test redis value derive macro",
170+
/// flags: [ReadOnly, Access],
171+
/// begin_search: Index({ index : 0 }),
172+
/// find_keys: Range({ last_key : 0, steps : 0, limit : 0 }),
173+
/// }
174+
/// ]
175+
/// }
176+
/// )]
177+
/// fn redis_value_derive(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
178+
/// Ok(RedisValueDerive {
179+
/// i: 10,
180+
/// f: 1.1,
181+
/// s: "s".to_owned(),
182+
/// u: 20,
183+
/// v: vec![1, 2, 3],
184+
/// v2: vec![
185+
/// RedisValueDeriveInner { i: 1 },
186+
/// RedisValueDeriveInner { i: 2 },
187+
/// ],
188+
/// hash_map: HashMap::from([("key".to_owned(), "val`".to_owned())]),
189+
/// hash_set: HashSet::from(["key".to_owned()]),
190+
/// ordered_map: BTreeMap::from([("key".to_owned(), RedisValueDeriveInner { i: 10 })]),
191+
/// ordered_set: BTreeSet::from(["key".to_owned()]),
192+
/// }
193+
/// .into())
194+
/// }
195+
/// ```
196+
///
197+
/// The [From] implementation generates a [RedisValue::OrderMap] such that the fields names
198+
/// are the map keys and the values are the result of running [Into] function on the field
199+
/// value and convert it into a [RedisValue].
200+
///
201+
/// The code above will generate the following reply (in resp3):
202+
///
203+
/// ```bash
204+
/// 127.0.0.1:6379> redis_value_derive
205+
/// 1# "f" => (double) 1.1
206+
/// 2# "hash_map" => 1# "key" => "val"
207+
/// 3# "hash_set" => 1~ "key"
208+
/// 4# "i" => (integer) 10
209+
/// 5# "ordered_map" => 1# "key" => 1# "i" => (integer) 10
210+
/// 6# "ordered_set" => 1~ "key"
211+
/// 7# "s" => "s"
212+
/// 8# "u" => (integer) 20
213+
/// 9# "v" =>
214+
/// 1) (integer) 1
215+
/// 2) (integer) 2
216+
/// 3) (integer) 3
217+
/// 10# "v2" =>
218+
/// 1) 1# "i" => (integer) 1
219+
/// 2) 1# "i" => (integer) 2
220+
/// ```
221+
///
222+
#[proc_macro_derive(RedisValue)]
223+
pub fn redis_value(item: TokenStream) -> TokenStream {
224+
redis_value::redis_value(item)
225+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use proc_macro::TokenStream;
2+
use quote::quote;
3+
use syn::{parse_macro_input, Data, DeriveInput, Fields};
4+
5+
pub fn redis_value(item: TokenStream) -> TokenStream {
6+
let struct_input: DeriveInput = parse_macro_input!(item);
7+
let struct_data = match struct_input.data {
8+
Data::Struct(s) => s,
9+
_ => {
10+
return quote! {compile_error!("RedisValue derive can only be apply on struct.")}.into()
11+
}
12+
};
13+
14+
let struct_name = struct_input.ident;
15+
16+
let fields = match struct_data.fields {
17+
Fields::Named(f) => f,
18+
_ => {
19+
return quote! {compile_error!("RedisValue derive can only be apply on struct with named fields.")}.into()
20+
}
21+
};
22+
23+
let fields = fields
24+
.named
25+
.into_iter()
26+
.map(|v| {
27+
let name = v.ident.ok_or("Field without a name is not supported.")?;
28+
Ok(name)
29+
})
30+
.collect::<Result<Vec<_>, &str>>();
31+
32+
let fields = match fields {
33+
Ok(f) => f,
34+
Err(e) => return quote! {compile_error!(#e)}.into(),
35+
};
36+
37+
let fields_names: Vec<_> = fields.iter().map(|v| v.to_string()).collect();
38+
39+
let res = quote! {
40+
impl From<#struct_name> for redis_module::redisvalue::RedisValue {
41+
fn from(val: #struct_name) -> redis_module::redisvalue::RedisValue {
42+
redis_module::redisvalue::RedisValue::OrderedMap(std::collections::BTreeMap::from([
43+
#((
44+
redis_module::redisvalue::RedisValueKey::String(#fields_names.to_owned()),
45+
val.#fields.into()
46+
), )*
47+
]))
48+
}
49+
}
50+
};
51+
res.into()
52+
}

src/macros.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ macro_rules! redis_command {
2020

2121
let args = $crate::decode_args(ctx, argv, argc);
2222
let response = $command_handler(&context, args);
23-
context.reply(response) as c_int
23+
context.reply(response.map(|v| v.into())) as c_int
2424
}
2525
/////////////////////
2626

src/redisvalue.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,48 @@ impl TryFrom<RedisValue> for String {
5252
}
5353
}
5454

55+
impl From<String> for RedisValueKey {
56+
fn from(s: String) -> Self {
57+
Self::String(s)
58+
}
59+
}
60+
61+
impl From<i64> for RedisValueKey {
62+
fn from(i: i64) -> Self {
63+
Self::Integer(i)
64+
}
65+
}
66+
67+
impl From<RedisString> for RedisValueKey {
68+
fn from(rs: RedisString) -> Self {
69+
Self::BulkRedisString(rs)
70+
}
71+
}
72+
73+
impl From<Vec<u8>> for RedisValueKey {
74+
fn from(s: Vec<u8>) -> Self {
75+
Self::BulkString(s)
76+
}
77+
}
78+
79+
impl From<&str> for RedisValueKey {
80+
fn from(s: &str) -> Self {
81+
s.to_owned().into()
82+
}
83+
}
84+
85+
impl From<&String> for RedisValueKey {
86+
fn from(s: &String) -> Self {
87+
s.clone().into()
88+
}
89+
}
90+
91+
impl From<bool> for RedisValueKey {
92+
fn from(b: bool) -> Self {
93+
Self::Bool(b)
94+
}
95+
}
96+
5597
impl From<()> for RedisValue {
5698
fn from(_: ()) -> Self {
5799
Self::Null
@@ -124,6 +166,40 @@ impl<T: Into<Self>> From<Vec<T>> for RedisValue {
124166
}
125167
}
126168

169+
impl<K: Into<RedisValueKey>, V: Into<RedisValue>> From<HashMap<K, V>> for RedisValue {
170+
fn from(items: HashMap<K, V>) -> Self {
171+
Self::Map(
172+
items
173+
.into_iter()
174+
.map(|(k, v)| (k.into(), v.into()))
175+
.collect(),
176+
)
177+
}
178+
}
179+
180+
impl<K: Into<RedisValueKey>, V: Into<RedisValue>> From<BTreeMap<K, V>> for RedisValue {
181+
fn from(items: BTreeMap<K, V>) -> Self {
182+
Self::OrderedMap(
183+
items
184+
.into_iter()
185+
.map(|(k, v)| (k.into(), v.into()))
186+
.collect(),
187+
)
188+
}
189+
}
190+
191+
impl<K: Into<RedisValueKey>> From<HashSet<K>> for RedisValue {
192+
fn from(items: HashSet<K>) -> Self {
193+
Self::Set(items.into_iter().map(Into::into).collect())
194+
}
195+
}
196+
197+
impl<K: Into<RedisValueKey>> From<BTreeSet<K>> for RedisValue {
198+
fn from(items: BTreeSet<K>) -> Self {
199+
Self::OrderedSet(items.into_iter().map(Into::into).collect())
200+
}
201+
}
202+
127203
impl<'root> TryFrom<&CallReply<'root>> for RedisValueKey {
128204
type Error = RedisError;
129205
fn try_from(reply: &CallReply<'root>) -> Result<Self, Self::Error> {

tests/integration.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::utils::{get_redis_connection, start_redis_server_with_module};
22
use anyhow::Context;
33
use anyhow::Result;
4+
use redis::Value;
45
use redis::{RedisError, RedisResult};
56

67
mod utils;
@@ -538,3 +539,20 @@ fn test_command_proc_macro() -> Result<()> {
538539

539540
Ok(())
540541
}
542+
543+
#[test]
544+
fn test_redis_value_derive() -> Result<()> {
545+
let port: u16 = 6497;
546+
let _guards = vec![start_redis_server_with_module("proc_macro_commands", port)
547+
.with_context(|| "failed to start redis server")?];
548+
let mut con =
549+
get_redis_connection(port).with_context(|| "failed to connect to redis server")?;
550+
551+
let res: Value = redis::cmd("redis_value_derive")
552+
.query(&mut con)
553+
.with_context(|| "failed to run string.set")?;
554+
555+
assert_eq!(res.as_sequence().unwrap().len(), 20);
556+
557+
Ok(())
558+
}

0 commit comments

Comments
 (0)