redis_server_wrapper/stack.rs
1//! Redis Stack server detection and module loading.
2//!
3//! This module handles auto-detection of the `redis-stack-server` binary and
4//! the Stack modules (RedisJSON, RediSearch, etc.) bundled with it.
5
6use std::path::Path;
7
8/// Detect the best redis-server binary.
9///
10/// Prefers the real `redis-server` binary inside a redis-stack-server
11/// Homebrew cask (not the wrapper script which overrides `--dir`), then
12/// falls back to `redis-server` on PATH.
13///
14/// # Example
15///
16/// ```no_run
17/// use redis_server_wrapper::stack::detect_server_bin;
18///
19/// let bin = detect_server_bin();
20/// println!("using redis-server binary: {bin}");
21/// ```
22pub fn detect_server_bin() -> String {
23 let patterns = [
24 "/opt/homebrew/Caskroom/redis-stack-server/*/bin/redis-server",
25 "/usr/local/Caskroom/redis-stack-server/*/bin/redis-server",
26 ];
27
28 for pattern in &patterns {
29 if let Ok(mut paths) = glob::glob(pattern)
30 && let Some(Ok(path)) = paths.next()
31 && let Some(s) = path.to_str()
32 {
33 return s.to_string();
34 }
35 }
36
37 "redis-server".to_string()
38}
39
40/// Detect Redis Stack modules next to the given binary.
41///
42/// If the binary lives inside a redis-stack installation (a sibling `lib/`
43/// directory exists), returns `--loadmodule` arguments for each discovered
44/// module. Returns an empty vec if no modules are found.
45///
46/// Modules are checked in this order:
47/// - `rediscompat.so`
48/// - `redisearch.so` (with `MAXSEARCHRESULTS 10000 MAXAGGREGATERESULTS 10000`)
49/// - `redistimeseries.so`
50/// - `rejson.so`
51/// - `redisbloom.so`
52///
53/// # Example
54///
55/// ```no_run
56/// use redis_server_wrapper::stack::{detect_server_bin, detect_stack_modules};
57///
58/// let bin = detect_server_bin();
59/// let module_args = detect_stack_modules(&bin);
60/// println!("module args: {module_args:?}");
61/// ```
62pub fn detect_stack_modules(server_bin: &str) -> Vec<String> {
63 let bin_path = Path::new(server_bin);
64
65 let lib_dir = match bin_path.parent().and_then(|p| p.parent()) {
66 Some(base) => base.join("lib"),
67 None => return Vec::new(),
68 };
69
70 if !lib_dir.is_dir() {
71 return Vec::new();
72 }
73
74 // (filename, extra args)
75 let modules: &[(&str, &[&str])] = &[
76 ("rediscompat.so", &[]),
77 (
78 "redisearch.so",
79 &["MAXSEARCHRESULTS", "10000", "MAXAGGREGATERESULTS", "10000"],
80 ),
81 ("redistimeseries.so", &[]),
82 ("rejson.so", &[]),
83 ("redisbloom.so", &[]),
84 ];
85
86 let mut args: Vec<String> = Vec::new();
87
88 for (filename, extra) in modules {
89 let module_path = lib_dir.join(filename);
90 if module_path.is_file() {
91 args.push("--loadmodule".to_string());
92 args.push(module_path.to_string_lossy().into_owned());
93 for arg in *extra {
94 args.push(arg.to_string());
95 }
96 }
97 }
98
99 args
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn detect_server_bin_fallback() {
108 // Without a real redis-stack installation the function must fall back
109 // to the plain binary name.
110 let bin = detect_server_bin();
111 // Either the fallback or a real path -- just make sure it's non-empty.
112 assert!(!bin.is_empty());
113 }
114
115 #[test]
116 fn detect_stack_modules_missing_lib() {
117 // A binary that has no sibling lib/ dir should produce no module args.
118 let args = detect_stack_modules("redis-server");
119 assert!(args.is_empty());
120 }
121
122 #[test]
123 fn detect_stack_modules_nonexistent_path() {
124 let args = detect_stack_modules("/nonexistent/bin/redis-server");
125 assert!(args.is_empty());
126 }
127}