Skip to main content

mollendorff_forge/parser/
mod.rs

1//! Parser module for Forge YAML models
2//!
3//! This module provides functionality for parsing Forge YAML files into
4//! structured `ParsedModel` objects that can be calculated and analyzed.
5//!
6//! # Submodules
7//! - `arrays`: Array/column type parsing (Number, Text, Date, Boolean)
8//! - `schema`: JSON Schema validation (v1.0.0 and v5.0.0)
9//! - `multi_doc`: Multi-document YAML parsing (v4.4.2)
10//! - `includes`: Cross-file include resolution (v4.0)
11//! - `variables`: Table and scalar variable parsing
12//! - `model`: Core model parsing logic
13
14mod arrays;
15mod includes;
16mod model;
17mod multi_doc;
18mod schema;
19mod variables;
20
21// Re-export commonly used functions
22pub use arrays::{detect_array_type, is_valid_date_format, parse_array_value, type_name};
23pub use includes::{parse_includes, resolve_includes};
24pub use model::{parse_nested_scalars, parse_scenarios, parse_v1_model};
25pub use multi_doc::{
26    detect_multi_document, parse_multi_document_yaml, parse_single_document_yaml,
27    split_yaml_documents,
28};
29pub use schema::{validate_against_schema, validate_v1_0_0_no_tables};
30pub use variables::{is_nested_scalar_section, parse_metadata, parse_scalar_variable, parse_table};
31
32use crate::error::ForgeResult;
33use crate::types::ParsedModel;
34
35/// Parse a Forge model file (v1.0.0 array format) and return a `ParsedModel`.
36///
37/// This is the main entry point for parsing Forge YAML files.
38///
39/// # Arguments
40/// * `path` - Path to the Forge YAML file
41///
42/// # Returns
43/// * `Ok(ParsedModel)` - Successfully parsed model with tables and scalars
44/// * `Err(ForgeError)` - Parse error with detailed context
45///
46/// # Errors
47///
48/// Returns an error if the file cannot be read, contains invalid YAML, or fails
49/// schema validation.
50///
51/// # Example
52/// ```no_run
53/// use mollendorff_forge::parser::parse_model;
54/// use std::path::Path;
55///
56/// let model = parse_model(Path::new("model.yaml"))?;
57/// println!("Tables: {}", model.tables.len());
58/// # Ok::<(), mollendorff_forge::error::ForgeError>(())
59/// ```
60pub fn parse_model(path: &std::path::Path) -> ForgeResult<ParsedModel> {
61    let content = std::fs::read_to_string(path)?;
62
63    // Check if this is a multi-document YAML file (v4.4.2)
64    // Multi-doc files have at least two document separators (---) on their own lines
65    // We need to skip comments and whitespace when detecting
66    let is_multi_doc = detect_multi_document(&content);
67
68    if is_multi_doc {
69        // Parse all documents and merge (v4.4.2)
70        parse_multi_document_yaml(&content, path)
71    } else {
72        // Single document parsing (original behavior)
73        parse_single_document_yaml(&content, path)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use std::io::Write;
81    use std::path::Path;
82    use tempfile::NamedTempFile;
83
84    #[test]
85    fn test_parse_file_not_found() {
86        let result = parse_model(Path::new("/nonexistent/path/file.yaml"));
87        assert!(result.is_err());
88    }
89
90    #[test]
91    fn test_parse_invalid_yaml() {
92        let yaml_content = "not: valid: yaml: [[[";
93
94        let mut temp_file = NamedTempFile::new().unwrap();
95        temp_file.write_all(yaml_content.as_bytes()).unwrap();
96
97        let result = parse_model(temp_file.path());
98        assert!(result.is_err());
99    }
100
101    #[test]
102    fn test_parse_table_column_not_array() {
103        let yaml_content = r#"
104_forge_version: "1.0.0"
105data:
106  values: 123
107"#;
108
109        let mut temp_file = NamedTempFile::new().unwrap();
110        temp_file.write_all(yaml_content.as_bytes()).unwrap();
111
112        let result = parse_model(temp_file.path());
113        assert!(result.is_err());
114    }
115}