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