use crate::error::ScrapsResult;
use crate::usecase::scrap::scrap_ref::ScrapRef;
use scraps_libs::markdown::query::TaskStatus;
use scraps_libs::model::scrap::Scrap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusFilter {
Open,
Done,
Deferred,
All,
}
impl StatusFilter {
fn matches(self, status: &TaskStatus) -> bool {
matches!(
(self, status),
(StatusFilter::All, _)
| (StatusFilter::Open, TaskStatus::Open)
| (StatusFilter::Done, TaskStatus::Done)
| (StatusFilter::Deferred, TaskStatus::Deferred)
)
}
}
pub fn status_label(status: &TaskStatus) -> &'static str {
match status {
TaskStatus::Open => "open",
TaskStatus::Done => "done",
TaskStatus::Deferred => "deferred",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TodoResult {
pub scrap: ScrapRef,
pub status: TaskStatus,
pub text: String,
pub line: usize,
}
pub struct TodoUsecase;
impl TodoUsecase {
pub fn new() -> TodoUsecase {
TodoUsecase
}
pub fn execute(
&self,
scraps: &[Scrap],
status_filter: StatusFilter,
) -> ScrapsResult<Vec<TodoResult>> {
let mut results: Vec<TodoResult> = Vec::new();
for scrap in scraps {
for item in scrap.task_items() {
if !status_filter.matches(&item.status) {
continue;
}
results.push(TodoResult {
scrap: scrap.into(),
status: item.status.clone(),
text: item.text.clone(),
line: item.line,
});
}
}
results.sort_by_key(|result| (result.scrap.sort_key(), result.line));
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
use scraps_libs::model::context::Ctx;
fn ctx(s: &str) -> Ctx {
Ctx::from(s)
}
#[test]
fn it_aggregates_open_tasks_by_default() {
let scraps = vec![
Scrap::new("a", &None, "- [ ] one\n- [x] two\n"),
Scrap::new("b", &None, "- [-] three\n- [ ] four\n"),
];
let results = TodoUsecase::new()
.execute(&scraps, StatusFilter::Open)
.unwrap();
assert_eq!(results.len(), 2);
let texts: Vec<&str> = results.iter().map(|r| r.text.as_str()).collect();
assert!(texts.contains(&"one"));
assert!(texts.contains(&"four"));
}
#[test]
fn it_filters_by_status_done() {
let scraps = vec![Scrap::new(
"a",
&None,
"- [ ] open\n- [x] done\n- [-] deferred\n",
)];
let results = TodoUsecase::new()
.execute(&scraps, StatusFilter::Done)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].status, TaskStatus::Done);
assert_eq!(results[0].text, "done");
}
#[test]
fn it_filters_by_status_deferred() {
let scraps = vec![Scrap::new(
"a",
&None,
"- [ ] open\n- [x] done\n- [-] deferred\n",
)];
let results = TodoUsecase::new()
.execute(&scraps, StatusFilter::Deferred)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].status, TaskStatus::Deferred);
}
#[test]
fn it_returns_all_statuses_when_filter_is_all() {
let scraps = vec![Scrap::new(
"a",
&None,
"- [ ] open\n- [x] done\n- [-] deferred\n",
)];
let results = TodoUsecase::new()
.execute(&scraps, StatusFilter::All)
.unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn it_records_line_number_and_ctx() {
let scraps = vec![Scrap::new(
"borrowing",
&Some(ctx("programming/rust")),
"# borrowing\n\n- [ ] implement Drop for X\n",
)];
let results = TodoUsecase::new()
.execute(&scraps, StatusFilter::Open)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].text, "implement Drop for X");
assert_eq!(results[0].line, 3);
assert_eq!(results[0].scrap.title, "borrowing");
assert_eq!(
results[0].scrap.ctx().as_ref().map(|c| c.to_string()),
Some("programming/rust".to_string())
);
}
#[test]
fn it_returns_empty_when_no_scraps_have_tasks() {
let scraps = vec![Scrap::new("a", &None, "# Just text, no tasks.\n")];
let results = TodoUsecase::new()
.execute(&scraps, StatusFilter::Open)
.unwrap();
assert!(results.is_empty());
}
}