diff --git a/Cargo.toml b/Cargo.toml index 546c012f..bfb1a9e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/examples/proc_macro_commands.rs b/examples/proc_macro_commands.rs new file mode 100644 index 00000000..50e86902 --- /dev/null +++ b/examples/proc_macro_commands.rs @@ -0,0 +1,66 @@ +use redis_module::{redis_module, Context, RedisResult, RedisString, RedisValue}; +use redis_module_macros::command; + +#[command( + { + flags: [ReadOnly], + arity: -2, + key_spec: [ + { + notes: "test command that define all the arguments at even possition as keys", + flags: [ReadOnly, Access], + begin_search: Index({ index : 1 }), + find_keys: Range({ last_key :- 1, steps : 2, limit : 0 }), + } + ] + } +)] +fn classic_keys(_ctx: &Context, _args: Vec) -> RedisResult { + Ok(RedisValue::SimpleStringStatic("OK")) +} + +#[command( + { + name: "keyword_keys", + flags: [ReadOnly], + arity: -2, + key_spec: [ + { + notes: "test command that define all the arguments at even possition as keys", + flags: [ReadOnly, Access], + begin_search: Keyword({ keyword : "foo", startfrom : 1 }), + find_keys: Range({ last_key :- 1, steps : 2, limit : 0 }), + } + ] + } +)] +fn keyword_keys(_ctx: &Context, _args: Vec) -> RedisResult { + Ok(RedisValue::SimpleStringStatic("OK")) +} + +#[command( + { + name: "num_keys", + flags: [ReadOnly, NoMandatoryKeys], + arity: -2, + key_spec: [ + { + notes: "test command that define all the arguments at even possition as keys", + flags: [ReadOnly, Access], + begin_search: Index({ index : 1 }), + find_keys: Keynum({ key_num_idx : 0, first_key : 1, key_step : 1 }), + } + ] + } +)] +fn num_keys(_ctx: &Context, _args: Vec) -> RedisResult { + Ok(RedisValue::SimpleStringStatic("OK")) +} + +redis_module! { + name: "server_events", + version: 1, + allocator: (redis_module::alloc::RedisAlloc, redis_module::alloc::RedisAlloc), + data_types: [], + commands: [], +} diff --git a/redismodule-rs-macros-internals/src/api_versions.rs b/redismodule-rs-macros-internals/src/api_versions.rs index 9f02059d..0faf3f55 100644 --- a/redismodule-rs-macros-internals/src/api_versions.rs +++ b/redismodule-rs-macros-internals/src/api_versions.rs @@ -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; diff --git a/redismodule-rs-macros-internals/src/lib.rs b/redismodule-rs-macros-internals/src/lib.rs index 771af114..befb6833 100644 --- a/redismodule-rs-macros-internals/src/lib.rs +++ b/redismodule-rs-macros-internals/src/lib.rs @@ -63,7 +63,7 @@ impl Parse for Args { /// ); /// ``` #[proc_macro] -pub fn redismodule_api(item: TokenStream) -> TokenStream { +pub fn api(item: TokenStream) -> TokenStream { let args = parse_macro_input!(item as Args); let minimum_require_version = args.requested_apis diff --git a/redismodule-rs-macros/Cargo.toml b/redismodule-rs-macros/Cargo.toml index 7f9354e9..08eb9293 100644 --- a/redismodule-rs-macros/Cargo.toml +++ b/redismodule-rs-macros/Cargo.toml @@ -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" diff --git a/redismodule-rs-macros/src/command.rs b/redismodule-rs-macros/src/command.rs new file mode 100644 index 00000000..f6e8584e --- /dev/null +++ b/redismodule-rs-macros/src/command.rs @@ -0,0 +1,376 @@ +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 RedisCommandFlags { + /// The command may modify the data set (it may also read from it). + Write, + + /// The command returns data from keys but never writes. + ReadOnly, + + /// The command is an administrative command (may change replication or perform similar tasks). + Admin, + + /// The command may use additional memory and should be denied during out of memory conditions. + DenyOOM, + + /// Don't allow this command in Lua scripts. + DenyScript, + + /// Allow this command while the server is loading data. Only commands not interacting with the data set + /// should be allowed to run in this mode. If not sure don't use this flag. + AllowLoading, + + /// The command publishes things on Pub/Sub channels. + PubSub, + + /// The command may have different outputs even starting from the same input arguments and key values. + /// Starting from Redis 7.0 this flag has been deprecated. Declaring a command as "random" can be done using + /// command tips, see https://redis.io/topics/command-tips. + Random, + + /// The command is allowed to run on slaves that don't serve stale data. Don't use if you don't know what + /// this means. + AllowStale, + + /// Don't propagate the command on monitor. Use this if the command has sensitive data among the arguments. + NoMonitor, + + /// Don't log this command in the slowlog. Use this if the command has sensitive data among the arguments. + NoSlowlog, + + /// The command time complexity is not greater than O(log(N)) where N is the size of the collection or + /// anything else representing the normal scalability issue with the command. + Fast, + + /// The command implements the interface to return the arguments that are keys. Used when start/stop/step + /// is not enough because of the command syntax. + GetkeysApi, + + /// The command should not register in Redis Cluster since is not designed to work with it because, for + /// example, is unable to report the position of the keys, programmatically creates key names, or any + /// other reason. + NoCluster, + + /// This command can be run by an un-authenticated client. Normally this is used by a command that is used + /// to authenticate a client. + NoAuth, + + /// This command may generate replication traffic, even though it's not a write command. + MayReplicate, + + /// All the keys this command may take are optional + NoMandatoryKeys, + + /// The command has the potential to block the client. + Blocking, + + /// Permit the command while the server is blocked either by a script or by a slow module command, see + /// RM_Yield. + AllowBusy, + + /// The command implements the interface to return the arguments that are channels. + GetchannelsApi, +} + +impl From<&RedisCommandFlags> for &'static str { + fn from(value: &RedisCommandFlags) -> Self { + match value { + RedisCommandFlags::Write => "write", + RedisCommandFlags::ReadOnly => "readonly", + RedisCommandFlags::Admin => "admin", + RedisCommandFlags::DenyOOM => "deny-oom", + RedisCommandFlags::DenyScript => "deny-script", + RedisCommandFlags::AllowLoading => "allow-loading", + RedisCommandFlags::PubSub => "pubsub", + RedisCommandFlags::Random => "random", + RedisCommandFlags::AllowStale => "allow-stale", + RedisCommandFlags::NoMonitor => "no-monitor", + RedisCommandFlags::NoSlowlog => "no-slowlog", + RedisCommandFlags::Fast => "fast", + RedisCommandFlags::GetkeysApi => "getkeys-api", + RedisCommandFlags::NoCluster => "no-cluster", + RedisCommandFlags::NoAuth => "no-auth", + RedisCommandFlags::MayReplicate => "may-replicate", + RedisCommandFlags::NoMandatoryKeys => "no-mandatory-keys", + RedisCommandFlags::Blocking => "blocking", + RedisCommandFlags::AllowBusy => "allow-busy", + RedisCommandFlags::GetchannelsApi => "getchannels-api", + } + } +} + +#[derive(Debug, Deserialize)] +pub enum RedisCommandKeySpecFlags { + /// Read-Only. Reads the value of the key, but doesn't necessarily return it. + ReadOnly, + + /// Read-Write. Modifies the data stored in the value of the key or its metadata. + ReadWrite, + + /// Overwrite. Overwrites the data stored in the value of the key. + Overwrite, + + /// Deletes the key. + Remove, + + /// Returns, copies or uses the user data from the value of the key. + Access, + + /// Updates data to the value, new value may depend on the old value. + Update, + + /// Adds data to the value with no chance of modification or deletion of existing data. + Insert, + + /// Explicitly deletes some content from the value of the key. + Delete, + + /// The key is not actually a key, but should be routed in cluster mode as if it was a key. + NotKey, + + /// The keyspec might not point out all the keys it should cover. + Incomplete, + + /// Some keys might have different flags depending on arguments. + VariableFlags, +} + +impl From<&RedisCommandKeySpecFlags> for &'static str { + fn from(value: &RedisCommandKeySpecFlags) -> Self { + match value { + RedisCommandKeySpecFlags::ReadOnly => "READ_ONLY", + RedisCommandKeySpecFlags::ReadWrite => "READ_WRITE", + RedisCommandKeySpecFlags::Overwrite => "OVERWRITE", + RedisCommandKeySpecFlags::Remove => "REMOVE", + RedisCommandKeySpecFlags::Access => "ACCESS", + RedisCommandKeySpecFlags::Update => "UPDATE", + RedisCommandKeySpecFlags::Insert => "INSERT", + RedisCommandKeySpecFlags::Delete => "DELETE", + RedisCommandKeySpecFlags::NotKey => "NOT_KEY", + RedisCommandKeySpecFlags::Incomplete => "INCOMPLETE", + RedisCommandKeySpecFlags::VariableFlags => "VARIABLE_FLAGS", + } + } +} + +#[derive(Debug, Deserialize)] +pub struct FindKeysRange { + last_key: i32, + steps: i32, + limit: i32, +} + +#[derive(Debug, Deserialize)] +pub struct FindKeysNum { + key_num_idx: i32, + first_key: i32, + key_step: i32, +} + +#[derive(Debug, Deserialize)] +pub enum FindKeys { + Range(FindKeysRange), + Keynum(FindKeysNum), +} + +#[derive(Debug, Deserialize)] +pub struct BeginSearchIndex { + index: i32, +} + +#[derive(Debug, Deserialize)] +pub struct BeginSearchKeyword { + keyword: String, + startfrom: i32, +} + +#[derive(Debug, Deserialize)] +pub enum BeginSearch { + Index(BeginSearchIndex), + Keyword(BeginSearchKeyword), // (keyword, startfrom) +} + +#[derive(Debug, Deserialize)] +pub struct KeySpecArg { + notes: Option, + flags: Vec, + begin_search: BeginSearch, + find_keys: FindKeys, +} + +#[derive(Debug, Deserialize)] +struct Args { + name: Option, + flags: Vec, + summary: Option, + complexity: Option, + since: Option, + tips: Option, + arity: i64, + key_spec: Vec, +} + +impl Parse for Args { + fn parse(input: ParseStream) -> parse::Result { + from_stream(config::JSONY, &input) + } +} + +fn to_token_stream(s: Option) -> 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 + .unwrap_or_else(|| original_function_name.to_string()); + let flags_str = args + .flags + .into_iter() + .fold(String::new(), |s, v| { + format!("{} {}", s, Into::<&'static str>::into(&v)) + }) + .trim() + .to_owned(); + let flags_literal = quote!(#flags_str); + 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: Vec<&'static str> = v.flags.iter().map(|v| v.into()).collect(); + quote! { + vec![#(redis_module::commands::KeySpecFlags::try_from(#flags)?, )*] + } + }) + .collect(); + + let key_spec_begin_search: Vec<_> = args + .key_spec + .iter() + .map(|v| match &v.begin_search { + BeginSearch::Index(i) => { + let i = i.index; + quote! { + redis_module::commands::BeginSearch::new_index(#i) + } + } + BeginSearch::Keyword(begin_search_keyword) => { + let k = begin_search_keyword.keyword.as_str(); + let i = begin_search_keyword.startfrom; + quote! { + redis_module::commands::BeginSearch::new_keyword(#k.to_owned(), #i) + } + } + }) + .collect(); + + let key_spec_find_keys: Vec<_> = args + .key_spec + .iter() + .map(|v| match &v.find_keys { + FindKeys::Keynum(find_keys_num) => { + let keynumidx = find_keys_num.key_num_idx; + let firstkey = find_keys_num.first_key; + let keystep = find_keys_num.key_step; + quote! { + redis_module::commands::FindKeys::new_keys_num(#keynumidx, #firstkey, #keystep) + } + } + FindKeys::Range(find_keys_range) => { + let last_key = find_keys_range.last_key; + let steps = find_keys_range.steps; + let limit = find_keys_range.limit; + quote! { + redis_module::commands::FindKeys::new_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::commands::COMMNADS_LIST)] + fn #get_command_info_function_name() -> Result { + let key_spec = vec![ + #( + redis_module::commands::KeySpec::new( + #key_spec_notes, + #key_spec_flags.into(), + #key_spec_begin_search, + #key_spec_find_keys, + ), + )* + ]; + Ok(redis_module::commands::CommandInfo::new( + #name_literal.to_owned(), + Some(#flags_literal.to_owned()), + #summary_literal, + #complexity_literal, + #since_literal, + #tips_literal, + #arity_literal, + key_spec, + #c_function_name, + )) + } + }; + gen.into() +} diff --git a/redismodule-rs-macros/src/lib.rs b/redismodule-rs-macros/src/lib.rs index a0633031..d947426d 100644 --- a/redismodule-rs-macros/src/lib.rs +++ b/redismodule-rs-macros/src/lib.rs @@ -2,6 +2,87 @@ use proc_macro::TokenStream; use quote::quote; use syn::ItemFn; +mod 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 (optional) - The command name. in case not given, the function name will be taken. +/// * flags - An array of `RedisCommandFlags`. +/// * 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: +/// * Readonly - Read-Only. Reads the value of the key, but doesn't necessarily return it. +/// * ReadWrite - Read-Write. Modifies the data stored in the value of the key or its metadata. +/// * Overwrite - Overwrite. Overwrites the data stored in the value of the key. +/// * Remove - 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. +/// * NotKey - 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. +/// * VariableFlags - 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 - An object of three element `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 - An object 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 +/// #[command( +/// { +/// name: "test", +/// flags: [ReadOnly], +/// arity: -2, +/// key_spec: [ +/// { +/// notes: "test command that define all the arguments at even possition as keys", +/// flags: [ReadOnly, Access], +/// begin_search: Keyword({ keyword : "foo", startfrom : 1 }), +/// find_keys: Range({ last_key :- 1, steps : 2, limit : 0 }), +/// } +/// ] +/// } +/// )] +/// fn test_command(_ctx: &Context, _args: Vec) -> 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 command(attr: TokenStream, item: TokenStream) -> TokenStream { + 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) { diff --git a/src/context/commands.rs b/src/context/commands.rs new file mode 100644 index 00000000..2795ffae --- /dev/null +++ b/src/context/commands.rs @@ -0,0 +1,505 @@ +use crate::raw; +use crate::Context; +use crate::RedisError; +use crate::Status; +use bitflags::bitflags; +use libc::c_char; +use linkme::distributed_slice; +use redis_module_macros_internals::api; +use std::ffi::CString; +use std::mem::MaybeUninit; +use std::os::raw::c_int; +use std::ptr; + +const COMMNAD_INFO_VERSION: raw::RedisModuleCommandInfoVersion = + raw::RedisModuleCommandInfoVersion { + version: 1, + sizeof_historyentry: std::mem::size_of::(), + sizeof_keyspec: std::mem::size_of::(), + sizeof_arg: std::mem::size_of::(), + }; + +bitflags! { + /// Key spec flags + /// + /// The first four refer to what the command actually does with the value or + /// metadata of the key, and not necessarily the user data or how it affects + /// it. Each key-spec must have exactly one of these. Any operation + /// that's not distinctly deletion, overwrite or read-only would be marked as + /// RW. + /// + /// The next four refer to user data inside the value of the key, not the + /// metadata like LRU, type, cardinality. It refers to the logical operation + /// on the user's data (actual input strings or TTL), being + /// used/returned/copied/changed. It doesn't refer to modification or + /// returning of metadata (like type, count, presence of data). ACCESS can be + /// combined with one of the write operations INSERT, DELETE or UPDATE. Any + /// write that's not an INSERT or a DELETE would be UPDATE. + pub struct KeySpecFlags : u32 { + /// Read-Only. Reads the value of the key, but doesn't necessarily return it. + const READ_ONLY = raw::REDISMODULE_CMD_KEY_RO; + + /// Read-Write. Modifies the data stored in the value of the key or its metadata. + const READ_WRITE = raw::REDISMODULE_CMD_KEY_RW; + + /// Overwrite. Overwrites the data stored in the value of the key. + const OVERWRITE = raw::REDISMODULE_CMD_KEY_OW; + + /// Deletes the key. + const REMOVE = raw::REDISMODULE_CMD_KEY_RM; + + /// Returns, copies or uses the user data from the value of the key. + const ACCESS = raw::REDISMODULE_CMD_KEY_ACCESS; + + /// Updates data to the value, new value may depend on the old value. + const UPDATE = raw::REDISMODULE_CMD_KEY_UPDATE; + + /// Adds data to the value with no chance of modification or deletion of existing data. + const INSERT = raw::REDISMODULE_CMD_KEY_INSERT; + + /// Explicitly deletes some content from the value of the key. + const DELETE = raw::REDISMODULE_CMD_KEY_DELETE; + + /// The key is not actually a key, but should be routed in cluster mode as if it was a key. + const NOT_KEY = raw::REDISMODULE_CMD_KEY_NOT_KEY; + + /// The keyspec might not point out all the keys it should cover. + const INCOMPLETE = raw::REDISMODULE_CMD_KEY_INCOMPLETE; + + /// Some keys might have different flags depending on arguments. + const VARIABLE_FLAGS = raw::REDISMODULE_CMD_KEY_VARIABLE_FLAGS; + } +} + +impl TryFrom<&str> for KeySpecFlags { + type Error = RedisError; + fn try_from(value: &str) -> Result { + match value.to_lowercase().as_str() { + "read_only" => Ok(KeySpecFlags::READ_ONLY), + "read_write" => Ok(KeySpecFlags::READ_WRITE), + "overwrite" => Ok(KeySpecFlags::OVERWRITE), + "remove" => Ok(KeySpecFlags::REMOVE), + "access" => Ok(KeySpecFlags::ACCESS), + "update" => Ok(KeySpecFlags::UPDATE), + "insert" => Ok(KeySpecFlags::INSERT), + "delete" => Ok(KeySpecFlags::DELETE), + "not_key" => Ok(KeySpecFlags::NOT_KEY), + "incomplete" => Ok(KeySpecFlags::INCOMPLETE), + "variable_flags" => Ok(KeySpecFlags::VARIABLE_FLAGS), + _ => Err(RedisError::String(format!( + "Value {value} is not a valid key spec flag." + ))), + } + } +} + +impl From> for KeySpecFlags { + fn from(value: Vec) -> Self { + value + .into_iter() + .fold(KeySpecFlags::empty(), |a, item| a | item) + } +} + +/// A version of begin search spec that finds the index +/// indicating where to start search for keys based on +/// an index. +pub struct BeginSearchIndex { + index: i32, +} + +/// A version of begin search spec that finds the index +/// indicating where to start search for keys based on +/// a keyword. +pub struct BeginSearchKeyword { + keyword: String, + startfrom: i32, +} + +/// This struct represents how Redis should start looking for keys. +/// There are 2 possible options: +/// 1. Index - start looking for keys from a given position. +/// 2. Keyword - Search for a specific keyward and start looking for keys from this keyword +pub enum BeginSearch { + Index(BeginSearchIndex), + Keyword(BeginSearchKeyword), +} + +impl BeginSearch { + pub fn new_index(index: i32) -> BeginSearch { + BeginSearch::Index(BeginSearchIndex { index }) + } + + pub fn new_keyword(keyword: String, startfrom: i32) -> BeginSearch { + BeginSearch::Keyword(BeginSearchKeyword { keyword, startfrom }) + } +} + +impl From<&BeginSearch> + for ( + raw::RedisModuleKeySpecBeginSearchType, + raw::RedisModuleCommandKeySpec__bindgen_ty_1, + ) +{ + fn from(value: &BeginSearch) -> Self { + match value { + BeginSearch::Index(index_spec) => ( + raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_INDEX, + raw::RedisModuleCommandKeySpec__bindgen_ty_1 { + index: raw::RedisModuleCommandKeySpec__bindgen_ty_1__bindgen_ty_1 { + pos: index_spec.index, + }, + }, + ), + BeginSearch::Keyword(keyword_spec) => { + let keyword = CString::new(keyword_spec.keyword.as_str()) + .unwrap() + .into_raw(); + ( + raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_KEYWORD, + raw::RedisModuleCommandKeySpec__bindgen_ty_1 { + keyword: raw::RedisModuleCommandKeySpec__bindgen_ty_1__bindgen_ty_2 { + keyword, + startfrom: keyword_spec.startfrom, + }, + }, + ) + } + } + } +} + +/// A version of find keys base on range. +/// * `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. +pub struct FindKeysRange { + last_key: i32, + steps: i32, + limit: i32, +} + +/// A version of find keys base on some argument representing the number of keys +/// * 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? +pub struct FindKeysNum { + key_num_idx: i32, + first_key: i32, + key_step: i32, +} + +/// 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 enum. +/// There are 2 possible options: +/// 1. Range - Required to specify additional 3 more values, `last_key`, `steps`, and `limit`. +/// 2. Keynum - Required to specify additional 3 more values, `keynumidx`, `firstkey`, and `keystep`. +/// Redis will consider the argument at `keynumidx` as an indicator +/// to the number of keys that will follow. Then it will start +/// from `firstkey` and jump each `keystep` to find the keys. +pub enum FindKeys { + Range(FindKeysRange), + Keynum(FindKeysNum), +} + +impl FindKeys { + pub fn new_range(last_key: i32, steps: i32, limit: i32) -> FindKeys { + FindKeys::Range(FindKeysRange { + last_key, + steps, + limit, + }) + } + + pub fn new_keys_num(key_num_idx: i32, first_key: i32, key_step: i32) -> FindKeys { + FindKeys::Keynum(FindKeysNum { + key_num_idx, + first_key, + key_step, + }) + } +} + +impl From<&FindKeys> + for ( + raw::RedisModuleKeySpecFindKeysType, + raw::RedisModuleCommandKeySpec__bindgen_ty_2, + ) +{ + fn from(value: &FindKeys) -> Self { + match value { + FindKeys::Range(range_spec) => ( + raw::RedisModuleKeySpecFindKeysType_REDISMODULE_KSPEC_FK_RANGE, + raw::RedisModuleCommandKeySpec__bindgen_ty_2 { + range: raw::RedisModuleCommandKeySpec__bindgen_ty_2__bindgen_ty_1 { + lastkey: range_spec.last_key, + keystep: range_spec.steps, + limit: range_spec.limit, + }, + }, + ), + FindKeys::Keynum(keynum_spec) => ( + raw::RedisModuleKeySpecFindKeysType_REDISMODULE_KSPEC_FK_KEYNUM, + raw::RedisModuleCommandKeySpec__bindgen_ty_2 { + keynum: raw::RedisModuleCommandKeySpec__bindgen_ty_2__bindgen_ty_2 { + keynumidx: keynum_spec.key_num_idx, + firstkey: keynum_spec.first_key, + keystep: keynum_spec.key_step, + }, + }, + ), + } + } +} + +/// A struct that specify how to find keys from a command. +/// It is devided into 2 parts: +/// 1. begin_search - indicate how to find the first command argument from where to start searching for keys. +/// 2. find_keys - the methose to use in order to find the keys. +pub struct KeySpec { + notes: Option, + flags: KeySpecFlags, + begin_search: BeginSearch, + find_keys: FindKeys, +} + +impl KeySpec { + pub fn new( + notes: Option, + flags: KeySpecFlags, + begin_search: BeginSearch, + find_keys: FindKeys, + ) -> KeySpec { + KeySpec { + notes, + flags, + begin_search, + find_keys, + } + } +} + +impl From<&KeySpec> for raw::RedisModuleCommandKeySpec { + fn from(value: &KeySpec) -> Self { + let (begin_search_type, bs) = (&value.begin_search).into(); + let (find_keys_type, fk) = (&value.find_keys).into(); + raw::RedisModuleCommandKeySpec { + notes: value + .notes + .as_ref() + .map(|v| CString::new(v.as_str()).unwrap().into_raw()) + .unwrap_or(ptr::null_mut()), + flags: value.flags.bits() as u64, + begin_search_type, + bs, + find_keys_type, + fk, + } + } +} + +type CommandCallback = + extern "C" fn(*mut raw::RedisModuleCtx, *mut *mut raw::RedisModuleString, i32) -> i32; + +/// A struct represent a CommandInfo +pub struct CommandInfo { + name: String, + flags: Option, + summary: Option, + complexity: Option, + since: Option, + tips: Option, + arity: i64, + key_spec: Vec, + callback: CommandCallback, +} + +impl CommandInfo { + pub fn new( + name: String, + flags: Option, + summary: Option, + complexity: Option, + since: Option, + tips: Option, + arity: i64, + key_spec: Vec, + callback: CommandCallback, + ) -> CommandInfo { + CommandInfo { + name, + flags, + summary, + complexity, + since, + tips, + arity, + key_spec, + callback, + } + } +} + +#[distributed_slice()] +pub static COMMNADS_LIST: [fn() -> Result] = [..]; + +pub fn get_redis_key_spec(key_spec: Vec) -> Vec { + let mut redis_key_spec: Vec = + key_spec.into_iter().map(|v| (&v).into()).collect(); + let zerod: raw::RedisModuleCommandKeySpec = unsafe { MaybeUninit::zeroed().assume_init() }; + redis_key_spec.push(zerod); + redis_key_spec +} + +api! {[ + RedisModule_CreateCommand, + RedisModule_GetCommand, + RedisModule_SetCommandInfo, + ], + /// Register all the commands located on `COMMNADS_LIST`. + fn register_commands_internal(ctx: &Context) -> Result<(), RedisError> { + COMMNADS_LIST.iter().try_for_each(|command| { + let command_info = command()?; + let name: CString = CString::new(command_info.name.as_str()).unwrap(); + let flags = CString::new( + command_info + .flags + .as_ref() + .map(|v| v.as_str()) + .unwrap_or(""), + ) + .unwrap(); + + if unsafe { + RedisModule_CreateCommand( + ctx.ctx, + name.as_ptr(), + Some(command_info.callback), + flags.as_ptr(), + 0, + 0, + 0, + ) + } == raw::Status::Err as i32 + { + return Err(RedisError::String(format!( + "Failed register command {}.", + command_info.name + ))); + } + + // Register the extra data of the command + let command = unsafe { RedisModule_GetCommand(ctx.ctx, name.as_ptr()) }; + + if command.is_null() { + return Err(RedisError::String(format!( + "Failed finding command {} after registration.", + command_info.name + ))); + } + + let summary = command_info + .summary + .as_ref() + .map(|v| Some(CString::new(v.as_str()).unwrap())) + .unwrap_or(None); + let complexity = command_info + .complexity + .as_ref() + .map(|v| Some(CString::new(v.as_str()).unwrap())) + .unwrap_or(None); + let since = command_info + .since + .as_ref() + .map(|v| Some(CString::new(v.as_str()).unwrap())) + .unwrap_or(None); + let tips = command_info + .tips + .as_ref() + .map(|v| Some(CString::new(v.as_str()).unwrap())) + .unwrap_or(None); + + let key_specs = get_redis_key_spec(command_info.key_spec); + + let mut redis_command_info = raw::RedisModuleCommandInfo { + version: &COMMNAD_INFO_VERSION, + summary: summary.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()), + complexity: complexity.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()), + since: since.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()), + history: ptr::null_mut(), // currently we will not support history + tips: tips.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()), + arity: command_info.arity as c_int, + key_specs: key_specs.as_ptr() as *mut raw::RedisModuleCommandKeySpec, + args: ptr::null_mut(), + }; + + if unsafe { RedisModule_SetCommandInfo(command, &mut redis_command_info as *mut raw::RedisModuleCommandInfo) } == raw::Status::Err as i32 { + return Err(RedisError::String(format!( + "Failed setting info for command {}.", + command_info.name + ))); + } + + // the only CString pointers which are not freed are those of the key_specs, lets free them here. + key_specs.into_iter().for_each(|v|{ + if !v.notes.is_null() { + unsafe{CString::from_raw(v.notes as *mut c_char)}; + } + if v.begin_search_type == raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_KEYWORD { + let keyword = unsafe{v.bs.keyword.keyword}; + if !keyword.is_null() { + unsafe{CString::from_raw(v.bs.keyword.keyword as *mut c_char)}; + } + } + }); + + Ok(()) + }) + } +} + +#[cfg(any( + feature = "min-redis-compatibility-version-7-2", + feature = "min-redis-compatibility-version-7-0" +))] +pub fn register_commands(ctx: &Context) -> Status { + register_commands_internal(ctx).map_or_else( + |e| { + ctx.log_warning(&e.to_string()); + Status::Err + }, + |_| Status::Ok, + ) +} + +#[cfg(any( + feature = "min-redis-compatibility-version-6-2", + feature = "min-redis-compatibility-version-6-0" +))] +pub fn register_commands(ctx: &Context) -> Status { + register_commands_internal(ctx).map_or_else( + |e| { + ctx.log_warning(&e.to_string()); + Status::Err + }, + |v| { + v.map_or_else( + |e| { + ctx.log_warning(&e.to_string()); + Status::Err + }, + |_| Status::Ok, + ) + }, + ) +} diff --git a/src/context/mod.rs b/src/context/mod.rs index 65ee90e9..94f04004 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -1,5 +1,5 @@ use bitflags::bitflags; -use redis_module_macros_internals::redismodule_api; +use redis_module_macros_internals::api; use std::ffi::CString; use std::os::raw::c_void; use std::os::raw::{c_char, c_int, c_long, c_longlong}; @@ -23,6 +23,7 @@ mod timer; pub mod blocked; pub mod call_reply; +pub mod commands; pub mod info; pub mod keys_cursor; pub mod server_events; @@ -695,7 +696,7 @@ impl Context { acl_permission_result.map_err(|_e| RedisError::Str("User does not have permissions on key")) } - redismodule_api!( + api!( [RedisModule_AddPostNotificationJob], /// When running inside a key space notification callback, it is dangerous and highly discouraged to perform any write /// operation. In order to still perform write actions in this scenario, Redis provides this API ([add_post_notification_job]) diff --git a/src/lib.rs b/src/lib.rs index f83d5895..7abe5b96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ pub use crate::raw::NotifyEvent; pub use crate::configuration::ConfigurationValue; pub use crate::configuration::EnumConfigurationValue; pub use crate::context::call_reply::{CallReply, CallResult, ErrorReply}; +pub use crate::context::commands; pub use crate::context::keys_cursor::KeysCursor; pub use crate::context::server_events; pub use crate::context::AclPermissions; diff --git a/src/macros.rs b/src/macros.rs index a3cda082..f3eed320 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -227,6 +227,10 @@ macro_rules! redis_module { $crate::redis_command!(ctx, $name, $command, $flags, $firstkey, $lastkey, $keystep); )* + if $crate::commands::register_commands(&context) == raw::Status::Err { + return raw::Status::Err as c_int; + } + $( $( $crate::redis_event_handler!(ctx, $(raw::NotifyEvent::$event_type |)+ raw::NotifyEvent::empty(), $event_handler); diff --git a/tests/integration.rs b/tests/integration.rs index 46e31ead..65ce9399 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -499,3 +499,42 @@ fn test_response() -> Result<()> { Ok(()) } + +#[test] +fn test_command_proc_macro() -> 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: Vec = redis::cmd("COMMAND") + .arg(&["GETKEYS", "classic_keys", "x", "foo", "y", "bar"]) + .query(&mut con) + .with_context(|| "failed to run string.set")?; + + assert_eq!(&res, &["x", "y"]); + + let res: Vec = redis::cmd("COMMAND") + .arg(&["GETKEYS", "keyword_keys", "foo", "x", "1", "y", "2"]) + .query(&mut con) + .with_context(|| "failed to run string.set")?; + + assert_eq!(&res, &["x", "y"]); + + let res: Vec = redis::cmd("COMMAND") + .arg(&["GETKEYS", "num_keys", "3", "x", "y", "z", "foo", "bar"]) + .query(&mut con) + .with_context(|| "failed to run string.set")?; + + assert_eq!(&res, &["x", "y", "z"]); + + let res: Vec = redis::cmd("COMMAND") + .arg(&["GETKEYS", "num_keys", "0", "foo", "bar"]) + .query(&mut con) + .with_context(|| "failed to run string.set")?; + + assert!(res.is_empty()); + + Ok(()) +}