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