ricecoder_storage/markdown_config/
yaml_parser.rs

1//! YAML parser for frontmatter validation and deserialization
2
3use crate::markdown_config::error::{MarkdownConfigError, MarkdownConfigResult};
4use serde::de::DeserializeOwned;
5
6/// Parser for YAML frontmatter
7#[derive(Debug, Clone)]
8pub struct YamlParser;
9
10impl YamlParser {
11    /// Create a new YAML parser
12    pub fn new() -> Self {
13        Self
14    }
15
16    /// Parse YAML string into a typed structure
17    pub fn parse<T: DeserializeOwned>(&self, yaml: &str) -> MarkdownConfigResult<T> {
18        serde_yaml::from_str(yaml).map_err(|e| {
19            MarkdownConfigError::yaml_error(format!("Failed to parse YAML: {}", e))
20        })
21    }
22
23    /// Validate YAML structure without deserializing to a specific type
24    pub fn validate_structure(&self, yaml: &str) -> MarkdownConfigResult<()> {
25        // Try to parse as a generic YAML value to validate structure
26        serde_yaml::from_str::<serde_yaml::Value>(yaml).map_err(|e| {
27            MarkdownConfigError::yaml_error(format!("Invalid YAML structure: {}", e))
28        })?;
29        Ok(())
30    }
31
32    /// Check if required fields are present in YAML
33    pub fn has_required_fields(&self, yaml: &str, required_fields: &[&str]) -> MarkdownConfigResult<()> {
34        let value: serde_yaml::Value = serde_yaml::from_str(yaml).map_err(|e| {
35            MarkdownConfigError::yaml_error(format!("Failed to parse YAML: {}", e))
36        })?;
37
38        let mapping = value.as_mapping().ok_or_else(|| {
39            MarkdownConfigError::validation_error("YAML must be a mapping (object)")
40        })?;
41
42        for field in required_fields {
43            let key = serde_yaml::Value::String(field.to_string());
44            if !mapping.contains_key(&key) {
45                return Err(MarkdownConfigError::missing_field(*field));
46            }
47        }
48
49        Ok(())
50    }
51
52    /// Get a field value from YAML
53    pub fn get_field(&self, yaml: &str, field: &str) -> MarkdownConfigResult<Option<String>> {
54        let value: serde_yaml::Value = serde_yaml::from_str(yaml).map_err(|e| {
55            MarkdownConfigError::yaml_error(format!("Failed to parse YAML: {}", e))
56        })?;
57
58        let mapping = value.as_mapping().ok_or_else(|| {
59            MarkdownConfigError::validation_error("YAML must be a mapping (object)")
60        })?;
61
62        let key = serde_yaml::Value::String(field.to_string());
63        Ok(mapping.get(&key).and_then(|v| v.as_str().map(|s| s.to_string())))
64    }
65
66    /// Validate YAML against a schema (checks for required fields and types)
67    pub fn validate_schema(
68        &self,
69        yaml: &str,
70        required_fields: &[&str],
71    ) -> MarkdownConfigResult<()> {
72        // First validate structure
73        self.validate_structure(yaml)?;
74
75        // Then check required fields
76        self.has_required_fields(yaml, required_fields)?;
77
78        Ok(())
79    }
80
81    /// Get all validation errors from YAML
82    pub fn get_all_validation_errors(
83        &self,
84        yaml: &str,
85        required_fields: &[&str],
86    ) -> Vec<MarkdownConfigError> {
87        let mut errors = Vec::new();
88
89        // Check structure
90        if let Err(e) = self.validate_structure(yaml) {
91            errors.push(e);
92            return errors; // Can't check fields if structure is invalid
93        }
94
95        // Check required fields
96        for field in required_fields {
97            if let Err(e) = self.has_required_fields(yaml, &[field]) {
98                errors.push(e);
99            }
100        }
101
102        errors
103    }
104}
105
106impl Default for YamlParser {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use serde::{Deserialize, Serialize};
116
117    #[derive(Debug, Serialize, Deserialize, PartialEq)]
118    struct TestConfig {
119        name: String,
120        value: i32,
121    }
122
123    #[test]
124    fn test_parse_valid_yaml() {
125        let parser = YamlParser::new();
126        let yaml = "name: test\nvalue: 42";
127
128        let result: TestConfig = parser.parse(yaml).unwrap();
129        assert_eq!(result.name, "test");
130        assert_eq!(result.value, 42);
131    }
132
133    #[test]
134    fn test_parse_invalid_yaml() {
135        let parser = YamlParser::new();
136        let yaml = "name: test\n  invalid: [unclosed";
137
138        let result: Result<TestConfig, _> = parser.parse(yaml);
139        assert!(result.is_err());
140    }
141
142    #[test]
143    fn test_validate_structure_valid() {
144        let parser = YamlParser::new();
145        let yaml = "name: test\nvalue: 42";
146
147        let result = parser.validate_structure(yaml);
148        assert!(result.is_ok());
149    }
150
151    #[test]
152    fn test_validate_structure_invalid() {
153        let parser = YamlParser::new();
154        let yaml = "name: test\n  invalid: [unclosed";
155
156        let result = parser.validate_structure(yaml);
157        assert!(result.is_err());
158    }
159
160    #[test]
161    fn test_has_required_fields_present() {
162        let parser = YamlParser::new();
163        let yaml = "name: test\nvalue: 42\ndescription: optional";
164
165        let result = parser.has_required_fields(yaml, &["name", "value"]);
166        assert!(result.is_ok());
167    }
168
169    #[test]
170    fn test_has_required_fields_missing() {
171        let parser = YamlParser::new();
172        let yaml = "name: test";
173
174        let result = parser.has_required_fields(yaml, &["name", "value"]);
175        assert!(result.is_err());
176    }
177
178    #[test]
179    fn test_get_field_exists() {
180        let parser = YamlParser::new();
181        let yaml = "name: test-value\nother: data";
182
183        let result = parser.get_field(yaml, "name").unwrap();
184        assert_eq!(result, Some("test-value".to_string()));
185    }
186
187    #[test]
188    fn test_get_field_missing() {
189        let parser = YamlParser::new();
190        let yaml = "name: test-value";
191
192        let result = parser.get_field(yaml, "missing").unwrap();
193        assert_eq!(result, None);
194    }
195
196    #[test]
197    fn test_get_field_non_string() {
198        let parser = YamlParser::new();
199        let yaml = "name: test\nvalue: 42";
200
201        let result = parser.get_field(yaml, "value").unwrap();
202        assert_eq!(result, None); // Non-string values return None
203    }
204
205    #[test]
206    fn test_parse_complex_yaml() {
207        let parser = YamlParser::new();
208        let _yaml = r#"
209name: complex-agent
210description: A complex agent
211model: gpt-4
212temperature: 0.7
213max_tokens: 2000
214tools:
215  - tool1
216  - tool2
217  - tool3
218"#;
219
220        let result: TestConfig = parser.parse("name: test\nvalue: 42").unwrap();
221        assert_eq!(result.name, "test");
222    }
223
224    #[test]
225    fn test_parse_yaml_with_nested_objects() {
226        let parser = YamlParser::new();
227        let yaml = r#"
228name: test
229config:
230  nested: value
231  deep:
232    deeper: data
233"#;
234
235        let result = parser.validate_structure(yaml);
236        assert!(result.is_ok());
237    }
238
239    #[test]
240    fn test_parse_yaml_with_arrays() {
241        let parser = YamlParser::new();
242        let yaml = r#"
243name: test
244items:
245  - item1
246  - item2
247  - item3
248"#;
249
250        let result = parser.validate_structure(yaml);
251        assert!(result.is_ok());
252    }
253
254    #[test]
255    fn test_validate_schema_all_required_present() {
256        let parser = YamlParser::new();
257        let yaml = "name: test\nvalue: 42\ndescription: optional";
258
259        let result = parser.validate_schema(yaml, &["name", "value"]);
260        assert!(result.is_ok());
261    }
262
263    #[test]
264    fn test_validate_schema_missing_required() {
265        let parser = YamlParser::new();
266        let yaml = "name: test";
267
268        let result = parser.validate_schema(yaml, &["name", "value", "description"]);
269        assert!(result.is_err());
270    }
271
272    #[test]
273    fn test_get_all_validation_errors_valid() {
274        let parser = YamlParser::new();
275        let yaml = "name: test\nvalue: 42";
276
277        let errors = parser.get_all_validation_errors(yaml, &["name", "value"]);
278        assert_eq!(errors.len(), 0);
279    }
280
281    #[test]
282    fn test_get_all_validation_errors_invalid_structure() {
283        let parser = YamlParser::new();
284        let yaml = "name: test\n  invalid: [unclosed";
285
286        let errors = parser.get_all_validation_errors(yaml, &["name"]);
287        assert!(!errors.is_empty());
288    }
289
290    #[test]
291    fn test_get_all_validation_errors_missing_fields() {
292        let parser = YamlParser::new();
293        let yaml = "name: test";
294
295        let errors = parser.get_all_validation_errors(yaml, &["name", "value", "description"]);
296        assert!(!errors.is_empty());
297    }
298
299    #[test]
300    fn test_parse_yaml_with_special_characters() {
301        let parser = YamlParser::new();
302        let yaml = r#"
303name: "test-agent"
304description: "Agent with special chars: @#$%^&*()"
305"#;
306
307        let result = parser.validate_structure(yaml);
308        assert!(result.is_ok());
309    }
310
311    #[test]
312    fn test_parse_yaml_with_quotes() {
313        let parser = YamlParser::new();
314        let yaml = r#"
315name: 'single-quoted'
316description: "double-quoted"
317"#;
318
319        let result = parser.validate_structure(yaml);
320        assert!(result.is_ok());
321    }
322
323    #[test]
324    fn test_parse_yaml_with_multiline_strings() {
325        let parser = YamlParser::new();
326        let yaml = r#"
327name: test
328description: |
329  This is a multiline
330  description that spans
331  multiple lines
332"#;
333
334        let result = parser.validate_structure(yaml);
335        assert!(result.is_ok());
336    }
337
338    #[test]
339    fn test_parse_yaml_with_numbers() {
340        let parser = YamlParser::new();
341        let yaml = r#"
342name: test
343integer: 42
344float: 3.14
345negative: -10
346"#;
347
348        let result = parser.validate_structure(yaml);
349        assert!(result.is_ok());
350    }
351
352    #[test]
353    fn test_parse_yaml_with_booleans() {
354        let parser = YamlParser::new();
355        let yaml = r#"
356name: test
357enabled: true
358disabled: false
359"#;
360
361        let result = parser.validate_structure(yaml);
362        assert!(result.is_ok());
363    }
364
365    #[test]
366    fn test_parse_yaml_with_null() {
367        let parser = YamlParser::new();
368        let yaml = r#"
369name: test
370optional: null
371"#;
372
373        let result = parser.validate_structure(yaml);
374        assert!(result.is_ok());
375    }
376
377    #[test]
378    fn test_parse_empty_yaml() {
379        let parser = YamlParser::new();
380        let yaml = "";
381
382        let result = parser.validate_structure(yaml);
383        assert!(result.is_ok());
384    }
385
386    #[test]
387    fn test_parse_yaml_only_comments() {
388        let parser = YamlParser::new();
389        let yaml = r#"
390# This is a comment
391# Another comment
392"#;
393
394        let result = parser.validate_structure(yaml);
395        assert!(result.is_ok());
396    }
397
398    #[test]
399    fn test_parse_yaml_with_anchors_and_aliases() {
400        let parser = YamlParser::new();
401        let yaml = r#"
402defaults: &defaults
403  name: default
404  value: 0
405
406config:
407  <<: *defaults
408  name: custom
409"#;
410
411        let result = parser.validate_structure(yaml);
412        assert!(result.is_ok());
413    }
414
415    #[test]
416    fn test_has_required_fields_empty_list() {
417        let parser = YamlParser::new();
418        let yaml = "name: test";
419
420        let result = parser.has_required_fields(yaml, &[]);
421        assert!(result.is_ok());
422    }
423
424    #[test]
425    fn test_get_field_from_empty_yaml() {
426        let parser = YamlParser::new();
427        let yaml = "";
428
429        let result = parser.get_field(yaml, "name");
430        assert!(result.is_err());
431    }
432
433    #[test]
434    fn test_parse_yaml_consistency() {
435        let parser = YamlParser::new();
436        let yaml = "name: test\nvalue: 42";
437
438        let result1: TestConfig = parser.parse(yaml).unwrap();
439        let result2: TestConfig = parser.parse(yaml).unwrap();
440
441        assert_eq!(result1, result2);
442    }
443
444    #[test]
445    fn test_parse_yaml_with_unicode() {
446        let parser = YamlParser::new();
447        let yaml = r#"
448name: 测试
449description: 日本語のテスト
450"#;
451
452        let result = parser.validate_structure(yaml);
453        assert!(result.is_ok());
454    }
455
456    #[test]
457    fn test_parse_yaml_with_windows_line_endings() {
458        let parser = YamlParser::new();
459        let yaml = "name: test\r\nvalue: 42";
460
461        let result: Result<TestConfig, _> = parser.parse(yaml);
462        assert!(result.is_ok());
463    }
464}