recall_echo/
frontmatter.rs1#[derive(Debug, Clone, PartialEq)]
8pub struct Frontmatter {
9 pub log: u32,
10 pub date: String,
11 pub session_id: String,
12 pub message_count: u32,
13 pub duration: String,
14 pub source: String,
15 pub topics: Vec<String>,
16}
17
18impl Frontmatter {
19 pub fn render(&self) -> String {
20 let topics = if self.topics.is_empty() {
21 "[]".to_string()
22 } else {
23 let items: Vec<String> = self.topics.iter().map(|t| format!("\"{t}\"")).collect();
24 format!("[{}]", items.join(", "))
25 };
26
27 format!(
28 "---\nlog: {}\ndate: \"{}\"\nsession_id: \"{}\"\nmessage_count: {}\nduration: \"{}\"\nsource: \"{}\"\ntopics: {}\n---",
29 self.log, self.date, self.session_id, self.message_count, self.duration, self.source, topics
30 )
31 }
32}
33
34pub fn parse(content: &str) -> Option<Frontmatter> {
36 let trimmed = content.trim();
37 if !trimmed.starts_with("---") {
38 return None;
39 }
40
41 let after_first = &trimmed[3..];
42 let end = after_first.find("---")?;
43 let block = &after_first[..end];
44
45 let mut log = None;
46 let mut date = None;
47 let mut session_id = None;
48 let mut message_count = None;
49 let mut duration = None;
50 let mut source = None;
51 let mut topics = Vec::new();
52
53 for line in block.lines() {
54 let line = line.trim();
55 if line.is_empty() {
56 continue;
57 }
58 let (key, val) = line.split_once(':')?;
59 let key = key.trim();
60 let val = val.trim().trim_matches('"');
61
62 match key {
63 "log" => log = val.parse().ok(),
64 "date" => date = Some(val.to_string()),
65 "session_id" => session_id = Some(val.to_string()),
66 "message_count" => message_count = val.parse().ok(),
67 "duration" => duration = Some(val.to_string()),
68 "source" => source = Some(val.to_string()),
69 "topics" => {
70 let inner = val.trim_matches(|c| c == '[' || c == ']');
71 if !inner.is_empty() {
72 topics = inner
73 .split(',')
74 .map(|t| t.trim().trim_matches('"').to_string())
75 .filter(|t| !t.is_empty())
76 .collect();
77 }
78 }
79 _ => {}
80 }
81 }
82
83 Some(Frontmatter {
84 log: log?,
85 date: date?,
86 session_id: session_id.unwrap_or_default(),
87 message_count: message_count.unwrap_or(0),
88 duration: duration.unwrap_or_default(),
89 source: source.unwrap_or_default(),
90 topics,
91 })
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn render_parse_roundtrip() {
100 let fm = Frontmatter {
101 log: 42,
102 date: "2026-03-05T14:30:00Z".to_string(),
103 session_id: "abc123".to_string(),
104 message_count: 34,
105 duration: "45m".to_string(),
106 source: "jsonl".to_string(),
107 topics: vec!["auth".to_string(), "JWT".to_string()],
108 };
109 let rendered = fm.render();
110 let parsed = parse(&rendered).unwrap();
111 assert_eq!(fm, parsed);
112 }
113
114 #[test]
115 fn render_empty_topics() {
116 let fm = Frontmatter {
117 log: 1,
118 date: "2026-03-05T00:00:00Z".to_string(),
119 session_id: "xyz".to_string(),
120 message_count: 0,
121 duration: "< 1m".to_string(),
122 source: "jsonl".to_string(),
123 topics: vec![],
124 };
125 let rendered = fm.render();
126 assert!(rendered.contains("topics: []"));
127 let parsed = parse(&rendered).unwrap();
128 assert_eq!(parsed.topics, Vec::<String>::new());
129 }
130
131 #[test]
132 fn parse_missing_frontmatter() {
133 assert!(parse("no frontmatter here").is_none());
134 }
135
136 #[test]
137 fn parse_malformed_frontmatter() {
138 assert!(parse("---\nlog: abc\n---").is_none());
139 }
140}