metis_core/domain/documents/
helpers.rs

1use super::traits::DocumentValidationError;
2use super::types::Tag;
3use chrono::{DateTime, Utc};
4use gray_matter;
5
6/// Helper methods for parsing frontmatter
7pub 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        // Test missing field
169        assert!(FrontmatterParser::extract_string(&map, "missing_field").is_err());
170
171        // Test wrong type
172        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        // Test missing field
182        assert!(FrontmatterParser::extract_bool(&map, "missing_field").is_err());
183
184        // Test wrong type
185        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        // Test missing field
198        assert!(FrontmatterParser::extract_integer(&map, "missing_field").is_err());
199
200        // Test wrong type
201        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        // Test missing field
212        assert!(FrontmatterParser::extract_datetime(&map, "missing_field").is_err());
213
214        // Test invalid format
215        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        // Test missing tags field
234        let empty_map = HashMap::new();
235        assert!(FrontmatterParser::extract_tags(&empty_map).is_err());
236
237        // Test wrong type
238        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        // Test missing field
251        assert!(FrontmatterParser::extract_string_array(&map, "missing_field").is_err());
252
253        // Test wrong type
254        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), // Invalid - not a string
265                Pod::String("#valid-tag".to_string()),
266            ]),
267        );
268
269        // Should still work, just ignoring invalid entries
270        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}