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

The PR introduce a new proc macro for command registrations. #326

Merged
merged 13 commits into from
May 15, 2023
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ crate-type = ["cdylib"]
name = "configuration"
crate-type = ["cdylib"]

[[example]]
name = "proc_macro_commands"
crate-type = ["cdylib"]

[[example]]
name = "acl"
crate-type = ["cdylib"]
Expand Down
67 changes: 67 additions & 0 deletions examples/proc_macro_commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use redis_module::{redis_module, Context, RedisResult, RedisString, RedisValue};
use redis_module_macros::redis_command;

#[redis_command(
iddm marked this conversation as resolved.
Show resolved Hide resolved
{
name: "classic_keys",
iddm marked this conversation as resolved.
Show resolved Hide resolved
flags: "readonly",
iddm marked this conversation as resolved.
Show resolved Hide resolved
arity: -2,
iddm marked this conversation as resolved.
Show resolved Hide resolved
key_spec: [
iddm marked this conversation as resolved.
Show resolved Hide resolved
{
notes: "test command that define all the arguments at even possition as keys",
flags: ["RO", "ACCESS"],
iddm marked this conversation as resolved.
Show resolved Hide resolved
begin_search: Index(1),
find_keys: Range((-1, 2, 0)),
}
]
}
)]
fn classic_keys(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
iddm marked this conversation as resolved.
Show resolved Hide resolved
Ok(RedisValue::SimpleStringStatic("OK"))
}

#[redis_command(
{
name: "keyword_keys",
flags: "readonly",
arity: -2,
key_spec: [
{
notes: "test command that define all the arguments at even possition as keys",
flags: ["RO", "ACCESS"],
begin_search: Keyword(("foo", 1)),
find_keys: Range((-1, 2, 0)),
}
]
}
)]
fn keyword_keys(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
Ok(RedisValue::SimpleStringStatic("OK"))
}

#[redis_command(
{
name: "num_keys",
flags: "readonly no-mandatory-keys",
arity: -2,
key_spec: [
{
notes: "test command that define all the arguments at even possition as keys",
flags: ["RO", "ACCESS"],
begin_search: Index(1),
find_keys: Keynum((0, 1, 1)),
}
]
}
)]
fn num_keys(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
Ok(RedisValue::SimpleStringStatic("OK"))
}

redis_module! {
name: "server_events",
version: 1,
allocator: (redis_module::alloc::RedisAlloc, redis_module::alloc::RedisAlloc),
data_types: [],
commands: [],
}
3 changes: 3 additions & 0 deletions redismodule-rs-macros-internals/src/api_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ lazy_static::lazy_static! {
("RedisModule_BlockClientSetPrivateData".to_string(), 70200),
("RedisModule_BlockClientOnAuth".to_string(), 70200),
("RedisModule_ACLAddLogEntryByUserName".to_string(), 70200),
("RedisModule_GetCommand".to_string(), 70000),
("RedisModule_SetCommandInfo".to_string(), 70000),

]);

pub(crate) static ref API_OLDEST_VERSION: usize = 60000;
Expand Down
3 changes: 3 additions & 0 deletions redismodule-rs-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ categories = ["database", "api-bindings"]
[dependencies]
syn = { version="1.0", features = ["full"]}
quote = "1.0"
proc-macro2 = "1"
serde = { version = "1", features = ["derive"] }
serde_syn = "0.1.0"

[lib]
name = "redis_module_macros"
Expand Down
80 changes: 80 additions & 0 deletions redismodule-rs-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,86 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::ItemFn;

mod redis_command;

/// 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:
/// * name - The command name,
/// * flags (optional) - Command flags such as `readonly`, for the full list please refer to https://redis.io/docs/reference/modules/modules-api-ref/#redismodule_createcommand
/// * summary (optional) - Command summary
/// * complexity (optional) - Command compexity
/// * since (optional) - At which module version the command was first introduce
/// * tips (optional) - Command tips for proxy, for more information please refer to https://redis.io/topics/command-tips
/// * arity - Number of arguments, including the command name itself. A positive number specifies an exact number of arguments and a negative number
/// specifies a minimum number of arguments.
/// * key_spec - A list of specs representing how to find the keys that the command might touch. the following options are available:
/// * notes (optional) - Some note about the key spec.
/// * flags - List of flags reprenting how the keys are accessed, the following options are available:
/// * RO - Read-Only. Reads the value of the key, but doesn't necessarily return it.
/// * RW - Read-Write. Modifies the data stored in the value of the key or its metadata.
/// * OW - Overwrite. Overwrites the data stored in the value of the key.
/// * RM - Deletes the key.
/// * ACCESS - Returns, copies or uses the user data from the value of the key.
/// * UPDATE - Updates data to the value, new value may depend on the old value.
/// * INSERT - Adds data to the value with no chance of modification or deletion of existing data.
/// * DELETE - Explicitly deletes some content from the value of the key.
/// * NOT_KEY - The key is not actually a key, but should be routed in cluster mode as if it was a key.
/// * INCOMPLETE - The keyspec might not point out all the keys it should cover.
/// * VARIABLE_FLAGS - Some keys might have different flags depending on arguments.
/// * begin_search - Represents how Redis should start looking for keys.
/// There are 2 possible options:
/// * Index - start looking for keys from a given position.
/// * Keyword - Search for a specific keyward and start looking for keys from this keyword
/// * FindKeys - After Redis finds the location from where it needs to start looking for keys,
/// Redis will start finding keys base on the information in this struct.
/// There are 2 possible options:
/// * Range - A tuple represent a range of `(last_key, steps, limit)`.
/// * last_key - Index of the last key relative to the result of the
/// begin search step. Can be negative, in which case it's not
/// relative. -1 indicates the last argument, -2 one before the
/// last and so on.
/// * steps - How many arguments should we skip after finding a
/// key, in order to find the next one.
/// * limit - If `lastkey` is -1, we use `limit` to stop the search
/// by a factor. 0 and 1 mean no limit. 2 means 1/2 of the
/// remaining args, 3 means 1/3, and so on.
/// * Keynum - A tuple of 3 elements `(keynumidx, firstkey, keystep)`.
/// * keynumidx - Index of the argument containing the number of
/// keys to come, relative to the result of the begin search step.
/// * firstkey - Index of the fist key relative to the result of the
/// begin search step. (Usually it's just after `keynumidx`, in
/// which case it should be set to `keynumidx + 1`.)
/// * keystep - How many arguments should we skip after finding a
/// key, in order to find the next one?
///
/// Example:
/// The following example will register a command called `foo`.
/// ```rust,no_run,ignore
/// #[redis_command(
/// {
/// name: "foo",
/// arity: 3,
/// key_spec: [
/// {
/// notes: "some notes",
/// flags: ["RW", "ACCESS"],
/// begin_search: Keyword(("foo", 1)),
/// find_keys: Range((1, 2, 3)),
/// }
/// ]
/// }
/// )]
/// fn test_command(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
/// Ok(RedisValue::SimpleStringStatic("OK"))
/// }
/// ```
///
/// **Notice**, by default Redis does not validate the command spec. User should validate the command keys on the module command code. The command spec is used for validation on cluster so Redis can raise a cross slot error when needed.
#[proc_macro_attribute]
pub fn redis_command(attr: TokenStream, item: TokenStream) -> TokenStream {
redis_command::redis_command(attr, item)
}

#[proc_macro_attribute]
pub fn role_changed_event_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast: ItemFn = match syn::parse(item) {
Expand Down
178 changes: 178 additions & 0 deletions redismodule-rs-macros/src/redis_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use serde::Deserialize;
use serde_syn::{config, from_stream};
use syn::{
parse,
parse::{Parse, ParseStream},
parse_macro_input, ItemFn,
};

#[derive(Debug, Deserialize)]
pub enum FindKeys {
Range((i32, i32, i32)), // (last_key, steps, limit)
Keynum((i32, i32, i32)), // (keynumidx, firstkey, keystep)
}

#[derive(Debug, Deserialize)]
pub enum BeginSearch {
Index(i32),
Keyword((String, i32)), // (keyword, startfrom)
}

#[derive(Debug, Deserialize)]
pub struct KeySpecArg {
notes: Option<String>,
flags: Vec<String>,
begin_search: BeginSearch,
find_keys: FindKeys,
}

#[derive(Debug, Deserialize)]
struct Args {
name: String,
flags: Option<String>,
summary: Option<String>,
complexity: Option<String>,
since: Option<String>,
tips: Option<String>,
arity: i64,
key_spec: Vec<KeySpecArg>,
}

impl Parse for Args {
fn parse(input: ParseStream) -> parse::Result<Self> {
from_stream(config::JSONY, &input)
}
}

fn to_token_stream(s: Option<String>) -> proc_macro2::TokenStream {
s.map(|v| quote! {Some(#v.to_owned())})
.unwrap_or(quote! {None})
}

pub(crate) fn redis_command(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as Args);
let func: ItemFn = match syn::parse(item) {
Ok(res) => res,
Err(e) => return e.to_compile_error().into(),
};

let original_function_name = func.sig.ident.clone();

let c_function_name = Ident::new(
&format!("_inner_{}", func.sig.ident.to_string()),
func.sig.ident.span(),
);

let get_command_info_function_name = Ident::new(
&format!("_inner_get_command_info_{}", func.sig.ident.to_string()),
func.sig.ident.span(),
);

let name_literal = args.name;
let flags_literal = to_token_stream(args.flags);
let summary_literal = to_token_stream(args.summary);
let complexity_literal = to_token_stream(args.complexity);
let since_literal = to_token_stream(args.since);
let tips_literal = to_token_stream(args.tips);
let arity_literal = args.arity;
let key_spec_notes: Vec<_> = args
.key_spec
.iter()
.map(|v| {
v.notes
.as_ref()
.map(|v| quote! {Some(#v.to_owned())})
.unwrap_or(quote! {None})
})
.collect();

let key_spec_flags: Vec<_> = args
.key_spec
.iter()
.map(|v| {
let flags = &v.flags;
quote! {
vec![#(redis_module::commnads::KeySpecFlags::try_from(#flags)?, )*]
}
})
.collect();

let key_spec_begin_search: Vec<_> = args
.key_spec
.iter()
.map(|v| match &v.begin_search {
BeginSearch::Index(i) => {
quote! {
redis_module::commnads::BeginSearch::Index(#i)
}
}
BeginSearch::Keyword((k, i)) => {
quote! {
redis_module::commnads::BeginSearch::Keyword((#k.to_owned(), #i))
}
}
})
.collect();

let key_spec_find_keys: Vec<_> = args
.key_spec
.iter()
.map(|v| match &v.find_keys {
FindKeys::Keynum((keynumidx, firstkey, keystep)) => {
quote! {
redis_module::commnads::FindKeys::Keynum((#keynumidx, #firstkey, #keystep))
}
}
FindKeys::Range((last_key, steps, limit)) => {
quote! {
redis_module::commnads::FindKeys::Range((#last_key, #steps, #limit))
}
}
})
.collect();

let gen = quote! {
#func

extern "C" fn #c_function_name(
ctx: *mut redis_module::raw::RedisModuleCtx,
argv: *mut *mut redis_module::raw::RedisModuleString,
argc: i32,
) -> i32 {
let context = redis_module::Context::new(ctx);

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

#[linkme::distributed_slice(redis_module::commnads::COMMNADS_LIST)]
fn #get_command_info_function_name() -> Result<redis_module::commnads::CommandInfo, redis_module::RedisError> {
let key_spec = vec![
#(
redis_module::commnads::KeySpec::new(
#key_spec_notes,
#key_spec_flags.into(),
#key_spec_begin_search,
#key_spec_find_keys,
),
)*
];
Ok(redis_module::commnads::CommandInfo::new(
#name_literal.to_owned(),
#flags_literal,
#summary_literal,
#complexity_literal,
#since_literal,
#tips_literal,
#arity_literal,
key_spec,
#c_function_name,
))
}
};
gen.into()
}
Loading