use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
use crate::category::Category;
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum SortOrder {
Size,
Name,
}
#[derive(Debug, Clone)]
pub enum EntryKind {
File,
Dir(Vec<Entry>),
}
#[derive(Debug, Clone)]
pub struct Entry {
pub name: String,
pub size: u64,
pub category: Category,
pub modified_days_ago: Option<u64>,
pub kind: EntryKind,
}
impl Entry {
pub fn file(
name: String,
size: u64,
category: Category,
modified_days_ago: Option<u64>,
) -> Self {
Self {
name,
size,
category,
modified_days_ago,
kind: EntryKind::File,
}
}
pub fn dir(
name: String,
category: Category,
modified_days_ago: Option<u64>,
children: Vec<Entry>,
) -> Self {
let size = children.iter().map(|c| c.size).sum();
Self {
name,
size,
category,
modified_days_ago,
kind: EntryKind::Dir(children),
}
}
pub fn is_dir(&self) -> bool {
matches!(self.kind, EntryKind::Dir(_))
}
pub fn children(&self) -> Option<&[Entry]> {
match &self.kind {
EntryKind::Dir(c) => Some(c),
EntryKind::File => None,
}
}
pub fn children_mut(&mut self) -> Option<&mut Vec<Entry>> {
match &mut self.kind {
EntryKind::Dir(c) => Some(c),
EntryKind::File => None,
}
}
pub fn sort(&mut self, order: &SortOrder, reverse: bool) {
if let Some(children) = self.children_mut() {
for child in children.iter_mut() {
child.sort(order, reverse);
}
children.sort_by(|a, b| {
let cmp = match order {
SortOrder::Size => b.size.cmp(&a.size),
SortOrder::Name => a.name.cmp(&b.name),
};
if reverse {
cmp.reverse()
} else {
cmp
}
});
}
}
}
impl Serialize for Entry {
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
let mut s = ser.serialize_struct("Entry", 6)?;
s.serialize_field("name", &self.name)?;
s.serialize_field("size", &self.size)?;
s.serialize_field("is_dir", &self.is_dir())?;
s.serialize_field("category", &self.category)?;
if let Some(d) = self.modified_days_ago {
s.serialize_field("modified_days_ago", &d)?;
}
if let EntryKind::Dir(children) = &self.kind {
s.serialize_field("children", children)?;
}
s.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn leaf(name: &str, size: u64) -> Entry {
Entry::file(name.to_string(), size, Category::Other, None)
}
fn dir_with(name: &str, children: Vec<Entry>) -> Entry {
Entry::dir(name.to_string(), Category::Other, None, children)
}
#[test]
fn sort_by_size_descending_by_default() {
let mut root = dir_with(
"root",
vec![leaf("small", 10), leaf("big", 1000), leaf("medium", 100)],
);
root.sort(&SortOrder::Size, false);
let names: Vec<_> = root
.children()
.unwrap()
.iter()
.map(|e| e.name.clone())
.collect();
assert_eq!(names, vec!["big", "medium", "small"]);
}
#[test]
fn sort_by_name_ascending() {
let mut root = dir_with("root", vec![leaf("c", 1), leaf("a", 2), leaf("b", 3)]);
root.sort(&SortOrder::Name, false);
let names: Vec<_> = root
.children()
.unwrap()
.iter()
.map(|e| e.name.clone())
.collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn sort_reverse_flips_order() {
let mut root = dir_with("root", vec![leaf("a", 1), leaf("b", 2), leaf("c", 3)]);
root.sort(&SortOrder::Size, true);
let names: Vec<_> = root
.children()
.unwrap()
.iter()
.map(|e| e.name.clone())
.collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn sort_recurses_into_children() {
let mut root = dir_with(
"root",
vec![dir_with("inner", vec![leaf("z", 1), leaf("a", 2)])],
);
root.sort(&SortOrder::Name, false);
let inner = &root.children().unwrap()[0];
let names: Vec<_> = inner
.children()
.unwrap()
.iter()
.map(|e| e.name.clone())
.collect();
assert_eq!(names, vec!["a", "z"]);
}
#[test]
fn dir_size_is_sum_of_children() {
let d = dir_with("root", vec![leaf("a", 10), leaf("b", 20), leaf("c", 30)]);
assert_eq!(d.size, 60);
assert!(d.is_dir());
}
#[test]
fn file_has_no_children() {
let f = leaf("a.txt", 42);
assert!(!f.is_dir());
assert!(f.children().is_none());
}
#[test]
fn json_round_trip_preserves_wire_shape() {
let root = dir_with("root", vec![leaf("a.txt", 10)]);
let json = serde_json::to_string(&root).unwrap();
assert!(json.contains("\"is_dir\":true"));
assert!(json.contains("\"children\":["));
assert!(json.contains("\"is_dir\":false"));
assert!(!json.contains("modified_days_ago"));
}
}