use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct CleanedItem {
pub path: PathBuf,
pub size: u64,
pub item_type: CleanedItemType,
pub label: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CleanedItemType {
File,
Directory,
SymLink,
}
impl CleanedItem {
pub fn new(
path: PathBuf,
size: u64,
item_type: CleanedItemType,
label: impl Into<String>,
) -> Self {
Self {
path,
size,
item_type,
label: label.into(),
}
}
pub fn file(path: PathBuf, size: u64, label: impl Into<String>) -> Self {
Self::new(path, size, CleanedItemType::File, label)
}
pub fn directory(path: PathBuf, size: u64, label: impl Into<String>) -> Self {
Self::new(path, size, CleanedItemType::Directory, label)
}
pub fn path_str(&self) -> String {
self.path.to_string_lossy().to_string()
}
pub fn filename(&self) -> String {
self.path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| self.path_str())
}
}
#[derive(Debug, Clone)]
pub struct CleaningResult {
pub total_bytes: u64,
pub items: Vec<CleanedItem>,
}
impl CleaningResult {
pub fn new() -> Self {
Self {
total_bytes: 0,
items: Vec::new(),
}
}
pub fn add_item(&mut self, item: CleanedItem) {
self.total_bytes += item.size;
self.items.push(item);
}
pub fn add_items(&mut self, items: Vec<CleanedItem>) {
for item in items {
self.add_item(item);
}
}
pub fn merge(&mut self, other: CleaningResult) {
self.total_bytes += other.total_bytes;
self.items.extend(other.items);
}
pub fn item_count(&self) -> usize {
self.items.len()
}
}
impl Default for CleaningResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RunOptions {
pub skip_confirmation: bool,
pub dry_run: bool,
}
impl RunOptions {
pub const fn execute() -> Self {
Self {
skip_confirmation: true,
dry_run: false,
}
}
pub const fn execute_with_confirmation() -> Self {
Self {
skip_confirmation: false,
dry_run: false,
}
}
pub const fn preview() -> Self {
Self {
skip_confirmation: true,
dry_run: true,
}
}
}
pub type CleanerFn = fn(RunOptions) -> anyhow::Result<CleaningResult>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cleaned_item_file_has_label() {
let item = CleanedItem::file(PathBuf::from("/tmp/foo"), 42, "Temp file");
assert_eq!(item.label, "Temp file");
assert_eq!(item.item_type, CleanedItemType::File);
assert_eq!(item.size, 42);
}
#[test]
fn cleaned_item_directory_has_label() {
let item = CleanedItem::directory(PathBuf::from("/tmp/dir"), 100, "Cache dir");
assert_eq!(item.label, "Cache dir");
assert_eq!(item.item_type, CleanedItemType::Directory);
}
#[test]
fn cleaning_result_add_item_updates_total() {
let mut result = CleaningResult::new();
result.add_item(CleanedItem::file(PathBuf::from("/a"), 10, "a"));
result.add_item(CleanedItem::file(PathBuf::from("/b"), 20, "b"));
assert_eq!(result.total_bytes, 30);
assert_eq!(result.item_count(), 2);
}
#[test]
fn cleaning_result_merge_combines_totals_and_items() {
let mut a = CleaningResult::new();
a.add_item(CleanedItem::file(PathBuf::from("/a"), 10, "a"));
let mut b = CleaningResult::new();
b.add_item(CleanedItem::file(PathBuf::from("/b"), 5, "b"));
a.merge(b);
assert_eq!(a.total_bytes, 15);
assert_eq!(a.item_count(), 2);
}
#[test]
fn cleaning_result_default_is_empty() {
let result = CleaningResult::default();
assert_eq!(result.total_bytes, 0);
assert_eq!(result.item_count(), 0);
}
#[test]
fn filename_falls_back_to_path_str_without_file_name() {
let item = CleanedItem::file(PathBuf::from("/"), 0, "root");
assert_eq!(item.filename(), item.path_str());
}
}
#[cfg(test)]
mod run_options_tests {
use super::RunOptions;
#[test]
fn execute_skips_confirmation_and_is_not_dry_run() {
let opts = RunOptions::execute();
assert!(opts.skip_confirmation);
assert!(!opts.dry_run);
}
#[test]
fn execute_with_confirmation_prompts_and_is_not_dry_run() {
let opts = RunOptions::execute_with_confirmation();
assert!(!opts.skip_confirmation);
assert!(!opts.dry_run);
}
#[test]
fn preview_skips_confirmation_and_is_dry_run() {
let opts = RunOptions::preview();
assert!(opts.skip_confirmation);
assert!(opts.dry_run);
}
}