recall_echo/graph/
util.rs1use chrono::{DateTime, Utc};
4
5#[must_use]
7pub fn strip_markdown_fencing(text: &str) -> String {
8 let trimmed = text.trim();
9 let stripped = trimmed
10 .strip_prefix("```json")
11 .or(trimmed.strip_prefix("```"))
12 .unwrap_or(trimmed);
13 let stripped = stripped.strip_suffix("```").unwrap_or(stripped);
14 stripped.trim().to_string()
15}
16
17#[must_use]
21pub fn extract_json_object(text: &str) -> Option<&str> {
22 let start = text.find('{')?;
23 let mut depth = 0;
24 let bytes = text.as_bytes();
25 for (i, &b) in bytes[start..].iter().enumerate() {
26 match b {
27 b'{' => depth += 1,
28 b'}' => {
29 depth -= 1;
30 if depth == 0 {
31 return Some(&text[start..start + i + 1]);
32 }
33 }
34 _ => {}
35 }
36 }
37 None
38}
39
40#[must_use]
44pub fn parse_datetime(val: &serde_json::Value) -> Option<DateTime<Utc>> {
45 match val {
46 serde_json::Value::String(s) => s.parse::<DateTime<Utc>>().ok().or_else(|| {
47 chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.fZ")
48 .ok()
49 .map(|ndt| ndt.and_utc())
50 }),
51 _ => None,
52 }
53}
54
55#[must_use]
59pub fn merge_json_objects(
60 base: &serde_json::Value,
61 overlay: &serde_json::Value,
62) -> serde_json::Value {
63 match (base, overlay) {
64 (serde_json::Value::Object(b), serde_json::Value::Object(o)) => {
65 let mut merged = b.clone();
66 for (k, v) in o {
67 merged.insert(k.clone(), v.clone());
68 }
69 serde_json::Value::Object(merged)
70 }
71 _ => overlay.clone(),
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn strip_fencing_json() {
81 let input = "```json\n{\"key\": \"value\"}\n```";
82 assert_eq!(strip_markdown_fencing(input), "{\"key\": \"value\"}");
83 }
84
85 #[test]
86 fn strip_fencing_plain() {
87 let input = "```\n{\"key\": \"value\"}\n```";
88 assert_eq!(strip_markdown_fencing(input), "{\"key\": \"value\"}");
89 }
90
91 #[test]
92 fn strip_fencing_none() {
93 let input = "{\"key\": \"value\"}";
94 assert_eq!(strip_markdown_fencing(input), input);
95 }
96
97 #[test]
98 fn extract_json_simple() {
99 let input = "Some text {\"key\": \"value\"} more text";
100 assert_eq!(extract_json_object(input), Some("{\"key\": \"value\"}"));
101 }
102
103 #[test]
104 fn extract_json_nested() {
105 let input = "{\"outer\": {\"inner\": 1}}";
106 assert_eq!(extract_json_object(input), Some(input));
107 }
108
109 #[test]
110 fn extract_json_none() {
111 assert_eq!(extract_json_object("no json here"), None);
112 }
113
114 #[test]
115 fn parse_datetime_iso() {
116 let val = serde_json::Value::String("2024-01-15T10:30:00Z".into());
117 let dt = parse_datetime(&val);
118 assert!(dt.is_some());
119 }
120
121 #[test]
122 fn parse_datetime_invalid() {
123 let val = serde_json::Value::String("not-a-date".into());
124 assert!(parse_datetime(&val).is_none());
125 }
126
127 #[test]
128 fn parse_datetime_non_string() {
129 let val = serde_json::json!(42);
130 assert!(parse_datetime(&val).is_none());
131 }
132
133 #[test]
134 fn merge_objects() {
135 let base = serde_json::json!({"a": 1, "b": 2});
136 let overlay = serde_json::json!({"b": 3, "c": 4});
137 let merged = merge_json_objects(&base, &overlay);
138 assert_eq!(merged, serde_json::json!({"a": 1, "b": 3, "c": 4}));
139 }
140
141 #[test]
142 fn merge_non_objects() {
143 let base = serde_json::json!("string");
144 let overlay = serde_json::json!(42);
145 assert_eq!(merge_json_objects(&base, &overlay), serde_json::json!(42));
146 }
147}