Skip to main content

lean_ctx/core/patterns/
curl.rs

1pub fn compress_with_cmd(command: &str, output: &str) -> Option<String> {
2    let cfg = crate::core::config::Config::load();
3    if !cfg.passthrough_urls.is_empty() {
4        for url in &cfg.passthrough_urls {
5            if command.contains(url.as_str()) {
6                return None;
7            }
8        }
9    }
10    compress(output)
11}
12
13pub fn compress(output: &str) -> Option<String> {
14    let trimmed = output.trim();
15
16    if trimmed.starts_with('{') || trimmed.starts_with('[') {
17        return compress_json(trimmed);
18    }
19
20    if trimmed.starts_with("<!") || trimmed.starts_with("<html") {
21        return compress_html(trimmed);
22    }
23
24    if trimmed.starts_with("HTTP/") {
25        return compress_headers(trimmed);
26    }
27
28    None
29}
30
31fn compress_json(output: &str) -> Option<String> {
32    let val: serde_json::Value = serde_json::from_str(output).ok()?;
33    let schema = extract_schema(&val, 0);
34    let size = output.len();
35
36    Some(format!("JSON ({} bytes):\n{schema}", size))
37}
38
39fn extract_schema(val: &serde_json::Value, depth: usize) -> String {
40    if depth > 3 {
41        return "  ".repeat(depth) + "...";
42    }
43
44    let indent = "  ".repeat(depth);
45
46    match val {
47        serde_json::Value::Object(map) => {
48            let mut lines = Vec::new();
49            for (key, value) in map.iter().take(15) {
50                let type_str = match value {
51                    serde_json::Value::Null => "null".to_string(),
52                    serde_json::Value::Bool(_) => "bool".to_string(),
53                    serde_json::Value::Number(_) => "number".to_string(),
54                    serde_json::Value::String(s) => {
55                        if is_sensitive_key(key) {
56                            format!("string({}, REDACTED)", s.len())
57                        } else if s.len() > 50 {
58                            format!("string({})", s.len())
59                        } else {
60                            format!("\"{}\"", s)
61                        }
62                    }
63                    serde_json::Value::Array(arr) => {
64                        if arr.is_empty() {
65                            "[]".to_string()
66                        } else {
67                            let inner = value_type(&arr[0]);
68                            format!("[{inner}; {}]", arr.len())
69                        }
70                    }
71                    serde_json::Value::Object(inner) => {
72                        if inner.len() <= 3 {
73                            let keys: Vec<&String> = inner.keys().collect();
74                            format!(
75                                "{{{}}}",
76                                keys.iter()
77                                    .map(|k| k.as_str())
78                                    .collect::<Vec<_>>()
79                                    .join(", ")
80                            )
81                        } else {
82                            format!("{{{}K}}", inner.len())
83                        }
84                    }
85                };
86                lines.push(format!("{indent}  {key}: {type_str}"));
87            }
88            if map.len() > 15 {
89                lines.push(format!("{indent}  ... +{} more keys", map.len() - 15));
90            }
91            format!("{indent}{{\n{}\n{indent}}}", lines.join("\n"))
92        }
93        serde_json::Value::Array(arr) => {
94            if arr.is_empty() {
95                format!("{indent}[]")
96            } else {
97                let inner = value_type(&arr[0]);
98                format!("{indent}[{inner}; {}]", arr.len())
99            }
100        }
101        _ => format!("{indent}{}", value_type(val)),
102    }
103}
104
105fn value_type(val: &serde_json::Value) -> String {
106    match val {
107        serde_json::Value::Null => "null".to_string(),
108        serde_json::Value::Bool(_) => "bool".to_string(),
109        serde_json::Value::Number(_) => "number".to_string(),
110        serde_json::Value::String(_) => "string".to_string(),
111        serde_json::Value::Array(_) => "array".to_string(),
112        serde_json::Value::Object(m) => format!("object({}K)", m.len()),
113    }
114}
115
116fn is_sensitive_key(key: &str) -> bool {
117    let lower = key.to_ascii_lowercase();
118    lower.contains("token")
119        || lower.contains("key")
120        || lower.contains("secret")
121        || lower.contains("password")
122        || lower.contains("passwd")
123        || lower.contains("auth")
124        || lower.contains("credential")
125        || lower.contains("api_key")
126        || lower.contains("apikey")
127        || lower.contains("access_token")
128        || lower.contains("refresh_token")
129        || lower.contains("private")
130}
131
132fn compress_html(output: &str) -> Option<String> {
133    let lines = output.lines().count();
134    let size = output.len();
135
136    let title = output
137        .find("<title>")
138        .and_then(|start| {
139            let after = &output[start + 7..];
140            after.find("</title>").map(|end| &after[..end])
141        })
142        .unwrap_or("(no title)");
143
144    Some(format!("HTML: \"{title}\" ({size} bytes, {lines} lines)"))
145}
146
147fn compress_headers(output: &str) -> Option<String> {
148    let mut status = String::new();
149    let mut content_type = String::new();
150    let mut content_length = String::new();
151
152    for line in output.lines().take(20) {
153        if line.starts_with("HTTP/") {
154            status = line.to_string();
155        } else if line.to_lowercase().starts_with("content-type:") {
156            content_type = line.split(':').nth(1).unwrap_or("").trim().to_string();
157        } else if line.to_lowercase().starts_with("content-length:") {
158            content_length = line.split(':').nth(1).unwrap_or("").trim().to_string();
159        }
160    }
161
162    if status.is_empty() {
163        return None;
164    }
165
166    let mut result = status;
167    if !content_type.is_empty() {
168        result.push_str(&format!(" | {content_type}"));
169    }
170    if !content_length.is_empty() {
171        result.push_str(&format!(" | {content_length}B"));
172    }
173
174    Some(result)
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn json_gets_compressed() {
183        let json = r#"{"name":"test","value":42,"nested":{"a":1,"b":2}}"#;
184        let result = compress(json);
185        assert!(result.is_some());
186        assert!(result.unwrap().contains("JSON"));
187    }
188
189    #[test]
190    fn html_gets_compressed() {
191        let html = "<!DOCTYPE html><html><head><title>Test</title></head><body></body></html>";
192        let result = compress(html);
193        assert!(result.is_some());
194        assert!(result.unwrap().contains("HTML"));
195    }
196
197    #[test]
198    fn plain_text_returns_none() {
199        assert!(compress("just plain text").is_none());
200    }
201}