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