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}