use crate::commands::{CmdMessage, CmdResult};
use crate::error::{PadzError, Result};
use crate::index::{DisplayIndex, PadSelector};
use crate::model::{Scope, TodoStatus};
use crate::store::Bucket;
use crate::store::DataStore;
use uuid::Uuid;
use super::helpers::{indexed_pads, pads_by_selectors};
pub fn run<S: DataStore>(
store: &mut S,
scope: Scope,
selectors: &[PadSelector],
recursive: bool,
confirmed: bool,
include_done: bool,
) -> Result<CmdResult> {
let pads_to_purge = if selectors.is_empty() {
let all_pads = indexed_pads(store, scope)?;
all_pads
.into_iter()
.filter(|dp| {
matches!(dp.index, DisplayIndex::Deleted(_))
|| (include_done && dp.pad.metadata.status == TodoStatus::Done)
})
.collect()
} else {
pads_by_selectors(store, scope, selectors, true)?
};
if pads_to_purge.is_empty() {
let mut res = CmdResult::default();
res.add_message(CmdMessage::info("No pads to purge."));
return Ok(res);
}
let target_ids: Vec<Uuid> = pads_to_purge.iter().map(|dp| dp.pad.metadata.id).collect();
let descendants = super::helpers::get_descendant_ids(store, scope, &target_ids)?;
if !descendants.is_empty() && !recursive {
return Err(PadzError::Api(format!(
"Cannot purge: {} pad(s) have children. Use --recursive (-r) to purge entire subtrees.",
pads_to_purge
.iter()
.filter(|dp| {
let id = dp.pad.metadata.id;
super::helpers::get_descendant_ids(store, scope, &[id])
.map(|d| !d.is_empty())
.unwrap_or(false)
})
.count()
)));
}
let total_count = pads_to_purge.len() + descendants.len();
if !confirmed {
return Err(PadzError::Api(format!(
"Purging {} pad(s). Aborted, confirm with --yes or -y for hard deletion.",
total_count
)));
}
let mut all_ids = target_ids;
all_ids.extend(descendants.clone());
all_ids.sort();
all_ids.dedup();
let mut result = CmdResult::default();
result.add_message(CmdMessage::info(format!(
"Purging {} pad(s)...",
total_count
)));
for id in all_ids {
for &bucket in &[Bucket::Deleted, Bucket::Active, Bucket::Archived] {
if store.get_pad(&id, scope, bucket).is_ok() {
store.delete_pad(&id, scope, bucket)?;
break;
}
}
}
for dp in pads_to_purge {
result.add_message(CmdMessage::success(format!(
"Purged: {} {}",
dp.index, dp.pad.metadata.title
)));
}
if !descendants.is_empty() {
result.add_message(CmdMessage::success(format!(
"And purged {} descendant(s)",
descendants.len()
)));
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::{create, delete, get};
use crate::index::DisplayIndex;
use crate::model::Scope;
use crate::store::bucketed::BucketedStore;
use crate::store::mem_backend::MemBackend;
#[test]
fn purges_deleted_pads_when_confirmed() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
delete::run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
let deleted = get::run(
&store,
Scope::Project,
get::PadFilter {
status: get::PadStatusFilter::Deleted,
..Default::default()
},
&[],
)
.unwrap();
assert_eq!(deleted.listed_pads.len(), 1);
let res = run(
&mut store,
Scope::Project,
&[],
false, true, false, )
.unwrap();
assert!(res.messages.iter().any(|m| m.content.contains("Purging 1")));
assert!(res
.messages
.iter()
.any(|m| m.content.contains("Purged: d1 A")));
let deleted_after = get::run(
&store,
Scope::Project,
get::PadFilter {
status: get::PadStatusFilter::Deleted,
..Default::default()
},
&[],
)
.unwrap();
assert_eq!(deleted_after.listed_pads.len(), 0);
}
#[test]
fn purge_without_confirmation_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
delete::run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
let result = run(
&mut store,
Scope::Project,
&[],
false, false, false, );
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Aborted"));
assert!(err.to_string().contains("--yes"));
assert!(err.to_string().contains("-y"));
let deleted = get::run(
&store,
Scope::Project,
get::PadFilter {
status: get::PadStatusFilter::Deleted,
..Default::default()
},
&[],
)
.unwrap();
assert_eq!(deleted.listed_pads.len(), 1);
}
#[test]
fn purges_specific_pads_even_if_active() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
let res = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
false, true, false, )
.unwrap();
assert!(res
.messages
.iter()
.any(|m| m.content.contains("Purged: 1 A")));
let listed = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(listed.listed_pads.len(), 0);
}
#[test]
fn does_nothing_if_no_deleted_pads() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
let res = run(&mut store, Scope::Project, &[], false, true, false).unwrap();
assert_eq!(res.messages.len(), 1);
assert!(res.messages[0].content.contains("No pads to purge"));
let listed = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(listed.listed_pads.len(), 1);
}
#[test]
fn purges_recursively_with_flag() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
delete::run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
let res = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Deleted(1)])],
true, true, false, )
.unwrap();
assert!(res.messages.iter().any(|m| m.content.contains("Purging 2"))); assert!(res
.messages
.iter()
.any(|m| m.content.contains("Purged: d1 Parent")));
assert!(res
.messages
.iter()
.any(|m| m.content.contains("And purged 1 descendant")));
assert_eq!(
store
.list_pads(Scope::Project, Bucket::Active)
.unwrap()
.len(),
0
);
assert_eq!(
store
.list_pads(Scope::Project, Bucket::Deleted)
.unwrap()
.len(),
0
);
}
#[test]
fn purge_without_recursive_fails_when_has_children() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
let result = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
false, true, false, );
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("have children"));
assert!(err.to_string().contains("--recursive"));
let all_pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
assert_eq!(all_pads.len(), 2);
}
#[test]
fn purge_selectors_vs_all() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "B".into(), "".into(), None).unwrap();
delete::run(
&mut store,
Scope::Project,
&[
PadSelector::Path(vec![DisplayIndex::Regular(1)]),
PadSelector::Path(vec![DisplayIndex::Regular(2)]),
],
)
.unwrap();
let res = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Deleted(1)])],
false, true, false, )
.unwrap();
assert!(res.messages.iter().any(|m| m.content.contains("Purging 1")));
let remaining = store.list_pads(Scope::Project, Bucket::Deleted).unwrap();
assert_eq!(remaining.len(), 1); }
#[test]
fn purge_nothing_found() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let res = run(&mut store, Scope::Project, &[], false, true, false).unwrap();
assert!(res.messages[0].content.contains("No pads to purge"));
}
#[test]
fn purge_error_includes_count() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "B".into(), "".into(), None).unwrap();
delete::run(
&mut store,
Scope::Project,
&[
PadSelector::Path(vec![DisplayIndex::Regular(1)]),
PadSelector::Path(vec![DisplayIndex::Regular(2)]),
],
)
.unwrap();
let result = run(&mut store, Scope::Project, &[], false, false, false);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Purging 2 pad(s)"));
}
#[test]
fn purge_include_done_removes_completed_pads() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Keep Me".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Complete Me".into(),
"".into(),
None,
)
.unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
let res = run(&mut store, Scope::Project, &[], false, true, true).unwrap();
assert!(res.messages.iter().any(|m| m.content.contains("Purging 1")));
let listed = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(listed.listed_pads.len(), 1);
assert_eq!(listed.listed_pads[0].pad.metadata.title, "Keep Me");
}
#[test]
fn purge_include_done_false_ignores_completed_pads() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
let res = run(&mut store, Scope::Project, &[], false, true, false).unwrap();
assert!(res.messages[0].content.contains("No pads to purge"));
let listed = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(listed.listed_pads.len(), 1);
}
#[test]
fn purge_include_done_and_deleted_together() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Done Pad".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Deleted Pad".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Active Pad".into(),
"".into(),
None,
)
.unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(3)])],
)
.unwrap();
delete::run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(2)])],
)
.unwrap();
let res = run(&mut store, Scope::Project, &[], false, true, true).unwrap();
assert!(res.messages.iter().any(|m| m.content.contains("Purging 2")));
let listed = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(listed.listed_pads.len(), 1);
assert_eq!(listed.listed_pads[0].pad.metadata.title, "Active Pad");
}
}