Skip to main content

mem7_core/
json.rs

1use serde::de::DeserializeOwned;
2
3/// Strip markdown code fences from an LLM response and deserialize as JSON.
4///
5/// Returns `Err(String)` with a descriptive message on failure so callers can
6/// wrap it into their own `Mem7Error` variant.
7pub fn parse_json_response<T: DeserializeOwned>(raw: &str) -> Result<T, String> {
8    let trimmed = raw.trim();
9    let cleaned = if trimmed.starts_with("```json") {
10        trimmed
11            .trim_start_matches("```json")
12            .trim_end_matches("```")
13            .trim()
14    } else if trimmed.starts_with("```") {
15        trimmed
16            .trim_start_matches("```")
17            .trim_end_matches("```")
18            .trim()
19    } else {
20        trimmed
21    };
22
23    serde_json::from_str(cleaned).map_err(|e| format!("JSON parse error: {e}\nRaw: {raw}"))
24}
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29    use serde::Deserialize;
30
31    #[derive(Debug, Deserialize)]
32    struct Sample {
33        value: String,
34    }
35
36    #[test]
37    fn plain_json() {
38        let r: Sample = parse_json_response(r#"{"value": "ok"}"#).unwrap();
39        assert_eq!(r.value, "ok");
40    }
41
42    #[test]
43    fn json_with_code_fence() {
44        let r: Sample = parse_json_response("```json\n{\"value\": \"fenced\"}\n```").unwrap();
45        assert_eq!(r.value, "fenced");
46    }
47
48    #[test]
49    fn json_with_generic_fence() {
50        let r: Sample = parse_json_response("```\n{\"value\": \"generic\"}\n```").unwrap();
51        assert_eq!(r.value, "generic");
52    }
53
54    #[test]
55    fn bad_json_returns_err() {
56        let r = parse_json_response::<Sample>("not json");
57        assert!(r.is_err());
58        assert!(r.unwrap_err().contains("JSON parse error"));
59    }
60}