Skip to main content

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}