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