Skip to main content

lean_ctx/core/patterns/
curl.rs

1pub fn compress(output: &str) -> Option<String> {
2    let trimmed = output.trim();
3
4    if trimmed.starts_with('{') || trimmed.starts_with('[') {
5        return compress_json(trimmed);
6    }
7
8    if trimmed.starts_with("<!") || trimmed.starts_with("<html") {
9        return compress_html(trimmed);
10    }
11
12    if trimmed.starts_with("HTTP/") {
13        return compress_headers(trimmed);
14    }
15
16    None
17}
18
19fn compress_json(output: &str) -> Option<String> {
20    let val: serde_json::Value = serde_json::from_str(output).ok()?;
21    let schema = extract_schema(&val, 0);
22    let size = output.len();
23
24    Some(format!("JSON ({} bytes):\n{schema}", size))
25}
26
27fn extract_schema(val: &serde_json::Value, depth: usize) -> String {
28    if depth > 3 {
29        return "  ".repeat(depth) + "...";
30    }
31
32    let indent = "  ".repeat(depth);
33
34    match val {
35        serde_json::Value::Object(map) => {
36            let mut lines = Vec::new();
37            for (key, value) in map.iter().take(15) {
38                let type_str = match value {
39                    serde_json::Value::Null => "null".to_string(),
40                    serde_json::Value::Bool(_) => "bool".to_string(),
41                    serde_json::Value::Number(_) => "number".to_string(),
42                    serde_json::Value::String(s) => {
43                        if s.len() > 50 {
44                            format!("string({})", s.len())
45                        } else {
46                            format!("\"{}\"", s)
47                        }
48                    }
49                    serde_json::Value::Array(arr) => {
50                        if arr.is_empty() {
51                            "[]".to_string()
52                        } else {
53                            let inner = value_type(&arr[0]);
54                            format!("[{inner}; {}]", arr.len())
55                        }
56                    }
57                    serde_json::Value::Object(inner) => {
58                        if inner.len() <= 3 {
59                            let keys: Vec<&String> = inner.keys().collect();
60                            format!(
61                                "{{{}}}",
62                                keys.iter()
63                                    .map(|k| k.as_str())
64                                    .collect::<Vec<_>>()
65                                    .join(", ")
66                            )
67                        } else {
68                            format!("{{{}K}}", inner.len())
69                        }
70                    }
71                };
72                lines.push(format!("{indent}  {key}: {type_str}"));
73            }
74            if map.len() > 15 {
75                lines.push(format!("{indent}  ... +{} more keys", map.len() - 15));
76            }
77            format!("{indent}{{\n{}\n{indent}}}", lines.join("\n"))
78        }
79        serde_json::Value::Array(arr) => {
80            if arr.is_empty() {
81                format!("{indent}[]")
82            } else {
83                let inner = value_type(&arr[0]);
84                format!("{indent}[{inner}; {}]", arr.len())
85            }
86        }
87        _ => format!("{indent}{}", value_type(val)),
88    }
89}
90
91fn value_type(val: &serde_json::Value) -> String {
92    match val {
93        serde_json::Value::Null => "null".to_string(),
94        serde_json::Value::Bool(_) => "bool".to_string(),
95        serde_json::Value::Number(_) => "number".to_string(),
96        serde_json::Value::String(_) => "string".to_string(),
97        serde_json::Value::Array(_) => "array".to_string(),
98        serde_json::Value::Object(m) => format!("object({}K)", m.len()),
99    }
100}
101
102fn compress_html(output: &str) -> Option<String> {
103    let lines = output.lines().count();
104    let size = output.len();
105
106    let title = output
107        .find("<title>")
108        .and_then(|start| {
109            let after = &output[start + 7..];
110            after.find("</title>").map(|end| &after[..end])
111        })
112        .unwrap_or("(no title)");
113
114    Some(format!("HTML: \"{title}\" ({size} bytes, {lines} lines)"))
115}
116
117fn compress_headers(output: &str) -> Option<String> {
118    let mut status = String::new();
119    let mut content_type = String::new();
120    let mut content_length = String::new();
121
122    for line in output.lines().take(20) {
123        if line.starts_with("HTTP/") {
124            status = line.to_string();
125        } else if line.to_lowercase().starts_with("content-type:") {
126            content_type = line.split(':').nth(1).unwrap_or("").trim().to_string();
127        } else if line.to_lowercase().starts_with("content-length:") {
128            content_length = line.split(':').nth(1).unwrap_or("").trim().to_string();
129        }
130    }
131
132    if status.is_empty() {
133        return None;
134    }
135
136    let mut result = status;
137    if !content_type.is_empty() {
138        result.push_str(&format!(" | {content_type}"));
139    }
140    if !content_length.is_empty() {
141        result.push_str(&format!(" | {content_length}B"));
142    }
143
144    Some(result)
145}