lean_ctx/core/patterns/
curl.rs1pub 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 s.len() > 50 {
56 format!("string({})", s.len())
57 } else {
58 format!("\"{}\"", s)
59 }
60 }
61 serde_json::Value::Array(arr) => {
62 if arr.is_empty() {
63 "[]".to_string()
64 } else {
65 let inner = value_type(&arr[0]);
66 format!("[{inner}; {}]", arr.len())
67 }
68 }
69 serde_json::Value::Object(inner) => {
70 if inner.len() <= 3 {
71 let keys: Vec<&String> = inner.keys().collect();
72 format!(
73 "{{{}}}",
74 keys.iter()
75 .map(|k| k.as_str())
76 .collect::<Vec<_>>()
77 .join(", ")
78 )
79 } else {
80 format!("{{{}K}}", inner.len())
81 }
82 }
83 };
84 lines.push(format!("{indent} {key}: {type_str}"));
85 }
86 if map.len() > 15 {
87 lines.push(format!("{indent} ... +{} more keys", map.len() - 15));
88 }
89 format!("{indent}{{\n{}\n{indent}}}", lines.join("\n"))
90 }
91 serde_json::Value::Array(arr) => {
92 if arr.is_empty() {
93 format!("{indent}[]")
94 } else {
95 let inner = value_type(&arr[0]);
96 format!("{indent}[{inner}; {}]", arr.len())
97 }
98 }
99 _ => format!("{indent}{}", value_type(val)),
100 }
101}
102
103fn value_type(val: &serde_json::Value) -> String {
104 match val {
105 serde_json::Value::Null => "null".to_string(),
106 serde_json::Value::Bool(_) => "bool".to_string(),
107 serde_json::Value::Number(_) => "number".to_string(),
108 serde_json::Value::String(_) => "string".to_string(),
109 serde_json::Value::Array(_) => "array".to_string(),
110 serde_json::Value::Object(m) => format!("object({}K)", m.len()),
111 }
112}
113
114fn compress_html(output: &str) -> Option<String> {
115 let lines = output.lines().count();
116 let size = output.len();
117
118 let title = output
119 .find("<title>")
120 .and_then(|start| {
121 let after = &output[start + 7..];
122 after.find("</title>").map(|end| &after[..end])
123 })
124 .unwrap_or("(no title)");
125
126 Some(format!("HTML: \"{title}\" ({size} bytes, {lines} lines)"))
127}
128
129fn compress_headers(output: &str) -> Option<String> {
130 let mut status = String::new();
131 let mut content_type = String::new();
132 let mut content_length = String::new();
133
134 for line in output.lines().take(20) {
135 if line.starts_with("HTTP/") {
136 status = line.to_string();
137 } else if line.to_lowercase().starts_with("content-type:") {
138 content_type = line.split(':').nth(1).unwrap_or("").trim().to_string();
139 } else if line.to_lowercase().starts_with("content-length:") {
140 content_length = line.split(':').nth(1).unwrap_or("").trim().to_string();
141 }
142 }
143
144 if status.is_empty() {
145 return None;
146 }
147
148 let mut result = status;
149 if !content_type.is_empty() {
150 result.push_str(&format!(" | {content_type}"));
151 }
152 if !content_length.is_empty() {
153 result.push_str(&format!(" | {content_length}B"));
154 }
155
156 Some(result)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn json_gets_compressed() {
165 let json = r#"{"name":"test","value":42,"nested":{"a":1,"b":2}}"#;
166 let result = compress(json);
167 assert!(result.is_some());
168 assert!(result.unwrap().contains("JSON"));
169 }
170
171 #[test]
172 fn html_gets_compressed() {
173 let html = "<!DOCTYPE html><html><head><title>Test</title></head><body></body></html>";
174 let result = compress(html);
175 assert!(result.is_some());
176 assert!(result.unwrap().contains("HTML"));
177 }
178
179 #[test]
180 fn plain_text_returns_none() {
181 assert!(compress("just plain text").is_none());
182 }
183}