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::iter;
11use std::mem::MaybeUninit;
12use std::os::raw::c_int;
13use std::ptr;
14
15const COMMNAD_INFO_VERSION: raw::RedisModuleCommandInfoVersion =
16 raw::RedisModuleCommandInfoVersion {
17 version: 1,
18 sizeof_historyentry: std::mem::size_of::<raw::RedisModuleCommandHistoryEntry>(),
19 sizeof_keyspec: std::mem::size_of::<raw::RedisModuleCommandKeySpec>(),
20 sizeof_arg: std::mem::size_of::<raw::RedisModuleCommandArg>(),
21 };
22
23bitflags! {
24 pub struct KeySpecFlags : u32 {
40 const READ_ONLY = raw::REDISMODULE_CMD_KEY_RO;
42
43 const READ_WRITE = raw::REDISMODULE_CMD_KEY_RW;
45
46 const OVERWRITE = raw::REDISMODULE_CMD_KEY_OW;
48
49 const REMOVE = raw::REDISMODULE_CMD_KEY_RM;
51
52 const ACCESS = raw::REDISMODULE_CMD_KEY_ACCESS;
54
55 const UPDATE = raw::REDISMODULE_CMD_KEY_UPDATE;
57
58 const INSERT = raw::REDISMODULE_CMD_KEY_INSERT;
60
61 const DELETE = raw::REDISMODULE_CMD_KEY_DELETE;
63
64 const NOT_KEY = raw::REDISMODULE_CMD_KEY_NOT_KEY;
66
67 const INCOMPLETE = raw::REDISMODULE_CMD_KEY_INCOMPLETE;
69
70 const VARIABLE_FLAGS = raw::REDISMODULE_CMD_KEY_VARIABLE_FLAGS;
72 }
73}
74
75impl TryFrom<&str> for KeySpecFlags {
76 type Error = RedisError;
77 fn try_from(value: &str) -> Result<Self, Self::Error> {
78 match value.to_lowercase().as_str() {
79 "read_only" => Ok(KeySpecFlags::READ_ONLY),
80 "read_write" => Ok(KeySpecFlags::READ_WRITE),
81 "overwrite" => Ok(KeySpecFlags::OVERWRITE),
82 "remove" => Ok(KeySpecFlags::REMOVE),
83 "access" => Ok(KeySpecFlags::ACCESS),
84 "update" => Ok(KeySpecFlags::UPDATE),
85 "insert" => Ok(KeySpecFlags::INSERT),
86 "delete" => Ok(KeySpecFlags::DELETE),
87 "not_key" => Ok(KeySpecFlags::NOT_KEY),
88 "incomplete" => Ok(KeySpecFlags::INCOMPLETE),
89 "variable_flags" => Ok(KeySpecFlags::VARIABLE_FLAGS),
90 _ => Err(RedisError::String(format!(
91 "Value {value} is not a valid key spec flag."
92 ))),
93 }
94 }
95}
96
97impl From<Vec<KeySpecFlags>> for KeySpecFlags {
98 fn from(value: Vec<KeySpecFlags>) -> Self {
99 value
100 .into_iter()
101 .fold(KeySpecFlags::empty(), |a, item| a | item)
102 }
103}
104
105pub struct BeginSearchIndex {
109 index: i32,
110}
111
112pub struct BeginSearchKeyword {
116 keyword: String,
117 startfrom: i32,
118}
119
120pub enum BeginSearch {
125 Index(BeginSearchIndex),
126 Keyword(BeginSearchKeyword),
127}
128
129impl BeginSearch {
130 pub fn new_index(index: i32) -> BeginSearch {
131 BeginSearch::Index(BeginSearchIndex { index })
132 }
133
134 pub fn new_keyword(keyword: String, startfrom: i32) -> BeginSearch {
135 BeginSearch::Keyword(BeginSearchKeyword { keyword, startfrom })
136 }
137}
138
139impl From<&BeginSearch>
140 for (
141 raw::RedisModuleKeySpecBeginSearchType,
142 raw::RedisModuleCommandKeySpec__bindgen_ty_1,
143 )
144{
145 fn from(value: &BeginSearch) -> Self {
146 match value {
147 BeginSearch::Index(index_spec) => (
148 raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_INDEX,
149 raw::RedisModuleCommandKeySpec__bindgen_ty_1 {
150 index: raw::RedisModuleCommandKeySpec__bindgen_ty_1__bindgen_ty_1 {
151 pos: index_spec.index,
152 },
153 },
154 ),
155 BeginSearch::Keyword(keyword_spec) => {
156 let keyword = CString::new(keyword_spec.keyword.as_str())
157 .unwrap()
158 .into_raw();
159 (
160 raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_KEYWORD,
161 raw::RedisModuleCommandKeySpec__bindgen_ty_1 {
162 keyword: raw::RedisModuleCommandKeySpec__bindgen_ty_1__bindgen_ty_2 {
163 keyword,
164 startfrom: keyword_spec.startfrom,
165 },
166 },
167 )
168 }
169 }
170 }
171}
172
173pub struct FindKeysRange {
184 last_key: i32,
185 steps: i32,
186 limit: i32,
187}
188
189pub struct FindKeysNum {
198 key_num_idx: i32,
199 first_key: i32,
200 key_step: i32,
201}
202
203pub enum FindKeys {
212 Range(FindKeysRange),
213 Keynum(FindKeysNum),
214}
215
216impl FindKeys {
217 pub fn new_range(last_key: i32, steps: i32, limit: i32) -> FindKeys {
218 FindKeys::Range(FindKeysRange {
219 last_key,
220 steps,
221 limit,
222 })
223 }
224
225 pub fn new_keys_num(key_num_idx: i32, first_key: i32, key_step: i32) -> FindKeys {
226 FindKeys::Keynum(FindKeysNum {
227 key_num_idx,
228 first_key,
229 key_step,
230 })
231 }
232}
233
234impl From<&FindKeys>
235 for (
236 raw::RedisModuleKeySpecFindKeysType,
237 raw::RedisModuleCommandKeySpec__bindgen_ty_2,
238 )
239{
240 fn from(value: &FindKeys) -> Self {
241 match value {
242 FindKeys::Range(range_spec) => (
243 raw::RedisModuleKeySpecFindKeysType_REDISMODULE_KSPEC_FK_RANGE,
244 raw::RedisModuleCommandKeySpec__bindgen_ty_2 {
245 range: raw::RedisModuleCommandKeySpec__bindgen_ty_2__bindgen_ty_1 {
246 lastkey: range_spec.last_key,
247 keystep: range_spec.steps,
248 limit: range_spec.limit,
249 },
250 },
251 ),
252 FindKeys::Keynum(keynum_spec) => (
253 raw::RedisModuleKeySpecFindKeysType_REDISMODULE_KSPEC_FK_KEYNUM,
254 raw::RedisModuleCommandKeySpec__bindgen_ty_2 {
255 keynum: raw::RedisModuleCommandKeySpec__bindgen_ty_2__bindgen_ty_2 {
256 keynumidx: keynum_spec.key_num_idx,
257 firstkey: keynum_spec.first_key,
258 keystep: keynum_spec.key_step,
259 },
260 },
261 ),
262 }
263 }
264}
265
266pub struct KeySpec {
271 notes: Option<String>,
272 flags: KeySpecFlags,
273 begin_search: BeginSearch,
274 find_keys: FindKeys,
275}
276
277impl KeySpec {
278 pub fn new(
279 notes: Option<String>,
280 flags: KeySpecFlags,
281 begin_search: BeginSearch,
282 find_keys: FindKeys,
283 ) -> KeySpec {
284 KeySpec {
285 notes,
286 flags,
287 begin_search,
288 find_keys,
289 }
290 }
291}
292
293impl From<&KeySpec> for raw::RedisModuleCommandKeySpec {
294 fn from(value: &KeySpec) -> Self {
295 let (begin_search_type, bs) = (&value.begin_search).into();
296 let (find_keys_type, fk) = (&value.find_keys).into();
297 raw::RedisModuleCommandKeySpec {
298 notes: value
299 .notes
300 .as_ref()
301 .map(|v| CString::new(v.as_str()).unwrap().into_raw())
302 .unwrap_or(ptr::null_mut()),
303 flags: value.flags.bits() as u64,
304 begin_search_type,
305 bs,
306 find_keys_type,
307 fk,
308 }
309 }
310}
311
312type CommandCallback =
313 extern "C" fn(*mut raw::RedisModuleCtx, *mut *mut raw::RedisModuleString, i32) -> i32;
314
315bitflags! {
316 pub struct CommandArgFlags : u32 {
317 const NONE = raw::REDISMODULE_CMD_ARG_NONE;
318 const OPTIONAL = raw::REDISMODULE_CMD_ARG_OPTIONAL;
319 const MULTIPLE = raw::REDISMODULE_CMD_ARG_MULTIPLE;
320 const MULTIPLE_TOKEN = raw::REDISMODULE_CMD_ARG_MULTIPLE_TOKEN;
321 }
322}
323
324impl TryFrom<&str> for CommandArgFlags {
325 type Error = RedisError;
326 fn try_from(value: &str) -> Result<Self, Self::Error> {
327 match value.to_lowercase().as_str() {
328 "none" => Ok(CommandArgFlags::NONE),
329 "optional" => Ok(CommandArgFlags::OPTIONAL),
330 "multiple" => Ok(CommandArgFlags::MULTIPLE),
331 "multiple_token" => Ok(CommandArgFlags::MULTIPLE_TOKEN),
332 _ => Err(RedisError::String(format!(
333 "Value {value} is not a valid command arg flag."
334 ))),
335 }
336 }
337}
338
339impl From<Vec<CommandArgFlags>> for CommandArgFlags {
340 fn from(value: Vec<CommandArgFlags>) -> Self {
341 value
342 .into_iter()
343 .fold(CommandArgFlags::empty(), |a, item| a | item)
344 }
345}
346
347pub struct RedisModuleCommandArg {
348 name: String,
349 type_: u32,
350 key_spec_index: Option<u32>,
351 token: Option<String>,
352 summary: Option<String>,
353 since: Option<String>,
354 flags: CommandArgFlags,
355 deprecated_since: Option<String>,
356 subargs: Option<Vec<RedisModuleCommandArg>>,
357 display_text: Option<String>,
358}
359
360impl RedisModuleCommandArg {
361 #[expect(clippy::too_many_arguments)]
362 pub fn new(
363 name: String,
364 type_: u32,
365 key_spec_index: Option<u32>,
366 token: Option<String>,
367 summary: Option<String>,
368 since: Option<String>,
369 flags: CommandArgFlags,
370 deprecated_since: Option<String>,
371 subargs: Option<Vec<RedisModuleCommandArg>>,
372 display_text: Option<String>,
373 ) -> RedisModuleCommandArg {
374 RedisModuleCommandArg {
375 name,
376 type_,
377 key_spec_index,
378 token,
379 summary,
380 since,
381 flags,
382 deprecated_since,
383 subargs,
384 display_text,
385 }
386 }
387}
388
389pub struct CommandInfo {
391 name: String,
392 flags: Option<String>,
393 enterprise_flags: Option<String>,
394 summary: Option<String>,
395 complexity: Option<String>,
396 since: Option<String>,
397 tips: Option<String>,
398 arity: i64,
399 key_spec: Vec<KeySpec>,
400 callback: CommandCallback,
401 args: Vec<RedisModuleCommandArg>,
402 acl_categories: Option<Vec<String>>,
403}
404
405impl CommandInfo {
406 #[expect(clippy::too_many_arguments)]
407 pub fn new(
408 name: String,
409 flags: Option<String>,
410 enterprise_flags: Option<String>,
411 summary: Option<String>,
412 complexity: Option<String>,
413 since: Option<String>,
414 tips: Option<String>,
415 arity: i64,
416 key_spec: Vec<KeySpec>,
417 callback: CommandCallback,
418 args: Vec<RedisModuleCommandArg>,
419 acl_categories: Option<Vec<String>>,
420 ) -> CommandInfo {
421 CommandInfo {
422 name,
423 flags,
424 enterprise_flags,
425 summary,
426 complexity,
427 since,
428 tips,
429 arity,
430 key_spec,
431 callback,
432 args,
433 acl_categories,
434 }
435 }
436}
437
438#[distributed_slice()]
439pub static COMMANDS_LIST: [fn() -> Result<CommandInfo, RedisError>] = [..];
440
441pub fn get_redis_key_spec(key_spec: Vec<KeySpec>) -> Vec<raw::RedisModuleCommandKeySpec> {
442 let mut redis_key_spec: Vec<raw::RedisModuleCommandKeySpec> =
443 key_spec.into_iter().map(|v| (&v).into()).collect();
444 let zerod: raw::RedisModuleCommandKeySpec = unsafe { MaybeUninit::zeroed().assume_init() };
445 redis_key_spec.push(zerod);
446 redis_key_spec
447}
448
449fn convert_command_arg_to_raw(arg: &RedisModuleCommandArg) -> raw::RedisModuleCommandArg {
450 let name = CString::new(arg.name.as_str()).unwrap().into_raw();
451 let token = arg
452 .token
453 .as_ref()
454 .map(|v| CString::new(v.as_str()).unwrap().into_raw())
455 .unwrap_or(ptr::null_mut());
456 let summary = arg
457 .summary
458 .as_ref()
459 .map(|v| CString::new(v.as_str()).unwrap().into_raw())
460 .unwrap_or(ptr::null_mut());
461 let since = arg
462 .since
463 .as_ref()
464 .map(|v| CString::new(v.as_str()).unwrap().into_raw())
465 .unwrap_or(ptr::null_mut());
466 let deprecated_since = arg
467 .deprecated_since
468 .as_ref()
469 .map(|v| CString::new(v.as_str()).unwrap().into_raw())
470 .unwrap_or(ptr::null_mut());
471 let display_text = arg
472 .display_text
473 .as_ref()
474 .map(|v| CString::new(v.as_str()).unwrap().into_raw())
475 .unwrap_or(ptr::null_mut());
476
477 let subargs = arg.subargs.as_ref().map_or(ptr::null_mut(), |v| {
478 Box::into_raw(
479 v.iter()
480 .map(convert_command_arg_to_raw)
481 .chain(iter::once(unsafe { MaybeUninit::zeroed().assume_init() }))
482 .collect::<Vec<_>>()
483 .into_boxed_slice(),
484 )
485 .cast()
486 });
487
488 raw::RedisModuleCommandArg {
489 name,
490 type_: arg.type_,
491 key_spec_index: arg.key_spec_index.unwrap_or(u32::MAX) as c_int,
492 token,
493 summary,
494 since,
495 flags: arg.flags.bits() as c_int,
496 deprecated_since,
497 subargs,
498 display_text,
499 }
500}
501
502pub fn get_redis_command_args(
503 args: Vec<RedisModuleCommandArg>,
504) -> Option<Vec<raw::RedisModuleCommandArg>> {
505 if args.is_empty() {
506 return None;
507 }
508
509 let raw_args: Vec<raw::RedisModuleCommandArg> = args
510 .iter()
511 .map(convert_command_arg_to_raw)
512 .chain(iter::once(unsafe { MaybeUninit::zeroed().assume_init() }))
513 .collect();
514
515 Some(raw_args)
516}
517
518fn free_command_arg(arg: &raw::RedisModuleCommandArg) {
519 if !arg.name.is_null() {
520 drop(unsafe { CString::from_raw(arg.name as *mut c_char) });
521 }
522 if !arg.token.is_null() {
523 drop(unsafe { CString::from_raw(arg.token as *mut c_char) });
524 }
525 if !arg.summary.is_null() {
526 drop(unsafe { CString::from_raw(arg.summary as *mut c_char) });
527 }
528 if !arg.since.is_null() {
529 drop(unsafe { CString::from_raw(arg.since as *mut c_char) });
530 }
531 if !arg.deprecated_since.is_null() {
532 drop(unsafe { CString::from_raw(arg.deprecated_since as *mut c_char) });
533 }
534 if !arg.display_text.is_null() {
535 drop(unsafe { CString::from_raw(arg.display_text as *mut c_char) });
536 }
537
538 if !arg.subargs.is_null() {
539 let mut i = 0;
540 loop {
541 let subarg_ptr = unsafe { arg.subargs.offset(i) };
542 if unsafe { (*subarg_ptr).name.is_null() } {
543 break;
544 }
545 free_command_arg(unsafe { &*subarg_ptr });
546 i += 1;
547 }
548 let len = i as usize + 1;
549 drop(unsafe { Vec::from_raw_parts(arg.subargs, len, len) });
550 }
551}
552
553api! {[
554 RedisModule_CreateCommand,
555 RedisModule_GetCommand,
556 RedisModule_SetCommandInfo,
557 RedisModule_SetCommandACLCategories,
558 ],
559 fn register_commands_internal(ctx: &Context) -> Result<(), RedisError> {
561 let is_enterprise = ctx.is_enterprise();
562 COMMANDS_LIST.iter().try_for_each(|command| {
563 let command_info = command()?;
564 let name: CString = CString::new(command_info.name.as_str()).unwrap();
565 let mut flags = command_info.flags.as_deref().unwrap_or("").to_owned();
566 if is_enterprise {
567 flags = format!("{flags} {}", command_info.enterprise_flags.as_deref().unwrap_or("")).trim().to_owned();
568 }
569 let flags = CString::new(flags).map_err(|e| RedisError::String(e.to_string()))?;
570
571 if unsafe {
572 RedisModule_CreateCommand(
573 ctx.ctx,
574 name.as_ptr(),
575 Some(command_info.callback),
576 flags.as_ptr(),
577 0,
578 0,
579 0,
580 )
581 } == raw::Status::Err as i32
582 {
583 return Err(RedisError::String(format!(
584 "Failed register command {}.",
585 command_info.name
586 )));
587 }
588
589 let command = unsafe { RedisModule_GetCommand(ctx.ctx, name.as_ptr()) };
591
592 if command.is_null() {
593 return Err(RedisError::String(format!(
594 "Failed finding command {} after registration.",
595 command_info.name
596 )));
597 }
598
599 if let Some(acl_categories) = command_info.acl_categories {
600 let acl_categories = CString::new(acl_categories.join(" ")).map_err(|e| RedisError::String(e.to_string()))?;
601 if unsafe { RedisModule_SetCommandACLCategories(command, acl_categories.as_ptr()) } == raw::Status::Err as i32 {
602 return Err(RedisError::String(format!(
603 "Failed setting ACL categories for command {}.",
604 command_info.name
605 )));
606 }
607 }
608
609 let summary = command_info
610 .summary
611 .as_ref()
612 .map(|v| Some(CString::new(v.as_str()).unwrap()))
613 .unwrap_or(None);
614 let complexity = command_info
615 .complexity
616 .as_ref()
617 .map(|v| Some(CString::new(v.as_str()).unwrap()))
618 .unwrap_or(None);
619 let since = command_info
620 .since
621 .as_ref()
622 .map(|v| Some(CString::new(v.as_str()).unwrap()))
623 .unwrap_or(None);
624 let tips = command_info
625 .tips
626 .as_ref()
627 .map(|v| Some(CString::new(v.as_str()).unwrap()))
628 .unwrap_or(None);
629
630 let key_specs = get_redis_key_spec(command_info.key_spec);
631
632 let args = get_redis_command_args(command_info.args);
633
634 let mut redis_command_info = raw::RedisModuleCommandInfo {
635 version: &COMMNAD_INFO_VERSION,
636 summary: summary.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
637 complexity: complexity.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
638 since: since.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
639 history: ptr::null_mut(), tips: tips.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null_mut()),
641 arity: command_info.arity as c_int,
642 key_specs: key_specs.as_ptr() as *mut raw::RedisModuleCommandKeySpec,
643 args: args.as_ref().map(Vec::as_ptr).unwrap_or(ptr::null_mut()) as *mut raw::RedisModuleCommandArg,
644 };
645
646 if unsafe { RedisModule_SetCommandInfo(command, &mut redis_command_info as *mut raw::RedisModuleCommandInfo) } == raw::Status::Err as i32 {
647 return Err(RedisError::String(format!(
648 "Failed setting info for command {}.",
649 command_info.name
650 )));
651 }
652
653 key_specs.into_iter().for_each(|v|{
655 if !v.notes.is_null() {
656 drop(unsafe{CString::from_raw(v.notes as *mut c_char)});
657 }
658 if v.begin_search_type == raw::RedisModuleKeySpecBeginSearchType_REDISMODULE_KSPEC_BS_KEYWORD {
659 let keyword = unsafe{v.bs.keyword.keyword};
660 if !keyword.is_null() {
661 drop(unsafe{CString::from_raw(v.bs.keyword.keyword as *mut c_char)});
662 }
663 }
664 });
665
666 args.unwrap_or_default().iter().for_each(free_command_arg);
667
668 Ok(())
669 })
670 }
671}
672
673#[cfg(all(
674 any(
675 feature = "min-redis-compatibility-version-8-0",
676 feature = "min-redis-compatibility-version-7-4",
677 feature = "min-redis-compatibility-version-7-2",
678 feature = "min-redis-compatibility-version-7-0"
679 ),
680 not(any(
681 feature = "min-redis-compatibility-version-6-2",
682 feature = "min-redis-compatibility-version-6-0"
683 ))
684))]
685pub fn register_commands(ctx: &Context) -> Status {
686 register_commands_internal(ctx).map_or_else(
687 |e| {
688 ctx.log_warning(&e.to_string());
689 Status::Err
690 },
691 |_| Status::Ok,
692 )
693}
694
695#[cfg(all(
696 any(
697 feature = "min-redis-compatibility-version-6-2",
698 feature = "min-redis-compatibility-version-6-0"
699 ),
700 not(any(
701 feature = "min-redis-compatibility-version-8-0",
702 feature = "min-redis-compatibility-version-7-4",
703 feature = "min-redis-compatibility-version-7-2",
704 feature = "min-redis-compatibility-version-7-0"
705 ))
706))]
707pub fn register_commands(ctx: &Context) -> Status {
708 register_commands_internal(ctx).map_or_else(
709 |e| {
710 if COMMANDS_LIST.is_empty() {
712 return Status::Ok;
713 }
714 ctx.log_warning(&e.to_string());
715 Status::Err
716 },
717 |v| {
718 v.map_or_else(
719 |e| {
720 ctx.log_warning(&e.to_string());
721 Status::Err
722 },
723 |_| Status::Ok,
724 )
725 },
726 )
727}