1use serde::de::DeserializeOwned;
12
13#[derive(Debug, thiserror::Error)]
15pub enum JsonRepairError {
16 #[error("JSON repair failed: {0}")]
18 RepairFailed(String),
19
20 #[error("Deserialization failed: {details}")]
22 DeserializationFailed { details: String },
23}
24
25pub fn parse_tolerant_json<T: DeserializeOwned>(raw: &str) -> Result<T, JsonRepairError> {
36 let repaired = repair_json(raw)?;
37 serde_json::from_str::<T>(&repaired).map_err(|e| JsonRepairError::DeserializationFailed {
38 details: format!("{} (repaired JSON: {})", e, truncate(&repaired, 200)),
39 })
40}
41
42pub fn repair_json(raw: &str) -> Result<String, JsonRepairError> {
46 let trimmed = raw.trim();
47
48 let stripped = strip_code_fences(trimmed);
50
51 let extracted = extract_bracket_pair(&stripped);
53
54 let quotes_fixed = fix_unescaped_quotes(&extracted);
56
57 let no_trailing = remove_trailing_commas("es_fixed);
59
60 let truncated = truncate_to_matching_bracket(&no_trailing);
62
63 if truncated.is_empty() {
65 return Err(JsonRepairError::RepairFailed(
66 "no JSON content found".to_string(),
67 ));
68 }
69
70 Ok(truncated)
71}
72
73pub fn strip_code_fences(text: &str) -> String {
75 let trimmed = text.trim();
76 if let Some(rest) = trimmed.strip_prefix("```json") {
77 if let Some(end) = rest.find("```") {
78 return rest[..end].trim().to_string();
79 }
80 return rest.trim().to_string();
82 }
83 if let Some(rest) = trimmed.strip_prefix("```") {
84 if let Some(end) = rest.find("```") {
85 return rest[..end].trim().to_string();
86 }
87 return rest.trim().to_string();
88 }
89 trimmed.to_string()
90}
91
92pub fn extract_bracket_pair(text: &str) -> String {
94 let bytes = text.as_bytes();
95 let start_idx = text.find(['[', '{']);
96
97 if let Some(start) = start_idx {
98 let open = bytes[start];
99 let close = if open == b'[' { b']' } else { b'}' };
100
101 let mut depth = 0i32;
102 let mut in_string = false;
103 let mut escape_next = false;
104
105 for i in start..bytes.len() {
106 let ch = bytes[i];
107 if escape_next {
108 escape_next = false;
109 continue;
110 }
111 if ch == b'\\' && in_string {
112 escape_next = true;
113 continue;
114 }
115 if ch == b'"' {
116 in_string = !in_string;
117 continue;
118 }
119 if in_string {
120 continue;
121 }
122 if ch == open {
123 depth += 1;
124 } else if ch == close {
125 depth -= 1;
126 if depth == 0 {
127 return text[start..=i].to_string();
128 }
129 }
130 }
131 }
132
133 text.to_string()
134}
135
136pub fn fix_unescaped_quotes(json: &str) -> String {
146 let chars: Vec<char> = json.chars().collect();
147 let n = chars.len();
148 let mut result = String::with_capacity(json.len());
149 let mut in_string = false;
150 let mut escape_next = false;
151
152 for i in 0..n {
153 let c = chars[i];
154
155 if in_string {
156 if escape_next {
157 result.push(c);
158 escape_next = false;
159 continue;
160 }
161 if c == '\\' {
162 result.push(c);
163 escape_next = true;
164 continue;
165 }
166 if c == '"' {
167 let mut j = i + 1;
169 while j < n && chars[j].is_whitespace() {
170 j += 1;
171 }
172 let is_closing = j >= n || matches!(chars[j], ':' | ',' | ']' | '}');
173 if is_closing {
174 in_string = false;
175 result.push(c);
176 } else {
177 result.push('\\');
179 result.push(c);
180 }
181 continue;
182 }
183 result.push(c);
184 } else if c == '"' {
185 in_string = true;
186 result.push(c);
187 } else {
188 result.push(c);
189 }
190 }
191
192 result
193}
194
195pub fn remove_trailing_commas(json: &str) -> String {
199 let mut result = String::with_capacity(json.len());
200 let chars: Vec<char> = json.chars().collect();
201 let len = chars.len();
202
203 for i in 0..len {
204 let ch = chars[i];
205 if ch == ',' {
207 let mut j = i + 1;
208 while j < len && chars[j].is_whitespace() {
209 j += 1;
210 }
211 if j < len && (chars[j] == ']' || chars[j] == '}') {
212 continue;
214 }
215 }
216 result.push(ch);
217 }
218
219 result
220}
221
222pub fn truncate_to_matching_bracket(json: &str) -> String {
224 let trimmed = json.trim_end();
226 if trimmed.ends_with(']') || trimmed.ends_with('}') {
227 return trimmed.to_string();
228 }
229
230 if let Some(last_close) = trimmed.rfind([']', '}']) {
232 return trimmed[..=last_close].to_string();
233 }
234
235 json.to_string()
236}
237
238fn truncate(s: &str, max_len: usize) -> String {
240 if s.len() <= max_len {
241 s.to_string()
242 } else {
243 let end = s
244 .char_indices()
245 .take(max_len)
246 .last()
247 .map(|(i, _)| i)
248 .unwrap_or(0);
249 format!("{}...", &s[..end])
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use serde::Deserialize;
257
258 #[derive(Debug, Deserialize, PartialEq)]
259 struct TestStruct {
260 name: String,
261 value: i32,
262 }
263
264 #[test]
265 fn test_parse_valid_json() {
266 let raw = r#"{"name": "test", "value": 42}"#;
267 let result: TestStruct = parse_tolerant_json(raw).unwrap();
268 assert_eq!(result.name, "test");
269 assert_eq!(result.value, 42);
270 }
271
272 #[test]
273 fn test_parse_json_with_code_fence() {
274 let raw = "```json\n{\"name\": \"test\", \"value\": 42}\n```";
275 let result: TestStruct = parse_tolerant_json(raw).unwrap();
276 assert_eq!(result.name, "test");
277 }
278
279 #[test]
280 fn test_parse_json_with_trailing_comma() {
281 let raw = r#"{"name": "test", "value": 42,}"#;
282 let result: TestStruct = parse_tolerant_json(raw).unwrap();
283 assert_eq!(result.name, "test");
284 assert_eq!(result.value, 42);
285 }
286
287 #[test]
288 fn test_parse_json_with_surrounding_text() {
289 let raw = "Here is the result: {\"name\": \"test\", \"value\": 42} done.";
290 let result: TestStruct = parse_tolerant_json(raw).unwrap();
291 assert_eq!(result.name, "test");
292 }
293
294 #[test]
295 fn test_parse_json_with_trailing_garbage() {
296 let raw = r#"{"name": "test", "value": 42} and some extra text"#;
297 let result: TestStruct = parse_tolerant_json(raw).unwrap();
298 assert_eq!(result.name, "test");
299 }
300
301 #[test]
302 fn test_parse_json_with_unescaped_inner_quotes() {
303 let raw = r#"{"name": "He said "hi" and left", "value": 1}"#;
305 let result: TestStruct = parse_tolerant_json(raw).unwrap();
306 assert_eq!(result.name, "He said \"hi\" and left");
307 assert_eq!(result.value, 1);
308 }
309
310 #[test]
311 fn test_parse_json_array_with_trailing_comma() {
312 let raw = r#"[{"name": "a", "value": 1}, {"name": "b", "value": 2},]"#;
313 let result: Vec<TestStruct> = parse_tolerant_json(raw).unwrap();
314 assert_eq!(result.len(), 2);
315 }
316
317 #[test]
318 fn test_parse_empty_text_fails() {
319 let result: Result<TestStruct, _> = parse_tolerant_json("");
320 assert!(result.is_err());
321 }
322
323 #[test]
324 fn test_parse_no_json_content_fails() {
325 let result: Result<TestStruct, _> = parse_tolerant_json("just some plain text");
326 assert!(result.is_err());
327 }
328
329 #[test]
330 fn test_fix_unescaped_quotes_inner_only() {
331 let input = r#"{"a": "He said "hi"", "b": 2}"#;
332 assert_eq!(
333 fix_unescaped_quotes(input),
334 r#"{"a": "He said \"hi\"", "b": 2}"#
335 );
336 }
337
338 #[test]
339 fn test_fix_unescaped_quotes_leaves_closing_quotes() {
340 let input = r#"{"key": "value", "n": 1}"#;
341 assert_eq!(fix_unescaped_quotes(input), input);
342 }
343
344 #[test]
345 fn test_fix_unescaped_quotes_empty_string() {
346 let input = r#"{"a": ""}"#;
347 assert_eq!(fix_unescaped_quotes(input), input);
348 }
349
350 #[test]
351 fn test_strip_code_fences_json() {
352 let input = "```json\n{\"key\": \"val\"}\n```";
353 assert_eq!(strip_code_fences(input), "{\"key\": \"val\"}");
354 }
355
356 #[test]
357 fn test_strip_code_fences_plain() {
358 let input = "```\n{\"key\": \"val\"}\n```";
359 assert_eq!(strip_code_fences(input), "{\"key\": \"val\"}");
360 }
361
362 #[test]
363 fn test_strip_code_fences_no_fence() {
364 let input = "{\"key\": \"val\"}";
365 assert_eq!(strip_code_fences(input), "{\"key\": \"val\"}");
366 }
367
368 #[test]
369 fn test_remove_trailing_commas_object() {
370 let input = r#"{"a": 1, "b": 2,}"#;
371 assert_eq!(remove_trailing_commas(input), r#"{"a": 1, "b": 2}"#);
372 }
373
374 #[test]
375 fn test_remove_trailing_commas_array() {
376 let input = r#"[1, 2, 3,]"#;
377 assert_eq!(remove_trailing_commas(input), r#"[1, 2, 3]"#);
378 }
379
380 #[test]
381 fn test_remove_trailing_commas_nested() {
382 let input = r#"{"arr": [1, 2,], "val": 3,}"#;
383 assert_eq!(
384 remove_trailing_commas(input),
385 r#"{"arr": [1, 2], "val": 3}"#
386 );
387 }
388
389 #[test]
390 fn test_extract_bracket_pair_array() {
391 let input = "prefix [1, 2, 3] suffix";
392 assert_eq!(extract_bracket_pair(input), "[1, 2, 3]");
393 }
394
395 #[test]
396 fn test_extract_bracket_pair_object() {
397 let input = r#"text {"a": 1} more"#;
398 assert_eq!(extract_bracket_pair(input), r#"{"a": 1}"#);
399 }
400
401 #[test]
402 fn test_parse_json_no_closing_fence() {
403 let raw = "```json\n{\"name\": \"test\", \"value\": 42}";
404 let result: TestStruct = parse_tolerant_json(raw).unwrap();
405 assert_eq!(result.name, "test");
406 }
407
408 #[test]
409 fn test_error_display() {
410 let err = JsonRepairError::RepairFailed("no json".to_string());
411 assert!(err.to_string().contains("no json"));
412 }
413}