use crate::error::{Error, Result};
use crate::filter::TaskFilter;
use crate::validation::ValidationWarning;
use crate::Taskdn;
use std::path::{Path, PathBuf};
impl Taskdn {
pub fn validate_task(&self, path: impl AsRef<Path>) -> Result<()> {
let task = self.get_task(path)?;
let warnings = task.validate();
let errors: Vec<&ValidationWarning> = warnings
.iter()
.filter(|w| matches!(w, ValidationWarning::MissingCompletedAt))
.collect();
if errors.is_empty() {
Ok(())
} else {
let error_messages: Vec<String> = errors.iter().map(|w| w.message()).collect();
Err(Error::Validation {
path: task.path,
message: error_messages.join("; "),
})
}
}
#[must_use]
pub fn validate_all_tasks(&self) -> Vec<(PathBuf, Error)> {
let mut errors = Vec::new();
let tasks = match self.list_tasks(&TaskFilter::new().include_archive_dir()) {
Ok(tasks) => tasks,
Err(e) => {
errors.push((self.config.tasks_dir.clone(), e));
return errors;
}
};
for task in tasks {
let warnings = task.validate();
let has_errors = warnings
.iter()
.any(|w| matches!(w, ValidationWarning::MissingCompletedAt));
if has_errors {
let error_messages: Vec<String> = warnings
.iter()
.filter(|w| matches!(w, ValidationWarning::MissingCompletedAt))
.map(ValidationWarning::message)
.collect();
errors.push((
task.path.clone(),
Error::Validation {
path: task.path,
message: error_messages.join("; "),
},
));
}
}
errors
}
pub fn get_task_warnings(&self, path: impl AsRef<Path>) -> Result<Vec<ValidationWarning>> {
let task = self.get_task(path)?;
Ok(task.validate())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::NewTask;
use crate::TaskdnConfig;
use std::fs;
use tempfile::TempDir;
fn setup_test_env() -> (TempDir, Taskdn) {
let temp = TempDir::new().unwrap();
let tasks_dir = temp.path().join("tasks");
let projects_dir = temp.path().join("projects");
let areas_dir = temp.path().join("areas");
fs::create_dir_all(&tasks_dir).unwrap();
fs::create_dir_all(&projects_dir).unwrap();
fs::create_dir_all(&areas_dir).unwrap();
let config = TaskdnConfig::new(tasks_dir, projects_dir, areas_dir);
let sdk = Taskdn::new(config).unwrap();
(temp, sdk)
}
mod validate_task {
use super::*;
#[test]
fn valid_task_passes() {
let (_temp, sdk) = setup_test_env();
let path = sdk.create_task(NewTask::new("Valid Task")).unwrap();
let result = sdk.validate_task(&path);
assert!(result.is_ok());
}
#[test]
fn task_without_completed_at_when_done_fails() {
let (_temp, sdk) = setup_test_env();
let content = r#"---
title: Invalid Done Task
status: done
created-at: 2025-01-01
updated-at: 2025-01-02
---
"#;
let path = sdk.config.tasks_dir.join("invalid.md");
fs::write(&path, content).unwrap();
let result = sdk.validate_task(&path);
assert!(matches!(result, Err(Error::Validation { .. })));
}
}
mod validate_all_tasks {
use super::*;
#[test]
fn all_valid_returns_empty() {
let (_temp, sdk) = setup_test_env();
sdk.create_task(NewTask::new("Task 1")).unwrap();
sdk.create_task(NewTask::new("Task 2")).unwrap();
let errors = sdk.validate_all_tasks();
assert!(errors.is_empty());
}
#[test]
fn returns_errors_for_invalid_tasks() {
let (_temp, sdk) = setup_test_env();
sdk.create_task(NewTask::new("Valid")).unwrap();
let content = r#"---
title: Invalid
status: done
created-at: 2025-01-01
updated-at: 2025-01-02
---
"#;
let path = sdk.config.tasks_dir.join("invalid.md");
fs::write(&path, content).unwrap();
let errors = sdk.validate_all_tasks();
assert_eq!(errors.len(), 1);
assert!(errors[0].0.ends_with("invalid.md"));
}
#[test]
fn includes_archived_tasks() {
let (_temp, sdk) = setup_test_env();
let archive_dir = sdk.config.tasks_dir.join("archive");
fs::create_dir_all(&archive_dir).unwrap();
let content = r#"---
title: Archived Invalid
status: done
created-at: 2025-01-01
updated-at: 2025-01-02
---
"#;
let path = archive_dir.join("archived-invalid.md");
fs::write(&path, content).unwrap();
let errors = sdk.validate_all_tasks();
assert_eq!(errors.len(), 1);
assert!(errors[0].0.to_string_lossy().contains("archive"));
}
}
mod get_task_warnings {
use super::*;
#[test]
fn returns_warnings() {
let (_temp, sdk) = setup_test_env();
let content = r#"---
title: Multi Project Task
status: inbox
created-at: 2025-01-01
updated-at: 2025-01-02
projects:
- "[[Project A]]"
- "[[Project B]]"
---
"#;
let path = sdk.config.tasks_dir.join("multi-project.md");
fs::write(&path, content).unwrap();
let warnings = sdk.get_task_warnings(&path).unwrap();
assert!(!warnings.is_empty());
}
}
}