Skip to main content

lean_ctx/server/
helpers.rs

1use serde_json::Value;
2
3pub fn get_str_array(
4    args: Option<&serde_json::Map<String, Value>>,
5    key: &str,
6) -> Option<Vec<String>> {
7    let val = args?.get(key)?;
8
9    // Normal path: native JSON array.
10    if let Some(arr) = val.as_array() {
11        let mut out = Vec::with_capacity(arr.len());
12        for v in arr {
13            out.push(v.as_str()?.to_string());
14        }
15        return Some(out);
16    }
17
18    // Fallback: some MCP bridges serialize arrays as JSON-encoded strings.
19    // Example: { "paths": "[\"src/main.rs\",\"src/lib.rs\"]" }
20    if let Some(s) = val.as_str() {
21        if let Ok(Value::Array(arr)) = serde_json::from_str::<Value>(s) {
22            let mut out = Vec::with_capacity(arr.len());
23            for v in &arr {
24                out.push(v.as_str()?.to_string());
25            }
26            return Some(out);
27        }
28    }
29
30    None
31}
32
33pub fn get_str(args: Option<&serde_json::Map<String, Value>>, key: &str) -> Option<String> {
34    args?
35        .get(key)?
36        .as_str()
37        .map(std::string::ToString::to_string)
38}
39
40pub fn get_int(args: Option<&serde_json::Map<String, Value>>, key: &str) -> Option<i64> {
41    args?.get(key)?.as_i64()
42}
43
44pub fn get_bool(args: Option<&serde_json::Map<String, Value>>, key: &str) -> Option<bool> {
45    args?.get(key)?.as_bool()
46}
47
48pub fn md5_hex(s: &str) -> String {
49    use md5::{Digest, Md5};
50    let mut hasher = Md5::new();
51    hasher.update(s.as_bytes());
52    format!("{:x}", hasher.finalize())
53}
54
55/// Fast MD5 fingerprint for dedup purposes.
56/// Hashes prefix + suffix + length for strings larger than 16 KB to avoid
57/// O(n) hashing on multi-megabyte tool outputs.
58pub fn md5_hex_fast(s: &str) -> String {
59    use md5::{Digest, Md5};
60    const THRESHOLD: usize = 16 * 1024;
61    let mut hasher = Md5::new();
62    if s.len() <= THRESHOLD {
63        hasher.update(s.as_bytes());
64    } else {
65        hasher.update(&s.as_bytes()[..8192]);
66        hasher.update(&s.as_bytes()[s.len() - 8192..]);
67        hasher.update(s.len().to_le_bytes());
68    }
69    format!("{:x}", hasher.finalize())
70}
71
72pub fn canonicalize_json(v: &Value) -> Value {
73    match v {
74        Value::Object(map) => {
75            let mut keys: Vec<&String> = map.keys().collect();
76            keys.sort();
77            let mut out = serde_json::Map::new();
78            for k in keys {
79                if let Some(val) = map.get(k) {
80                    out.insert(k.clone(), canonicalize_json(val));
81                }
82            }
83            Value::Object(out)
84        }
85        Value::Array(arr) => Value::Array(arr.iter().map(canonicalize_json).collect()),
86        other => other.clone(),
87    }
88}
89
90pub fn canonical_args_string(args: Option<&serde_json::Map<String, Value>>) -> String {
91    let v = args.map_or(Value::Null, |m| Value::Object(m.clone()));
92    let canon = canonicalize_json(&v);
93    serde_json::to_string(&canon).unwrap_or_default()
94}
95
96pub fn extract_search_pattern_from_command(command: &str) -> Option<String> {
97    let parts: Vec<&str> = command.split_whitespace().collect();
98    if parts.len() < 2 {
99        return None;
100    }
101    let cmd = parts[0];
102    if cmd == "grep" || cmd == "rg" || cmd == "ag" || cmd == "ack" {
103        for (i, part) in parts.iter().enumerate().skip(1) {
104            if !part.starts_with('-') {
105                return Some(part.to_string());
106            }
107            if (*part == "-e" || *part == "--regexp" || *part == "-m") && i + 1 < parts.len() {
108                return Some(parts[i + 1].to_string());
109            }
110        }
111    }
112    if cmd == "find" || cmd == "fd" {
113        for (i, part) in parts.iter().enumerate() {
114            if (*part == "-name" || *part == "-iname") && i + 1 < parts.len() {
115                return Some(
116                    parts[i + 1]
117                        .trim_matches('\'')
118                        .trim_matches('"')
119                        .to_string(),
120                );
121            }
122        }
123        if cmd == "fd" && parts.len() >= 2 && !parts[1].starts_with('-') {
124            return Some(parts[1].to_string());
125        }
126    }
127    None
128}