use crate::domain::documents::types::DocumentType;
use crate::Result;
use crate::{Adr, Initiative, MetisError, Strategy, Task, Vision};
use std::path::Path;
pub struct DocumentValidationService;
#[derive(Debug)]
pub struct ValidationResult {
pub document_type: DocumentType,
pub is_valid: bool,
pub errors: Vec<String>,
}
impl DocumentValidationService {
pub fn new() -> Self {
Self
}
pub async fn validate_document<P: AsRef<Path>>(
&self,
file_path: P,
) -> Result<ValidationResult> {
let file_path = file_path.as_ref();
if !file_path.exists() {
return Err(MetisError::NotFound("File does not exist".to_string()));
}
if !file_path.is_file() {
return Err(MetisError::NotFound("Path is not a file".to_string()));
}
let mut validation_results = Vec::new();
match Vision::from_file(file_path).await {
Ok(_vision) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Vision,
is_valid: true,
errors: vec![],
});
}
Err(e) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Vision,
is_valid: false,
errors: vec![format!("Vision validation failed: {}", e)],
});
}
}
match Strategy::from_file(file_path).await {
Ok(_strategy) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Strategy,
is_valid: true,
errors: vec![],
});
}
Err(e) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Strategy,
is_valid: false,
errors: vec![format!("Strategy validation failed: {}", e)],
});
}
}
match Initiative::from_file(file_path).await {
Ok(_initiative) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Initiative,
is_valid: true,
errors: vec![],
});
}
Err(e) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Initiative,
is_valid: false,
errors: vec![format!("Initiative validation failed: {}", e)],
});
}
}
match Task::from_file(file_path).await {
Ok(_task) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Task,
is_valid: true,
errors: vec![],
});
}
Err(e) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Task,
is_valid: false,
errors: vec![format!("Task validation failed: {}", e)],
});
}
}
match Adr::from_file(file_path).await {
Ok(_adr) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Adr,
is_valid: true,
errors: vec![],
});
}
Err(e) => {
validation_results.push(ValidationResult {
document_type: DocumentType::Adr,
is_valid: false,
errors: vec![format!("ADR validation failed: {}", e)],
});
}
}
if let Some(valid_result) = validation_results.iter().find(|r| r.is_valid) {
return Ok(ValidationResult {
document_type: valid_result.document_type,
is_valid: true,
errors: vec![],
});
}
let all_errors: Vec<String> = validation_results
.into_iter()
.flat_map(|r| r.errors)
.collect();
Ok(ValidationResult {
document_type: DocumentType::Vision, is_valid: false,
errors: all_errors,
})
}
pub async fn detect_document_type<P: AsRef<Path>>(&self, file_path: P) -> Result<DocumentType> {
let result = self.validate_document(file_path).await?;
if result.is_valid {
Ok(result.document_type)
} else {
Err(MetisError::InvalidDocument(format!(
"Could not determine document type: {}",
result.errors.join("; ")
)))
}
}
pub async fn validate_document_as_type<P: AsRef<Path>>(
&self,
file_path: P,
expected_type: DocumentType,
) -> Result<bool> {
let file_path = file_path.as_ref();
match expected_type {
DocumentType::Vision => match Vision::from_file(file_path).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
},
DocumentType::Strategy => match Strategy::from_file(file_path).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
},
DocumentType::Initiative => match Initiative::from_file(file_path).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
},
DocumentType::Task => match Task::from_file(file_path).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
},
DocumentType::Adr => match Adr::from_file(file_path).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
},
}
}
pub async fn is_valid_document<P: AsRef<Path>>(&self, file_path: P) -> bool {
self.validate_document(file_path)
.await
.map(|result| result.is_valid)
.unwrap_or(false)
}
}
impl Default for DocumentValidationService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[tokio::test]
async fn test_validate_valid_vision_document() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("vision.md");
let vision_content = r##"---
id: test-vision
title: Test Vision
level: vision
short_code: TEST-V-0801
created_at: 2023-01-01T00:00:00Z
updated_at: 2023-01-01T00:00:00Z
archived: false
tags:
- "#vision"
- "#phase/draft"
exit_criteria_met: false
---
# Test Vision
This is a test vision document.
"##;
fs::write(&file_path, vision_content).unwrap();
let service = DocumentValidationService::new();
let result = service.validate_document(&file_path).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.document_type, DocumentType::Vision);
assert!(result.errors.is_empty());
}
#[tokio::test]
async fn test_validate_invalid_document() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("invalid.md");
let invalid_content = r##"# Invalid Document
This has no frontmatter.
"##;
fs::write(&file_path, invalid_content).unwrap();
let service = DocumentValidationService::new();
let result = service.validate_document(&file_path).await.unwrap();
assert!(!result.is_valid);
assert!(!result.errors.is_empty());
}
#[tokio::test]
async fn test_detect_document_type() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("vision.md");
let vision_content = r##"---
id: test-vision
title: Test Vision
level: vision
short_code: TEST-V-0802
created_at: 2023-01-01T00:00:00Z
updated_at: 2023-01-01T00:00:00Z
archived: false
tags:
- "#vision"
- "#phase/draft"
exit_criteria_met: false
---
# Test Vision
This is a test vision document.
"##;
fs::write(&file_path, vision_content).unwrap();
let service = DocumentValidationService::new();
let doc_type = service.detect_document_type(&file_path).await.unwrap();
assert_eq!(doc_type, DocumentType::Vision);
}
#[tokio::test]
async fn test_validate_document_as_type() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("vision.md");
let vision_content = r##"---
id: test-vision
title: Test Vision
level: vision
short_code: TEST-V-0802
created_at: 2023-01-01T00:00:00Z
updated_at: 2023-01-01T00:00:00Z
archived: false
tags:
- "#vision"
- "#phase/draft"
exit_criteria_met: false
---
# Test Vision
This is a test vision document.
"##;
fs::write(&file_path, vision_content).unwrap();
let service = DocumentValidationService::new();
assert!(service
.validate_document_as_type(&file_path, DocumentType::Vision)
.await
.unwrap());
assert!(!service
.validate_document_as_type(&file_path, DocumentType::Strategy)
.await
.unwrap());
}
#[tokio::test]
async fn test_validate_nonexistent_file() {
let service = DocumentValidationService::new();
let result = service.validate_document("/nonexistent/file.md").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), MetisError::NotFound(_)));
}
}