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