metis_core/application/services/document/
validation.rs1use crate::domain::documents::types::DocumentType;
2use crate::Result;
3use crate::{Adr, Initiative, MetisError, Strategy, Task, Vision};
4use std::path::Path;
5
6pub struct DocumentValidationService;
8
9#[derive(Debug)]
11pub struct ValidationResult {
12 pub document_type: DocumentType,
13 pub is_valid: bool,
14 pub errors: Vec<String>,
15}
16
17impl DocumentValidationService {
18 pub fn new() -> Self {
20 Self
21 }
22
23 pub async fn validate_document<P: AsRef<Path>>(
25 &self,
26 file_path: P,
27 ) -> Result<ValidationResult> {
28 let file_path = file_path.as_ref();
29
30 if !file_path.exists() {
32 return Err(MetisError::NotFound("File does not exist".to_string()));
33 }
34
35 if !file_path.is_file() {
36 return Err(MetisError::NotFound("Path is not a file".to_string()));
37 }
38
39 let mut validation_results = Vec::new();
41
42 match Vision::from_file(file_path).await {
44 Ok(_vision) => {
45 validation_results.push(ValidationResult {
46 document_type: DocumentType::Vision,
47 is_valid: true,
48 errors: vec![],
49 });
50 }
51 Err(e) => {
52 validation_results.push(ValidationResult {
53 document_type: DocumentType::Vision,
54 is_valid: false,
55 errors: vec![format!("Vision validation failed: {}", e)],
56 });
57 }
58 }
59
60 match Strategy::from_file(file_path).await {
62 Ok(_strategy) => {
63 validation_results.push(ValidationResult {
64 document_type: DocumentType::Strategy,
65 is_valid: true,
66 errors: vec![],
67 });
68 }
69 Err(e) => {
70 validation_results.push(ValidationResult {
71 document_type: DocumentType::Strategy,
72 is_valid: false,
73 errors: vec![format!("Strategy validation failed: {}", e)],
74 });
75 }
76 }
77
78 match Initiative::from_file(file_path).await {
80 Ok(_initiative) => {
81 validation_results.push(ValidationResult {
82 document_type: DocumentType::Initiative,
83 is_valid: true,
84 errors: vec![],
85 });
86 }
87 Err(e) => {
88 validation_results.push(ValidationResult {
89 document_type: DocumentType::Initiative,
90 is_valid: false,
91 errors: vec![format!("Initiative validation failed: {}", e)],
92 });
93 }
94 }
95
96 match Task::from_file(file_path).await {
98 Ok(_task) => {
99 validation_results.push(ValidationResult {
100 document_type: DocumentType::Task,
101 is_valid: true,
102 errors: vec![],
103 });
104 }
105 Err(e) => {
106 validation_results.push(ValidationResult {
107 document_type: DocumentType::Task,
108 is_valid: false,
109 errors: vec![format!("Task validation failed: {}", e)],
110 });
111 }
112 }
113
114 match Adr::from_file(file_path).await {
116 Ok(_adr) => {
117 validation_results.push(ValidationResult {
118 document_type: DocumentType::Adr,
119 is_valid: true,
120 errors: vec![],
121 });
122 }
123 Err(e) => {
124 validation_results.push(ValidationResult {
125 document_type: DocumentType::Adr,
126 is_valid: false,
127 errors: vec![format!("ADR validation failed: {}", e)],
128 });
129 }
130 }
131
132 if let Some(valid_result) = validation_results.iter().find(|r| r.is_valid) {
134 return Ok(ValidationResult {
135 document_type: valid_result.document_type,
136 is_valid: true,
137 errors: vec![],
138 });
139 }
140
141 let all_errors: Vec<String> = validation_results
143 .into_iter()
144 .flat_map(|r| r.errors)
145 .collect();
146
147 Ok(ValidationResult {
148 document_type: DocumentType::Vision, is_valid: false,
150 errors: all_errors,
151 })
152 }
153
154 pub async fn detect_document_type<P: AsRef<Path>>(&self, file_path: P) -> Result<DocumentType> {
156 let result = self.validate_document(file_path).await?;
157
158 if result.is_valid {
159 Ok(result.document_type)
160 } else {
161 Err(MetisError::InvalidDocument(format!(
162 "Could not determine document type: {}",
163 result.errors.join("; ")
164 )))
165 }
166 }
167
168 pub async fn validate_document_as_type<P: AsRef<Path>>(
170 &self,
171 file_path: P,
172 expected_type: DocumentType,
173 ) -> Result<bool> {
174 let file_path = file_path.as_ref();
175
176 match expected_type {
177 DocumentType::Vision => match Vision::from_file(file_path).await {
178 Ok(_) => Ok(true),
179 Err(_) => Ok(false),
180 },
181 DocumentType::Strategy => match Strategy::from_file(file_path).await {
182 Ok(_) => Ok(true),
183 Err(_) => Ok(false),
184 },
185 DocumentType::Initiative => match Initiative::from_file(file_path).await {
186 Ok(_) => Ok(true),
187 Err(_) => Ok(false),
188 },
189 DocumentType::Task => match Task::from_file(file_path).await {
190 Ok(_) => Ok(true),
191 Err(_) => Ok(false),
192 },
193 DocumentType::Adr => match Adr::from_file(file_path).await {
194 Ok(_) => Ok(true),
195 Err(_) => Ok(false),
196 },
197 }
198 }
199
200 pub async fn is_valid_document<P: AsRef<Path>>(&self, file_path: P) -> bool {
202 self.validate_document(file_path)
203 .await
204 .map(|result| result.is_valid)
205 .unwrap_or(false)
206 }
207}
208
209impl Default for DocumentValidationService {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::fs;
219 use tempfile::tempdir;
220
221 #[tokio::test]
222 async fn test_validate_valid_vision_document() {
223 let temp_dir = tempdir().unwrap();
224 let file_path = temp_dir.path().join("vision.md");
225
226 let vision_content = r##"---
228id: test-vision
229title: Test Vision
230level: vision
231short_code: TEST-V-0801
232created_at: 2023-01-01T00:00:00Z
233updated_at: 2023-01-01T00:00:00Z
234archived: false
235tags:
236 - "#vision"
237 - "#phase/draft"
238exit_criteria_met: false
239---
240
241# Test Vision
242
243This is a test vision document.
244"##;
245 fs::write(&file_path, vision_content).unwrap();
246
247 let service = DocumentValidationService::new();
248 let result = service.validate_document(&file_path).await.unwrap();
249
250 assert!(result.is_valid);
251 assert_eq!(result.document_type, DocumentType::Vision);
252 assert!(result.errors.is_empty());
253 }
254
255 #[tokio::test]
256 async fn test_validate_invalid_document() {
257 let temp_dir = tempdir().unwrap();
258 let file_path = temp_dir.path().join("invalid.md");
259
260 let invalid_content = r##"# Invalid Document
262
263This has no frontmatter.
264"##;
265 fs::write(&file_path, invalid_content).unwrap();
266
267 let service = DocumentValidationService::new();
268 let result = service.validate_document(&file_path).await.unwrap();
269
270 assert!(!result.is_valid);
271 assert!(!result.errors.is_empty());
272 }
273
274 #[tokio::test]
275 async fn test_detect_document_type() {
276 let temp_dir = tempdir().unwrap();
277 let file_path = temp_dir.path().join("vision.md");
278
279 let vision_content = r##"---
281id: test-vision
282title: Test Vision
283level: vision
284short_code: TEST-V-0802
285created_at: 2023-01-01T00:00:00Z
286updated_at: 2023-01-01T00:00:00Z
287archived: false
288tags:
289 - "#vision"
290 - "#phase/draft"
291exit_criteria_met: false
292---
293
294# Test Vision
295
296This is a test vision document.
297"##;
298 fs::write(&file_path, vision_content).unwrap();
299
300 let service = DocumentValidationService::new();
301 let doc_type = service.detect_document_type(&file_path).await.unwrap();
302
303 assert_eq!(doc_type, DocumentType::Vision);
304 }
305
306 #[tokio::test]
307 async fn test_validate_document_as_type() {
308 let temp_dir = tempdir().unwrap();
309 let file_path = temp_dir.path().join("vision.md");
310
311 let vision_content = r##"---
313id: test-vision
314title: Test Vision
315level: vision
316short_code: TEST-V-0802
317created_at: 2023-01-01T00:00:00Z
318updated_at: 2023-01-01T00:00:00Z
319archived: false
320tags:
321 - "#vision"
322 - "#phase/draft"
323exit_criteria_met: false
324---
325
326# Test Vision
327
328This is a test vision document.
329"##;
330 fs::write(&file_path, vision_content).unwrap();
331
332 let service = DocumentValidationService::new();
333
334 assert!(service
336 .validate_document_as_type(&file_path, DocumentType::Vision)
337 .await
338 .unwrap());
339
340 assert!(!service
342 .validate_document_as_type(&file_path, DocumentType::Strategy)
343 .await
344 .unwrap());
345 }
346
347 #[tokio::test]
348 async fn test_validate_nonexistent_file() {
349 let service = DocumentValidationService::new();
350 let result = service.validate_document("/nonexistent/file.md").await;
351
352 assert!(result.is_err());
353 assert!(matches!(result.unwrap_err(), MetisError::NotFound(_)));
354 }
355}