lean_ctx_patterns/
json_schema.rs1use crate::json_crush;
4
5pub fn compress(output: &str) -> Option<String> {
6 let trimmed = output.trim();
7
8 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
9 return None;
10 }
11
12 let val: serde_json::Value = match serde_json::from_str(trimmed) {
13 Ok(v) => v,
14 Err(_) => return None,
15 };
16
17 if let Some(text) = json_crush::crush_value_if_beneficial(&val, trimmed.len()) {
21 return Some(text);
22 }
23
24 let schema = extract_schema(&val, 0);
25 Some(schema)
26}
27
28pub fn crush_verbatim(output: &str) -> Option<String> {
34 json_crush::crush_text_if_beneficial(output)
35}
36
37fn extract_schema(val: &serde_json::Value, depth: usize) -> String {
38 let indent = " ".repeat(depth);
39 match val {
40 serde_json::Value::Object(map) => {
41 if map.is_empty() {
42 return format!("{indent}{{}}");
43 }
44 if depth > 3 {
45 return format!("{indent}{{...{} keys}}", map.len());
46 }
47
48 let mut entries = Vec::new();
49 for (key, value) in map.iter().take(20) {
50 let type_str = type_of(value);
51 match value {
52 serde_json::Value::Object(inner) if !inner.is_empty() && depth < 3 => {
53 let nested = extract_schema(value, depth + 1);
54 entries.push(format!("{indent} {key}: {{\n{nested}\n{indent} }}"));
55 }
56 serde_json::Value::Array(arr) if !arr.is_empty() => {
57 let item_type = if let Some(first) = arr.first() {
58 type_of(first)
59 } else {
60 "any".to_string()
61 };
62 entries.push(format!("{indent} {key}: [{item_type}...{}]", arr.len()));
63 }
64 _ => {
65 entries.push(format!("{indent} {key}: {type_str}"));
66 }
67 }
68 }
69 if map.len() > 20 {
70 entries.push(format!("{indent} ...+{} more keys", map.len() - 20));
71 }
72 entries.join("\n")
73 }
74 serde_json::Value::Array(arr) => {
75 if arr.is_empty() {
76 return format!("{indent}[]");
77 }
78 let first_schema = extract_schema(&arr[0], depth + 1);
79 format!(
80 "{indent}[{} items, each:\n{first_schema}\n{indent}]",
81 arr.len()
82 )
83 }
84 other => format!("{indent}{}", type_of(other)),
85 }
86}
87
88fn type_of(val: &serde_json::Value) -> String {
89 match val {
90 serde_json::Value::Null => "null".to_string(),
91 serde_json::Value::Bool(_) => "bool".to_string(),
92 serde_json::Value::Number(n) => {
93 if n.is_f64() {
94 "float".to_string()
95 } else {
96 "int".to_string()
97 }
98 }
99 serde_json::Value::String(s) => {
100 if s.len() > 50 {
101 format!("str({})", s.len())
102 } else {
103 "str".to_string()
104 }
105 }
106 serde_json::Value::Array(arr) => format!("[...{}]", arr.len()),
107 serde_json::Value::Object(map) => format!("{{...{} keys}}", map.len()),
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn redundant_array(n: usize) -> String {
118 let items: Vec<String> = (0..n)
119 .map(|i| {
120 format!(
121 "{{\"status\":\"ok\",\"region\":\"us-east-1\",\"tier\":\"standard\",\"id\":{i}}}"
122 )
123 })
124 .collect();
125 format!("[{}]", items.join(","))
126 }
127
128 #[test]
129 fn compress_prefers_lossless_crush_for_redundant_array() {
130 let raw = redundant_array(12);
131 let out = compress(&raw).expect("array-of-objects compresses");
132
133 assert!(out.contains("_lc_crush"), "expected crushed form: {out}");
136 assert!(!out.contains("items, each"), "schema outline leaked: {out}");
137
138 assert!(
140 out.len() * 2 <= raw.len(),
141 "crush must at least halve payload"
142 );
143
144 let restored = json_crush::reconstruct(&out).expect("crushed form reconstructs");
146 let expected: serde_json::Value = serde_json::from_str(&raw).unwrap();
147 assert_eq!(restored, expected, "roundtrip must be lossless");
148 }
149
150 #[test]
151 fn compress_falls_back_to_schema_for_heterogeneous_array() {
152 let raw = r#"[{"id":1,"name":"alice","email":"a@x.io"},{"id":2,"name":"bob","email":"b@y.io"},{"id":3,"name":"cara","email":"c@z.io"}]"#;
155 let out = compress(raw).expect("array compresses to schema");
156 assert!(
157 !out.contains("_lc_crush"),
158 "should not crush low-redundancy"
159 );
160 assert!(
161 out.contains("items, each"),
162 "expected schema outline: {out}"
163 );
164 }
165
166 #[test]
167 fn crush_verbatim_some_only_when_it_pays() {
168 let raw = redundant_array(20);
170 let crushed = crush_verbatim(&raw).expect("redundant verbatim json is crushed");
171 assert!(crushed.len() * 2 <= raw.len());
172 let restored = json_crush::reconstruct(&crushed).unwrap();
173 assert_eq!(
174 restored,
175 serde_json::from_str::<serde_json::Value>(&raw).unwrap()
176 );
177
178 let hetero = r#"[{"id":1,"k":"aaa"},{"id":2,"k":"bbb"}]"#;
180 assert!(crush_verbatim(hetero).is_none());
181
182 assert!(crush_verbatim("not json at all").is_none());
184 assert!(crush_verbatim("").is_none());
185 }
186
187 #[test]
188 fn compress_is_deterministic() {
189 let raw = redundant_array(15);
190 let a = compress(&raw).unwrap();
191 let b = compress(&raw).unwrap();
192 assert_eq!(a, b, "output must be a pure function of input");
193 }
194}