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
231created_at: 2023-01-01T00:00:00Z
232updated_at: 2023-01-01T00:00:00Z
233archived: false
234tags:
235 - "#vision"
236 - "#phase/draft"
237exit_criteria_met: false
238---
239
240# Test Vision
241
242This is a test vision document.
243"##;
244 fs::write(&file_path, vision_content).unwrap();
245
246 let service = DocumentValidationService::new();
247 let result = service.validate_document(&file_path).await.unwrap();
248
249 assert!(result.is_valid);
250 assert_eq!(result.document_type, DocumentType::Vision);
251 assert!(result.errors.is_empty());
252 }
253
254 #[tokio::test]
255 async fn test_validate_invalid_document() {
256 let temp_dir = tempdir().unwrap();
257 let file_path = temp_dir.path().join("invalid.md");
258
259 let invalid_content = r##"# Invalid Document
261
262This has no frontmatter.
263"##;
264 fs::write(&file_path, invalid_content).unwrap();
265
266 let service = DocumentValidationService::new();
267 let result = service.validate_document(&file_path).await.unwrap();
268
269 assert!(!result.is_valid);
270 assert!(!result.errors.is_empty());
271 }
272
273 #[tokio::test]
274 async fn test_detect_document_type() {
275 let temp_dir = tempdir().unwrap();
276 let file_path = temp_dir.path().join("vision.md");
277
278 let vision_content = r##"---
280id: test-vision
281title: Test Vision
282level: vision
283created_at: 2023-01-01T00:00:00Z
284updated_at: 2023-01-01T00:00:00Z
285archived: false
286tags:
287 - "#vision"
288 - "#phase/draft"
289exit_criteria_met: false
290---
291
292# Test Vision
293
294This is a test vision document.
295"##;
296 fs::write(&file_path, vision_content).unwrap();
297
298 let service = DocumentValidationService::new();
299 let doc_type = service.detect_document_type(&file_path).await.unwrap();
300
301 assert_eq!(doc_type, DocumentType::Vision);
302 }
303
304 #[tokio::test]
305 async fn test_validate_document_as_type() {
306 let temp_dir = tempdir().unwrap();
307 let file_path = temp_dir.path().join("vision.md");
308
309 let vision_content = r##"---
311id: test-vision
312title: Test Vision
313level: vision
314created_at: 2023-01-01T00:00:00Z
315updated_at: 2023-01-01T00:00:00Z
316archived: false
317tags:
318 - "#vision"
319 - "#phase/draft"
320exit_criteria_met: false
321---
322
323# Test Vision
324
325This is a test vision document.
326"##;
327 fs::write(&file_path, vision_content).unwrap();
328
329 let service = DocumentValidationService::new();
330
331 assert!(service
333 .validate_document_as_type(&file_path, DocumentType::Vision)
334 .await
335 .unwrap());
336
337 assert!(!service
339 .validate_document_as_type(&file_path, DocumentType::Strategy)
340 .await
341 .unwrap());
342 }
343
344 #[tokio::test]
345 async fn test_validate_nonexistent_file() {
346 let service = DocumentValidationService::new();
347 let result = service.validate_document("/nonexistent/file.md").await;
348
349 assert!(result.is_err());
350 assert!(matches!(result.unwrap_err(), MetisError::NotFound(_)));
351 }
352}