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