metis_core/domain/documents/
helpers.rs1use super::traits::DocumentValidationError;
2use super::types::Tag;
3use chrono::{DateTime, Utc};
4use gray_matter;
5
6pub struct FrontmatterParser;
8
9impl FrontmatterParser {
10 pub fn extract_string(
11 map: &std::collections::HashMap<String, gray_matter::Pod>,
12 key: &str,
13 ) -> Result<String, DocumentValidationError> {
14 match map.get(key) {
15 Some(gray_matter::Pod::String(s)) => Ok(s.clone()),
16 Some(_) => Err(DocumentValidationError::InvalidContent(format!(
17 "{} must be a string",
18 key
19 ))),
20 None => Err(DocumentValidationError::MissingRequiredField(
21 key.to_string(),
22 )),
23 }
24 }
25
26 pub fn extract_bool(
27 map: &std::collections::HashMap<String, gray_matter::Pod>,
28 key: &str,
29 ) -> Result<bool, DocumentValidationError> {
30 match map.get(key) {
31 Some(gray_matter::Pod::Boolean(b)) => Ok(*b),
32 Some(_) => Err(DocumentValidationError::InvalidContent(format!(
33 "{} must be a boolean",
34 key
35 ))),
36 None => Err(DocumentValidationError::MissingRequiredField(
37 key.to_string(),
38 )),
39 }
40 }
41
42 pub fn extract_integer(
43 map: &std::collections::HashMap<String, gray_matter::Pod>,
44 key: &str,
45 ) -> Result<i64, DocumentValidationError> {
46 match map.get(key) {
47 Some(gray_matter::Pod::Integer(i)) => Ok(*i),
48 Some(_) => Err(DocumentValidationError::InvalidContent(format!(
49 "{} must be an integer",
50 key
51 ))),
52 None => Err(DocumentValidationError::MissingRequiredField(
53 key.to_string(),
54 )),
55 }
56 }
57
58 pub fn extract_datetime(
59 map: &std::collections::HashMap<String, gray_matter::Pod>,
60 key: &str,
61 ) -> Result<DateTime<Utc>, DocumentValidationError> {
62 let date_str = Self::extract_string(map, key)?;
63 DateTime::parse_from_rfc3339(&date_str)
64 .map(|dt| dt.with_timezone(&Utc))
65 .map_err(|_| {
66 DocumentValidationError::InvalidContent(format!(
67 "Invalid datetime format for {}",
68 key
69 ))
70 })
71 }
72
73 pub fn extract_tags(
74 map: &std::collections::HashMap<String, gray_matter::Pod>,
75 ) -> Result<Vec<Tag>, DocumentValidationError> {
76 match map.get("tags") {
77 Some(gray_matter::Pod::Array(arr)) => {
78 let mut tags = Vec::new();
79 for item in arr {
80 if let gray_matter::Pod::String(tag_str) = item {
81 if let Ok(tag) = tag_str.parse::<Tag>() {
82 tags.push(tag);
83 }
84 }
85 }
86 Ok(tags)
87 }
88 Some(_) => Err(DocumentValidationError::InvalidContent(
89 "tags must be an array".to_string(),
90 )),
91 None => Err(DocumentValidationError::MissingRequiredField(
92 "tags".to_string(),
93 )),
94 }
95 }
96
97 pub fn extract_string_array(
98 map: &std::collections::HashMap<String, gray_matter::Pod>,
99 key: &str,
100 ) -> Result<Vec<String>, DocumentValidationError> {
101 match map.get(key) {
102 Some(gray_matter::Pod::Array(arr)) => {
103 let mut strings = Vec::new();
104 for item in arr {
105 if let gray_matter::Pod::String(s) = item {
106 strings.push(s.clone());
107 }
108 }
109 Ok(strings)
110 }
111 Some(_) => Err(DocumentValidationError::InvalidContent(format!(
112 "{} must be an array",
113 key
114 ))),
115 None => Err(DocumentValidationError::MissingRequiredField(
116 key.to_string(),
117 )),
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::super::types::{Phase, Tag};
125 use super::*;
126 use gray_matter::Pod;
127 use std::collections::HashMap;
128
129 fn create_test_map() -> HashMap<String, Pod> {
130 let mut map = HashMap::new();
131 map.insert(
132 "string_field".to_string(),
133 Pod::String("test_value".to_string()),
134 );
135 map.insert("bool_field".to_string(), Pod::Boolean(true));
136 map.insert("integer_field".to_string(), Pod::Integer(42));
137 map.insert(
138 "date_field".to_string(),
139 Pod::String("2025-01-01T12:00:00Z".to_string()),
140 );
141 map.insert(
142 "tags".to_string(),
143 Pod::Array(vec![
144 Pod::String("#phase/draft".to_string()),
145 Pod::String("#vision".to_string()),
146 Pod::String("urgent".to_string()),
147 ]),
148 );
149 map.insert(
150 "string_array".to_string(),
151 Pod::Array(vec![
152 Pod::String("item1".to_string()),
153 Pod::String("item2".to_string()),
154 ]),
155 );
156 map
157 }
158
159 #[test]
160 fn test_extract_string() {
161 let map = create_test_map();
162
163 assert_eq!(
164 FrontmatterParser::extract_string(&map, "string_field").unwrap(),
165 "test_value"
166 );
167
168 assert!(FrontmatterParser::extract_string(&map, "missing_field").is_err());
170
171 assert!(FrontmatterParser::extract_string(&map, "bool_field").is_err());
173 }
174
175 #[test]
176 fn test_extract_bool() {
177 let map = create_test_map();
178
179 assert!(FrontmatterParser::extract_bool(&map, "bool_field").unwrap());
180
181 assert!(FrontmatterParser::extract_bool(&map, "missing_field").is_err());
183
184 assert!(FrontmatterParser::extract_bool(&map, "string_field").is_err());
186 }
187
188 #[test]
189 fn test_extract_integer() {
190 let map = create_test_map();
191
192 assert_eq!(
193 FrontmatterParser::extract_integer(&map, "integer_field").unwrap(),
194 42
195 );
196
197 assert!(FrontmatterParser::extract_integer(&map, "missing_field").is_err());
199
200 assert!(FrontmatterParser::extract_integer(&map, "string_field").is_err());
202 }
203
204 #[test]
205 fn test_extract_datetime() {
206 let map = create_test_map();
207
208 let dt = FrontmatterParser::extract_datetime(&map, "date_field").unwrap();
209 assert_eq!(dt.to_rfc3339(), "2025-01-01T12:00:00+00:00");
210
211 assert!(FrontmatterParser::extract_datetime(&map, "missing_field").is_err());
213
214 let mut bad_map = HashMap::new();
216 bad_map.insert(
217 "bad_date".to_string(),
218 Pod::String("not-a-date".to_string()),
219 );
220 assert!(FrontmatterParser::extract_datetime(&bad_map, "bad_date").is_err());
221 }
222
223 #[test]
224 fn test_extract_tags() {
225 let map = create_test_map();
226
227 let tags = FrontmatterParser::extract_tags(&map).unwrap();
228 assert_eq!(tags.len(), 3);
229 assert!(tags.contains(&Tag::Phase(Phase::Draft)));
230 assert!(tags.contains(&Tag::Label("vision".to_string())));
231 assert!(tags.contains(&Tag::Label("urgent".to_string())));
232
233 let empty_map = HashMap::new();
235 assert!(FrontmatterParser::extract_tags(&empty_map).is_err());
236
237 let mut bad_map = HashMap::new();
239 bad_map.insert("tags".to_string(), Pod::String("not-an-array".to_string()));
240 assert!(FrontmatterParser::extract_tags(&bad_map).is_err());
241 }
242
243 #[test]
244 fn test_extract_string_array() {
245 let map = create_test_map();
246
247 let strings = FrontmatterParser::extract_string_array(&map, "string_array").unwrap();
248 assert_eq!(strings, vec!["item1", "item2"]);
249
250 assert!(FrontmatterParser::extract_string_array(&map, "missing_field").is_err());
252
253 assert!(FrontmatterParser::extract_string_array(&map, "string_field").is_err());
255 }
256
257 #[test]
258 fn test_extract_tags_with_invalid_tags() {
259 let mut map = HashMap::new();
260 map.insert(
261 "tags".to_string(),
262 Pod::Array(vec![
263 Pod::String("#phase/draft".to_string()),
264 Pod::Integer(123), Pod::String("#valid-tag".to_string()),
266 ]),
267 );
268
269 let tags = FrontmatterParser::extract_tags(&map).unwrap();
271 assert_eq!(tags.len(), 2);
272 assert!(tags.contains(&Tag::Phase(Phase::Draft)));
273 assert!(tags.contains(&Tag::Label("valid-tag".to_string())));
274 }
275}