data_modelling_core/import/
decision.rs

1//! Decision (MADR) importer
2//!
3//! Parses MADR-compliant decision YAML files (.madr.yaml) and converts them to Decision models.
4//! Also handles the decisions index file (decisions.yaml).
5
6use super::ImportError;
7use crate::models::decision::{Decision, DecisionIndex};
8
9#[cfg(feature = "schema-validation")]
10use crate::validation::schema::validate_decision_internal;
11
12/// Decision importer for parsing MADR-compliant YAML files
13pub struct DecisionImporter;
14
15impl DecisionImporter {
16    /// Create a new Decision importer instance
17    pub fn new() -> Self {
18        Self
19    }
20
21    /// Import a decision from YAML content
22    ///
23    /// Optionally validates against the JSON schema if the `schema-validation` feature is enabled.
24    ///
25    /// # Arguments
26    ///
27    /// * `yaml_content` - Decision YAML content as a string
28    ///
29    /// # Returns
30    ///
31    /// A `Decision` parsed from the YAML content
32    ///
33    /// # Example
34    ///
35    /// ```rust
36    /// use data_modelling_core::import::decision::DecisionImporter;
37    ///
38    /// let importer = DecisionImporter::new();
39    /// let yaml = r#"
40    /// id: 550e8400-e29b-41d4-a716-446655440000
41    /// number: 1
42    /// title: "Use ODCS Format for Data Contracts"
43    /// status: accepted
44    /// category: datadesign
45    /// date: "2024-01-15T10:00:00Z"
46    /// context: "We need a standard format for data contracts."
47    /// decision: "Use ODCS v3.1.0 format."
48    /// createdAt: "2024-01-15T10:00:00Z"
49    /// updatedAt: "2024-01-15T10:00:00Z"
50    /// "#;
51    /// let decision = importer.import(yaml).unwrap();
52    /// assert_eq!(decision.title, "Use ODCS Format for Data Contracts");
53    /// ```
54    pub fn import(&self, yaml_content: &str) -> Result<Decision, ImportError> {
55        // Validate against JSON Schema if feature is enabled
56        #[cfg(feature = "schema-validation")]
57        {
58            validate_decision_internal(yaml_content).map_err(ImportError::ValidationError)?;
59        }
60
61        // Parse the YAML content
62        Decision::from_yaml(yaml_content)
63            .map_err(|e| ImportError::ParseError(format!("Failed to parse decision YAML: {}", e)))
64    }
65
66    /// Import a decision without schema validation
67    ///
68    /// Use this when you want to skip schema validation for performance
69    /// or when importing from a trusted source.
70    ///
71    /// # Arguments
72    ///
73    /// * `yaml_content` - Decision YAML content as a string
74    ///
75    /// # Returns
76    ///
77    /// A `Decision` parsed from the YAML content
78    pub fn import_without_validation(&self, yaml_content: &str) -> Result<Decision, ImportError> {
79        Decision::from_yaml(yaml_content)
80            .map_err(|e| ImportError::ParseError(format!("Failed to parse decision YAML: {}", e)))
81    }
82
83    /// Import a decisions index from YAML content
84    ///
85    /// # Arguments
86    ///
87    /// * `yaml_content` - Decisions index YAML content (decisions.yaml)
88    ///
89    /// # Returns
90    ///
91    /// A `DecisionIndex` parsed from the YAML content
92    ///
93    /// # Example
94    ///
95    /// ```rust
96    /// use data_modelling_core::import::decision::DecisionImporter;
97    ///
98    /// let importer = DecisionImporter::new();
99    /// let yaml = r#"
100    /// schema_version: "1.0"
101    /// decisions: []
102    /// next_number: 1
103    /// "#;
104    /// let index = importer.import_index(yaml).unwrap();
105    /// assert_eq!(index.next_number, 1);
106    /// ```
107    pub fn import_index(&self, yaml_content: &str) -> Result<DecisionIndex, ImportError> {
108        DecisionIndex::from_yaml(yaml_content).map_err(|e| {
109            ImportError::ParseError(format!("Failed to parse decisions index YAML: {}", e))
110        })
111    }
112
113    /// Import multiple decisions from a directory
114    ///
115    /// Loads all `.madr.yaml` files from the specified directory.
116    ///
117    /// # Arguments
118    ///
119    /// * `dir_path` - Path to the directory containing decision files
120    ///
121    /// # Returns
122    ///
123    /// A vector of parsed `Decision` objects and any import errors
124    pub fn import_from_directory(
125        &self,
126        dir_path: &std::path::Path,
127    ) -> Result<(Vec<Decision>, Vec<ImportError>), ImportError> {
128        let mut decisions = Vec::new();
129        let mut errors = Vec::new();
130
131        if !dir_path.exists() {
132            return Err(ImportError::IoError(format!(
133                "Directory does not exist: {}",
134                dir_path.display()
135            )));
136        }
137
138        if !dir_path.is_dir() {
139            return Err(ImportError::IoError(format!(
140                "Path is not a directory: {}",
141                dir_path.display()
142            )));
143        }
144
145        // Read all .madr.yaml files
146        let entries = std::fs::read_dir(dir_path)
147            .map_err(|e| ImportError::IoError(format!("Failed to read directory: {}", e)))?;
148
149        for entry in entries.flatten() {
150            let path = entry.path();
151            if path.extension().and_then(|s| s.to_str()) == Some("yaml")
152                && path
153                    .file_name()
154                    .and_then(|s| s.to_str())
155                    .is_some_and(|name| name.ends_with(".madr.yaml"))
156            {
157                match std::fs::read_to_string(&path) {
158                    Ok(content) => match self.import(&content) {
159                        Ok(decision) => decisions.push(decision),
160                        Err(e) => errors.push(ImportError::ParseError(format!(
161                            "Failed to import {}: {}",
162                            path.display(),
163                            e
164                        ))),
165                    },
166                    Err(e) => errors.push(ImportError::IoError(format!(
167                        "Failed to read {}: {}",
168                        path.display(),
169                        e
170                    ))),
171                }
172            }
173        }
174
175        // Sort decisions by number
176        decisions.sort_by_key(|d| d.number);
177
178        Ok((decisions, errors))
179    }
180}
181
182impl Default for DecisionImporter {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_import_decision() {
194        let importer = DecisionImporter::new();
195        let yaml = r#"
196id: 550e8400-e29b-41d4-a716-446655440000
197number: 1
198title: "Use ODCS Format for Data Contracts"
199status: accepted
200category: datadesign
201date: "2024-01-15T10:00:00Z"
202context: "We need a standard format for data contracts."
203decision: "Use ODCS v3.1.0 format."
204createdAt: "2024-01-15T10:00:00Z"
205updatedAt: "2024-01-15T10:00:00Z"
206"#;
207        let result = importer.import_without_validation(yaml);
208        assert!(result.is_ok());
209        let decision = result.unwrap();
210        assert_eq!(decision.title, "Use ODCS Format for Data Contracts");
211        assert_eq!(decision.number, 1);
212    }
213
214    #[test]
215    fn test_import_decision_index() {
216        let importer = DecisionImporter::new();
217        let yaml = r#"
218schema_version: "1.0"
219decisions: []
220next_number: 1
221"#;
222        let result = importer.import_index(yaml);
223        assert!(result.is_ok());
224        let index = result.unwrap();
225        assert_eq!(index.next_number, 1);
226        assert_eq!(index.schema_version, "1.0");
227    }
228
229    #[test]
230    fn test_import_invalid_yaml() {
231        let importer = DecisionImporter::new();
232        let yaml = "not: valid: yaml: at: all";
233        let result = importer.import_without_validation(yaml);
234        assert!(result.is_err());
235    }
236}