lean_ctx/core/patterns/
curl.rs1pub 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}