redis_module/context/
commands.rs

1use crate::raw;
2use crate::Context;
3use crate::RedisError;
4use crate::Status;
5use bitflags::bitflags;
6use libc::c_char;
7use linkme::distributed_slice;
8use redis_module_macros_internals::api;
9use std::ffi::CString;
10use std::mem::MaybeUninit;
11use std::os::raw::c_int;
12use std::ptr;
13
14const COMMNAD_INFO_VERSION: raw::RedisModuleCommandInfoVersion =
15    raw::RedisModuleCommandInfoVersion {
16        version: 1,
17        sizeof_historyentry: std::mem::size_of::<raw::RedisModuleCommandHistoryEntry>(),
18        sizeof_keyspec: std::mem::size_of::<raw::RedisModuleCommandKeySpec>(),
19        sizeof_arg: std::mem::size_of::<raw::RedisModuleCommandArg>(),
20    };
21
22bitflags! {
23    /// Key spec flags
24    ///
25    /// The first four refer to what the command actually does with the value or
26    /// metadata of the key, and not necessarily the user data or how it affects
27    /// it. Each key-spec must have exactly one of these. Any operation
28    /// that's not distinctly deletion, overwrite or read-only would be marked as
29    /// RW.
30    ///
31    /// The next four refer to user data inside the value of the key, not the
32    /// metadata like LRU, type, cardinality. It refers to the logical operation
33    /// on the user's data (actual input strings or TTL), being
34    /// used/returned/copied/changed. It doesn't refer to modification or
35    /// returning of metadata (like type, count, presence of data). ACCESS can be
36    /// combined with one of the write operations INSERT, DELETE or UPDATE. Any
37    /// write that's not an INSERT or a DELETE would be UPDATE.
38    pub struct KeySpecFlags : u32 {
39        /// Read-Only. Reads the value of the key, but doesn't necessarily return it.
40        const READ_ONLY = raw::REDISMODULE_CMD_KEY_RO;
41
42        /// Read-Write. Modifies the data stored in the value of the key or its metadata.
43        const READ_WRITE = raw::REDISMODULE_CMD_KEY_RW;
44
45        /// Overwrite. Overwrites the data stored in the value of the key.
46        const OVERWRITE = raw::REDISMODULE_CMD_KEY_OW;
47
48        /// Deletes the key.
49        const REMOVE = raw::REDISMODULE_CMD_KEY_RM;
50
51        /// Returns, copies or uses the user data from the value of the key.
52        const ACCESS = raw::REDISMODULE_CMD_KEY_ACCESS;
53
54        /// Updates data to the value, new value may depend on the old value.
55        const UPDATE = raw::REDISMODULE_CMD_KEY_UPDATE;
56
57        /// Adds data to the value with no chance of modification or deletion of existing data.
58        const INSERT = raw::REDISMODULE_CMD_KEY_INSERT;
59
60        /// Explicitly deletes some content from the value of the key.
61        const DELETE = raw::REDISMODULE_CMD_KEY_DELETE;
62
63        /// The key is not actually a key, but should be routed in cluster mode as if it was a key.
64        const NOT_KEY = raw::REDISMODULE_CMD_KEY_NOT_KEY;
65
66        /// The keyspec might not point out all the keys it should cover.
67        const INCOMPLETE = raw::REDISMODULE_CMD_KEY_INCOMPLETE;
68
69        /// Some keys might have different flags depending on arguments.
70        const VARIABLE_FLAGS = raw::REDISMODULE_CMD_KEY_VARIABLE_FLAGS;
71    }
72}
73
74impl TryFrom<&str> for KeySpecFlags {
75    type Error = RedisError;
76    fn try_from(value: &str) -> Result<Self, Self::Error> {
77        match value.to_lowercase().as_str() {
78            "read_only" => Ok(KeySpecFlags::READ_ONLY),
79            "read_write" => Ok(KeySpecFlags::READ_WRITE),
80            "overwrite" => Ok(KeySpecFlags::OVERWRITE),
81            "remove" => Ok(KeySpecFlags::REMOVE),
82            "access" => Ok(KeySpecFlags::ACCESS),
83            "update" => Ok(KeySpecFlags::UPDATE),
84            "insert" => Ok(KeySpecFlags::INSERT),
85            "delete" => Ok(KeySpecFlags::DELETE),
86            "not_key" => Ok(KeySpecFlags::NOT_KEY),
87            "incomplete" => Ok(KeySpecFlags::INCOMPLETE),
88            "variable_flags" => Ok(KeySpecFlags::VARIABLE_FLAGS),
89            _ => Err(RedisError::String(format!(
90                "Value {value} is not a valid key spec flag."
91            ))),
92        }
93    }
94}
95
96impl From<Vec<KeySpecFlags>> for KeySpecFlags {
97    fn from(value: Vec<KeySpecFlags>) -> Self {
98        value
99            .into_iter()
100            .fold(KeySpecFlags::empty(), |a, item| a | item)
101    }
102}
103
104/// A version of begin search spec that finds the index
105/// indicating where to start search for keys based on
106/// an index.
107pub struct BeginSearchIndex {
108    index: i32,
109}
110
111/// A version of begin search spec that finds the index
112/// indicating where to start search for keys based on
113/// a keyword.
114pub struct BeginSearchKeyword {
115    keyword: String,
116    startfrom: i32,
117}
118
119/// This struct represents how Redis should start looking for keys.
120/// There are 2 possible options:
121/// 1. Index - start looking for keys from a given position.
122/// 2. Keyword - Search for a specific keyward and start looking for keys from this keyword
123pub enum BeginSearch {
124    Index(BeginSearchIndex),
125    Keyword(BeginSearchKeyword),
126}
127
128impl BeginSearch {
129    pub fn new_index(index: i32) -> BeginSearch {
130        BeginSearch::Index(BeginSearchIndex { index })
131    }
132
133    pub fn new_keyword(keyword: String, startfrom: i32) -> BeginSearch {
134        BeginSearch::Keyword(BeginSearchKeyword { keyword, startfrom })
135    }
136}
137
138impl From<&BeginSearch>
139    for (
140        raw::RedisModuleKeySpecBeginSearchType,
141        raw::RedisModuleCommandKeySpec__bindgen_ty_1,
142    )
143{
144    fn from(value: &BeginSearch) -> Self {
145        match value {
146            BeginSearch::Index(index_spec) => (
147                raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_INDEX,
148                raw::RedisModuleCommandKeySpec__bindgen_ty_1 {
149                    index: raw::RedisModuleCommandKeySpec__bindgen_ty_1__bindgen_ty_1 {
150                        pos: index_spec.index,
151                    },
152                },
153            ),
154            BeginSearch::Keyword(keyword_spec) => {
155                let keyword = CString::new(keyword_spec.keyword.as_str())
156                    .unwrap()
157                    .into_raw();
158                (
159                    raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_KEYWORD,
160                    raw::RedisModuleCommandKeySpec__bindgen_ty_1 {
161                        keyword: raw::RedisModuleCommandKeySpec__bindgen_ty_1__bindgen_ty_2 {
162                            keyword,
163                            startfrom: keyword_spec.startfrom,
164                        },
165                    },
166                )
167            }
168        }
169    }
170}
171
172/// A version of find keys base on range.
173/// * `last_key` - Index of the last key relative to the result of the
174///   begin search step. Can be negative, in which case it's not
175///   relative. -1 indicates the last argument, -2 one before the
176///   last and so on.
177/// * `steps` - How many arguments should we skip after finding a
178///   key, in order to find the next one.
179/// * `limit` - If `lastkey` is -1, we use `limit` to stop the search
180///   by a factor. 0 and 1 mean no limit. 2 means 1/2 of the
181///   remaining args, 3 means 1/3, and so on.
182pub struct FindKeysRange {
183    last_key: i32,
184    steps: i32,
185    limit: i32,
186}
187
188/// A version of find keys base on some argument representing the number of keys
189/// * keynumidx - Index of the argument containing the number of
190///   keys to come, relative to the result of the begin search step.
191/// * firstkey - Index of the fist key relative to the result of the
192///   begin search step. (Usually it's just after `keynumidx`, in
193///   which case it should be set to `keynumidx + 1`.)
194/// * keystep - How many arguments should we skip after finding a
195///   key, in order to find the next one?
196pub struct FindKeysNum {
197    key_num_idx: i32,
198    first_key: i32,
199    key_step: i32,
200}
201
202/// After Redis finds the location from where it needs to start looking for keys,
203/// Redis will start finding keys base on the information in this enum.
204/// There are 2 possible options:
205/// 1. Range -   Required to specify additional 3 more values, `last_key`, `steps`, and `limit`.
206/// 2. Keynum -  Required to specify additional 3 more values, `keynumidx`, `firstkey`, and `keystep`.
207///              Redis will consider the argument at `keynumidx` as an indicator
208///              to the number of keys that will follow. Then it will start
209///              from `firstkey` and jump each `keystep` to find the keys.
210pub enum FindKeys {
211    Range(FindKeysRange),
212    Keynum(FindKeysNum),
213}
214
215impl FindKeys {
216    pub fn new_range(last_key: i32, steps: i32, limit: i32) -> FindKeys {
217        FindKeys::Range(FindKeysRange {
218            last_key,
219            steps,
220            limit,
221        })
222    }
223
224    pub fn new_keys_num(key_num_idx: i32, first_key: i32, key_step: i32) -> FindKeys {
225        FindKeys::Keynum(FindKeysNum {
226            key_num_idx,
227            first_key,
228            key_step,
229        })
230    }
231}
232
233impl From<&FindKeys>
234    for (
235        raw::RedisModuleKeySpecFindKeysType,
236        raw::RedisModuleCommandKeySpec__bindgen_ty_2,
237    )
238{
239    fn from(value: &FindKeys) -> Self {
240        match value {
241            FindKeys::Range(range_spec) => (
242                raw::RedisModuleKeySpecFindKeysType_REDISMODULE_KSPEC_FK_RANGE,
243                raw::RedisModuleCommandKeySpec__bindgen_ty_2 {
244                    range: raw::RedisModuleCommandKeySpec__bindgen_ty_2__bindgen_ty_1 {
245                        lastkey: range_spec.last_key,
246                        keystep: range_spec.steps,
247                        limit: range_spec.limit,
248                    },
249                },
250            ),
251            FindKeys::Keynum(keynum_spec) => (
252                raw::RedisModuleKeySpecFindKeysType_REDISMODULE_KSPEC_FK_KEYNUM,
253                raw::RedisModuleCommandKeySpec__bindgen_ty_2 {
254                    keynum: raw::RedisModuleCommandKeySpec__bindgen_ty_2__bindgen_ty_2 {
255                        keynumidx: keynum_spec.key_num_idx,
256                        firstkey: keynum_spec.first_key,
257                        keystep: keynum_spec.key_step,
258                    },
259                },
260            ),
261        }
262    }
263}
264
265/// A struct that specify how to find keys from a command.
266/// It is devided into 2 parts:
267/// 1. begin_search - indicate how to find the first command argument from where to start searching for keys.
268/// 2. find_keys - the methose to use in order to find the keys.
269pub struct KeySpec {
270    notes: Option<String>,
271    flags: KeySpecFlags,
272    begin_search: BeginSearch,
273    find_keys: FindKeys,
274}
275
276impl KeySpec {
277    pub fn new(
278        notes: Option<String>,
279        flags: KeySpecFlags,
280        begin_search: BeginSearch,
281        find_keys: FindKeys,
282    ) -> KeySpec {
283        KeySpec {
284            notes,
285            flags,
286            begin_search,
287            find_keys,
288        }
289    }
290}
291
292impl From<&KeySpec> for raw::RedisModuleCommandKeySpec {
293    fn from(value: &KeySpec) -> Self {
294        let (begin_search_type, bs) = (&value.begin_search).into();
295        let (find_keys_type, fk) = (&value.find_keys).into();
296        raw::RedisModuleCommandKeySpec {
297            notes: value
298                .notes
299                .as_ref()
300                .map(|v| CString::new(v.as_str()).unwrap().into_raw())
301                .unwrap_or(ptr::null_mut()),
302            flags: value.flags.bits() as u64,
303            begin_search_type,
304            bs,
305            find_keys_type,
306            fk,
307        }
308    }
309}
310
311type CommandCallback =
312    extern "C" fn(*mut raw::RedisModuleCtx, *mut *mut raw::RedisModuleString, i32) -> i32;
313
314/// A struct represent a CommandInfo
315pub struct CommandInfo {
316    name: String,
317    flags: Option<String>,
318    summary: Option<String>,
319    complexity: Option<String>,
320    since: Option<String>,
321    tips: Option<String>,
322    arity: i64,
323    key_spec: Vec<KeySpec>,
324    callback: CommandCallback,
325}
326
327impl CommandInfo {
328    pub fn new(
329        name: String,
330        flags: Option<String>,
331        summary: Option<String>,
332        complexity: Option<String>,
333        since: Option<String>,
334        tips: Option<String>,
335        arity: i64,
336        key_spec: Vec<KeySpec>,
337        callback: CommandCallback,
338    ) -> CommandInfo {
339        CommandInfo {
340            name,
341            flags,
342            summary,
343            complexity,
344            since,
345            tips,
346            arity,
347            key_spec,
348            callback,
349        }
350    }
351}
352
353#[distributed_slice()]
354pub static COMMANDS_LIST: [fn() -> Result<CommandInfo, RedisError>] = [..];
355
356pub fn get_redis_key_spec(key_spec: Vec<KeySpec>) -> Vec<raw::RedisModuleCommandKeySpec> {
357    let mut redis_key_spec: Vec<raw::RedisModuleCommandKeySpec> =
358        key_spec.into_iter().map(|v| (&v).into()).collect();
359    let zerod: raw::RedisModuleCommandKeySpec = unsafe { MaybeUninit::zeroed().assume_init() };
360    redis_key_spec.push(zerod);
361    redis_key_spec
362}
363
364api! {[
365        RedisModule_CreateCommand,
366        RedisModule_GetCommand,
367        RedisModule_SetCommandInfo,
368    ],
369    /// Register all the commands located on `COMMNADS_LIST`.
370    fn register_commands_internal(ctx: &Context) -> Result<(), RedisError> {
371        COMMANDS_LIST.iter().try_for_each(|command| {
372            let command_info = command()?;
373            let name: CString = CString::new(command_info.name.as_str()).unwrap();
374            let flags = CString::new(
375                command_info
376                    .flags
377                    .as_deref()
378                    .unwrap_or(""),
379            )
380            .unwrap();
381
382            if unsafe {
383                RedisModule_CreateCommand(
384                    ctx.ctx,
385                    name.as_ptr(),
386                    Some(command_info.callback),
387                    flags.as_ptr(),
388                    0,
389                    0,
390                    0,
391                )
392            } == raw::Status::Err as i32
393            {
394                return Err(RedisError::String(format!(
395                    "Failed register command {}.",
396                    command_info.name
397                )));
398            }
399
400            // Register the extra data of the command
401            let command = unsafe { RedisModule_GetCommand(ctx.ctx, name.as_ptr()) };
402
403            if command.is_null() {
404                return Err(RedisError::String(format!(
405                    "Failed finding command {} after registration.",
406                    command_info.name
407                )));
408            }
409
410            let summary = command_info
411                .summary
412                .as_ref()
413                .map(|v| Some(CString::new(v.as_str()).unwrap()))
414                .unwrap_or(None);
415            let complexity = command_info
416                .complexity
417                .as_ref()
418                .map(|v| Some(CString::new(v.as_str()).unwrap()))
419                .unwrap_or(None);
420            let since = command_info
421                .since
422                .as_ref()
423                .map(|v| Some(CString::new(v.as_str()).unwrap()))
424                .unwrap_or(None);
425            let tips = command_info
426                .tips
427                .as_ref()
428                .map(|v| Some(CString::new(v.as_str()).unwrap()))
429                .unwrap_or(None);
430
431            let key_specs = get_redis_key_spec(command_info.key_spec);
432
433            let mut redis_command_info = raw::RedisModuleCommandInfo {
434                version: &COMMNAD_INFO_VERSION,
435                summary: summary.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
436                complexity: complexity.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
437                since: since.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
438                history: ptr::null_mut(), // currently we will not support history
439                tips: tips.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
440                arity: command_info.arity as c_int,
441                key_specs: key_specs.as_ptr() as *mut raw::RedisModuleCommandKeySpec,
442                args: ptr::null_mut(),
443            };
444
445            if unsafe { RedisModule_SetCommandInfo(command, &mut redis_command_info as *mut raw::RedisModuleCommandInfo) } == raw::Status::Err as i32 {
446                return Err(RedisError::String(format!(
447                    "Failed setting info for command {}.",
448                    command_info.name
449                )));
450            }
451
452            // the only CString pointers which are not freed are those of the key_specs, lets free them here.
453            key_specs.into_iter().for_each(|v|{
454                if !v.notes.is_null() {
455                    unsafe{CString::from_raw(v.notes as *mut c_char)};
456                }
457                if v.begin_search_type == raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_KEYWORD {
458                    let keyword = unsafe{v.bs.keyword.keyword};
459                    if !keyword.is_null() {
460                        unsafe{CString::from_raw(v.bs.keyword.keyword as *mut c_char)};
461                    }
462                }
463            });
464
465            Ok(())
466        })
467    }
468}
469
470#[cfg(all(
471    any(
472        feature = "min-redis-compatibility-version-7-2",
473        feature = "min-redis-compatibility-version-7-0"
474    ),
475    not(any(
476        feature = "min-redis-compatibility-version-6-2",
477        feature = "min-redis-compatibility-version-6-0"
478    ))
479))]
480pub fn register_commands(ctx: &Context) -> Status {
481    register_commands_internal(ctx).map_or_else(
482        |e| {
483            ctx.log_warning(&e.to_string());
484            Status::Err
485        },
486        |_| Status::Ok,
487    )
488}
489
490#[cfg(all(
491    any(
492        feature = "min-redis-compatibility-version-6-2",
493        feature = "min-redis-compatibility-version-6-0"
494    ),
495    not(any(
496        feature = "min-redis-compatibility-version-7-2",
497        feature = "min-redis-compatibility-version-7-0"
498    ))
499))]
500pub fn register_commands(ctx: &Context) -> Status {
501    register_commands_internal(ctx).map_or_else(
502        |e| {
503            ctx.log_warning(&e.to_string());
504            Status::Err
505        },
506        |v| {
507            v.map_or_else(
508                |e| {
509                    ctx.log_warning(&e.to_string());
510                    Status::Err
511                },
512                |_| Status::Ok,
513            )
514        },
515    )
516}