From dd93d788b5f9a03a7312f83aa2fcf1ee7c9835af Mon Sep 17 00:00:00 2001 From: Kenneth Love <11908+kennethlove@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:46:50 -0700 Subject: [PATCH] Back to vec (#4) * Using a Vec again. Sorting works. * Cleaned up imports, renamed "name" to "task" * Added rstest and used it. Small fixes and clean up. Renamed sorting to get_sorted * small renames, etc --- Cargo.lock | 99 +++++++++++++++++++++++++ Cargo.toml | 3 + src/cli.rs | 175 +++++++++++++++++++++++++------------------- src/db.rs | 197 +++++++++++++++++++++++++++----------------------- src/streak.rs | 8 +- src/tui.rs | 16 +--- 6 files changed, 316 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b18bf90..18c279e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -697,6 +697,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" @@ -732,6 +738,12 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "globset" version = "0.4.14" @@ -1374,6 +1386,15 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1537,6 +1558,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.5" @@ -1607,12 +1634,51 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rstest" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.72", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.34" @@ -1725,6 +1791,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.204" @@ -1818,6 +1890,7 @@ dependencies = [ "ratatui 0.27.0", "reqwest", "ron", + "rstest", "serde", "serde_json", "tabled", @@ -2124,6 +2197,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2606,6 +2696,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index fcc3c78..97aa465 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,6 @@ tui-input = "0.9.0" tui_confirm_dialog = "0.2.2" unicode-width = "0.1.13" uuid = { version = "1.10.0", features = ["serde", "v4", "fast-rng"] } + +[dev-dependencies] +rstest = "0.22.0" \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index ce36c60..8b1895f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::path::Path; use ansi_term::{Color, Style}; @@ -29,8 +28,8 @@ struct Cli { enum Commands { #[command(about = "List all streaks", long_about = None, short_flag = 'l')] List { - #[clap(short, long)] - sort_by: Option, + #[arg(long, default_value = "task+", help = "Sort by field")] + sort_by: String, }, #[command(about = "Create a new streak", long_about = None, short_flag = 'a')] Add { @@ -38,7 +37,7 @@ enum Commands { frequency: Frequency, #[clap(short, long)] - name: String, + task: String, }, #[command(about = "Get one streak", long_about = None, short_flag='o')] Get { ident: String }, @@ -53,7 +52,7 @@ enum Commands { /// Create a new daily streak item fn new_daily(name: String, db: &mut Database) -> Result> { let streak = Streak::new_daily(name); - db.streaks.lock().unwrap().insert(streak.id, streak.clone()); + db.streaks.push(streak.clone()); db.save()?; Ok(streak) } @@ -61,26 +60,25 @@ fn new_daily(name: String, db: &mut Database) -> Result Result> { let streak = Streak::new_weekly(name); - db.streaks.lock().unwrap().insert(streak.id, streak.clone()); + db.streaks.push(streak.clone()); db.save()?; Ok(streak) } +#[allow(dead_code)] /// Get all streaks -fn get_all( - mut db: Database, - sort: Option<(SortByField, SortByDirection)>, -) -> HashMap { - match db.get_all(sort) { +fn get_all(mut db: Database) -> Vec { + match db.get_all() { Some(streaks) => streaks.clone(), - None => HashMap::::new(), + None => Vec::::new(), } } /// Get one single streak #[allow(dead_code)] fn get_one(db: &mut Database, id: Uuid) -> Option { - if let Some(streak) = db.streaks.lock().unwrap().get(&id) { + let streak = db.streaks.clone().into_iter().find(|s| s.id == id); + if let Some(streak) = streak { return Some(streak.clone()); } None @@ -102,7 +100,6 @@ fn get_one_by_id(db: &mut Database, ident: &str) -> Option { } /// Check in to a streak today -/// Index is decremented by 1 to match 0-based indexing fn checkin(db: &mut Database, ident: &str) -> Result<(), Box> { let mut streak = get_one_by_id(db, ident).unwrap(); if let Some(check_in) = streak.last_checkin { @@ -125,12 +122,12 @@ fn delete(db: &mut Database, ident: &str) -> Result<(), Box) -> String { +fn build_table(streaks: Vec) -> String { let mut builder = Builder::new(); let header_style = Style::new().italic(); builder.push_record([ header_style.paint("\nIdent").to_string(), - header_style.paint("\nStreak").to_string(), + header_style.paint("\nTask").to_string(), header_style.paint("\nFreq").to_string(), header_style.paint("\nStatus").to_string(), header_style.paint("\nLast Check In").to_string(), @@ -138,16 +135,15 @@ fn build_table(streaks: HashMap) -> String { header_style.paint("Longest\nStreak").to_string(), header_style.paint("\nTotal").to_string(), ]); - // let zipper: Vec<_> = (1..).zip(streaks.iter()).collect(); - // for (idx, (_id, streak)) in zipper.iter() { - for (id, streak) in streaks.iter() { + + for streak in streaks.iter() { let mut wrapped_text = String::new(); let wrapped_lines = textwrap::wrap(&streak.task.as_str(), 60); for line in wrapped_lines { wrapped_text.push_str(&format!("{line}")); } - let id = &id.to_string()[0..5]; + let id = &streak.id.to_string()[0..5]; let index = Style::new().bold().paint(format!("{}", id)); let streak_name = Style::new().bold().paint(wrapped_text); let frequency = Style::new().paint(format!("{}", &streak.frequency)); @@ -166,6 +162,7 @@ fn build_table(streaks: HashMap) -> String { let total_checkins = Style::new() .bold() .paint(format!("{:^5}", &streak.total_checkins)); + builder.push_record([ index.to_string(), streak_name.to_string(), @@ -187,8 +184,9 @@ pub fn get_database_url() -> String { path.to_string_lossy().to_string() } +#[derive(Debug)] pub enum SortByField { - Name, + Task, Frequency, LastCheckIn, CurrentStreak, @@ -196,35 +194,45 @@ pub enum SortByField { TotalCheckins, } +#[derive(Debug)] pub enum SortByDirection { Ascending, Descending, } -pub fn get_sort_order(sort_by: Option) -> Option<(SortByField, SortByDirection)> { - let (sort_field, sort_direction) = match &sort_by { - Some(sort) => { - let direction = match sort.chars().next().unwrap() { - '+' => SortByDirection::Ascending, - '-' => SortByDirection::Descending, - _ => SortByDirection::Ascending, - }; - let field_name: String = sort.chars().skip_while(|&c| c == '+' || c == '-').collect(); - - let field = match field_name.as_str() { - "name" => SortByField::Name, - "frequency" => SortByField::Frequency, - "last_checkin" => SortByField::LastCheckIn, - "current_streak" => SortByField::CurrentStreak, - "longest_streak" => SortByField::LongestStreak, - "total_checkins" => SortByField::TotalCheckins, - _ => SortByField::Name, - }; - (field, direction) - } - None => (SortByField::Name, SortByDirection::Ascending), +pub fn get_sort_order(sort_by: String) -> Option<(SortByField, SortByDirection)> { + let sign = match sort_by.chars().rev().next().unwrap() { + '+' => Some(SortByDirection::Ascending), + '-' => Some(SortByDirection::Descending), + _ => None, + }; + if sign.is_none() { + return None; + } + + let ln = sort_by.len() - 1; + let field = match sort_by[..ln].to_lowercase().as_str() { + "task" => SortByField::Task, + "streak" => SortByField::Task, + "name" => SortByField::Task, + "frequency" => SortByField::Frequency, + "freq" => SortByField::Frequency, + "last_checkin" => SortByField::LastCheckIn, + "last-checkin" => SortByField::LastCheckIn, + "last" => SortByField::LastCheckIn, + "current_streak" => SortByField::CurrentStreak, + "current-streak" => SortByField::CurrentStreak, + "current" => SortByField::CurrentStreak, + "longest_streak" => SortByField::LongestStreak, + "longest-streak" => SortByField::LongestStreak, + "longest" => SortByField::LongestStreak, + "total_checkins" => SortByField::TotalCheckins, + "total-checkins" => SortByField::TotalCheckins, + "total" => SortByField::TotalCheckins, + _ => SortByField::Task, }; - Some((sort_field, sort_direction)) + + Some((field, sign.unwrap())) } /// Parses command line options @@ -234,9 +242,9 @@ pub fn parse() { let mut db = Database::new(db_url.as_str()).expect("Could not load database"); let response_style = Style::new().bold().fg(Color::Green); match &cli.command { - Commands::Add { name, frequency } => match frequency { + Commands::Add { task, frequency } => match frequency { Frequency::Daily => { - let streak = new_daily(name.to_string(), &mut db).unwrap(); + let streak = new_daily(task.to_string(), &mut db).unwrap(); let response = response_style .paint("Created a new daily streak:") .to_string(); @@ -244,7 +252,7 @@ pub fn parse() { println!("{tada} {response} {}", streak.task); } Frequency::Weekly => { - let streak = new_weekly(name.to_string(), &mut db).unwrap(); + let streak = new_weekly(task.to_string(), &mut db).unwrap(); let response = response_style .paint("Created a new weekly streak:") .to_string(); @@ -253,14 +261,16 @@ pub fn parse() { } }, Commands::List { sort_by } => { - let streak_list = get_all(db, get_sort_order(sort_by.clone())); + let sort_by = get_sort_order(sort_by.to_string()); + let streak_list = match sort_by { + Some((field, direction)) => db.get_sorted(field, direction), + None => db.get_all().unwrap(), + }; println!("{}", build_table(streak_list)); } Commands::Get { ident } => { - let streak = db.get_by_id(&ident).unwrap(); - let mut hash = HashMap::::new(); - hash.insert(streak.id, streak); - println!("{}", build_table(hash)); + let streak = vec![db.get_by_id(&ident).unwrap()]; + println!("{}", build_table(streak)); } Commands::CheckIn { ident } => match checkin(&mut db, ident) { Ok(_) => { @@ -291,21 +301,20 @@ pub fn parse() { #[cfg(test)] mod tests { - use std::sync::Mutex; - use assert_cmd::Command; use assert_fs::TempDir; + use rstest::*; - lazy_static::lazy_static! { - static ref FILE_LOCK: Mutex<()> = Mutex::new(()); + #[fixture] + pub fn command() -> Command { + Command::cargo_bin("skidmarks").unwrap() } - #[test] - fn get_all() { + #[rstest] + fn get_all(mut command: Command) { let temp = TempDir::new().unwrap(); - let mut cmd = Command::cargo_bin("skidmarks").unwrap(); - let list_assert = cmd + let list_assert = command .arg("--database-url") .arg(format!("{}{}", temp.path().display(), "test-get-all.ron")) .arg("list") @@ -313,15 +322,14 @@ mod tests { list_assert.success(); } - #[test] - fn new_daily_command() { + #[rstest] + fn new_daily_command(mut command: Command) { let temp = TempDir::new().unwrap(); - let mut cmd = Command::cargo_bin("skidmarks").unwrap(); - let add_assert = cmd + let add_assert = command .arg("--database-url") .arg(format!("{}{}", temp.path().display(), "test-new-daily.ron")) .arg("add") - .arg("--name") + .arg("--task") .arg("Test Streak") .arg("--frequency") .arg("daily") @@ -329,11 +337,10 @@ mod tests { add_assert.success(); } - #[test] - fn new_weekly_command() { + #[rstest] + fn new_weekly_command(mut command: Command) { let temp = TempDir::new().unwrap(); - let mut cmd = Command::cargo_bin("skidmarks").unwrap(); - let add_assert = cmd + let add_assert = command .arg("--database-url") .arg(format!( "{}{}", @@ -341,7 +348,7 @@ mod tests { "test-new-weekly.ron" )) .arg("add") - .arg("--name") + .arg("--task") .arg("Test Streak") .arg("--frequency") .arg("weekly") @@ -349,20 +356,36 @@ mod tests { add_assert.success(); } - #[test] - fn test_sort_order() { + #[rstest] + fn test_sort_order( + #[values( + "task+", + "task-", + "frequency+", + "frequency-", + "last_checkin+", + "last_checkin-", + "current_streak+", + "current_streak-", + "longest_streak+", + "longest_streak-", + "total_checkins+", + "total_checkins-" + )] + sort_string: &str, + mut command: Command, + ) { let temp = TempDir::new().unwrap(); - let mut cmd = Command::cargo_bin("skidmarks").unwrap(); - let list_assert = cmd + let list_assert = command .arg("--database-url") .arg(format!( "{}{}", temp.path().display(), "test-sort-order.ron" )) - .arg("--sort-by") - .arg("+name") .arg("list") + .arg("--sort-by") + .arg(sort_string) .assert(); list_assert.success(); } diff --git a/src/db.rs b/src/db.rs index 4917af4..be32aa0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,7 +1,6 @@ -use std::collections::HashMap; use std::fs::{File, OpenOptions}; use std::io::Write; -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use crate::cli::{SortByDirection, SortByField}; use crate::streak::Streak; @@ -14,8 +13,8 @@ lazy_static::lazy_static! { #[derive(Debug)] pub struct Database { pub filename: String, - // pub streaks: Arc>>, - pub streaks: Arc>>, + pub streaks: Vec, // pub streaks: Arc>>, + // pub streaks: Arc>>, } impl Clone for Database { @@ -29,15 +28,14 @@ impl Clone for Database { impl PartialEq for Database { fn eq(&self, other: &Self) -> bool { - self.filename == other.filename - && *self.streaks.lock().unwrap() == *other.streaks.lock().unwrap() + self.filename == other.filename && *self.streaks == *other.streaks } } impl Database { fn create_if_missing(filename: &str) -> Result<(), std::io::Error> { // let data = "[]".as_bytes(); - let data = "{}".as_bytes(); + let data = "[]".as_bytes(); let metadata = match std::fs::metadata(filename) { Ok(meta) => meta, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { @@ -62,19 +60,19 @@ impl Database { } pub fn num_tasks(&self) -> usize { - self.streaks.lock().unwrap().len() + self.streaks.len() } - fn load_database(filename: &str) -> Result, std::io::Error> { + fn load_database(filename: &str) -> Result, std::io::Error> { Self::create_if_missing(filename)?; let contents = std::fs::read_to_string(filename)?; - let decoded: HashMap = - ron::de::from_str(&contents).unwrap_or_else(|_| HashMap::::new()); + let decoded: Vec = + ron::de::from_str(&contents).unwrap_or_else(|_| Vec::::new()); Ok(decoded) } fn save_database(&self, filename: &str) { - let streaks: HashMap = self.streaks.lock().unwrap().clone(); + let streaks: Vec = self.streaks.clone(); let encoded = ron::ser::to_string(&streaks).unwrap(); let mut file = OpenOptions::new() .write(true) @@ -91,32 +89,37 @@ impl Database { } pub fn create_from_string(filename: &str, data: &str) -> Result { - let db = Self::new(filename)?; - let streaks: HashMap = ron::de::from_str(data).unwrap(); - let mut db_streaks = db.streaks.lock().unwrap(); - for (id, streak) in streaks { - db_streaks.insert(id, streak); + let mut db = Self::new(filename)?; + let streaks: Vec = ron::de::from_str(data).unwrap(); + for streak in streaks { + db.streaks.push(streak); } - Ok(db.clone()) + Ok(db) } pub fn add(&mut self, streak: Streak) -> Result<(), std::io::Error> { - let mut streaks = self.streaks.lock().unwrap(); - streaks.insert(streak.id.clone(), streak); + let mut streaks = self.streaks.clone(); + streaks.push(streak); + self.streaks = streaks; Ok(()) } pub fn update(&mut self, id: Uuid, streak: Streak) -> Result<(), std::io::Error> { - let mut streaks = self.streaks.lock().unwrap(); - let old_streak: &mut Streak = streaks.get_mut(&id).unwrap(); - let _ = old_streak.update(streak); + self.delete(id)?; + let mut streaks = self.streaks.clone(); + streaks.push(streak); + self.streaks = streaks; Ok(()) } pub fn delete(&mut self, id: Uuid) -> Result<(), std::io::Error> { - let mut streaks = self.streaks.lock().unwrap(); - streaks.remove(&id); - + let streaks = self.streaks.clone(); + let filtered_streaks = streaks + .iter() + .filter(|s| s.id != id) + .cloned() + .collect::>(); + self.streaks = filtered_streaks; Ok(()) } @@ -124,66 +127,68 @@ impl Database { Self::create_if_missing(filename)?; let existing_db = Self::load_database(filename)?; let new_db = Self { - streaks: Arc::new(Mutex::new(existing_db.clone())), + streaks: existing_db.clone(), filename: filename.to_string(), }; Ok(new_db) } - pub fn get_all( - &mut self, - sort_order: Option<(SortByField, SortByDirection)>, - ) -> Option> { - let streaks = self.streaks.lock(); - match streaks { - Ok(streaks) => { - if streaks.is_empty() { - Some(HashMap::::new()) - } else { - let mut streaks = streaks.clone(); - streaks = self.sort(streaks, sort_order); - Some(streaks) - } - } - _ => None, + pub fn get_all(&mut self) -> Option> { + match self.streaks.len() { + 0 => None, + _ => Some(self.streaks.clone()), } } - fn sort( + pub fn get_sorted( &self, - streaks: HashMap, - sort_order: Option<(SortByField, SortByDirection)>, - ) -> HashMap { - if let Some((field, direction)) = sort_order { - let mut streaks = streaks.values().map(|s| s.clone()).collect::>(); - match field { - SortByField::Name => match direction { - SortByDirection::Ascending => streaks.sort_by(|a, b| a.task.cmp(&b.task)), - SortByDirection::Descending => streaks.sort_by(|a, b| b.task.cmp(&a.task)), - }, - SortByField::Frequency => { - todo!(); - } - SortByField::LastCheckIn => { - todo!(); - } - SortByField::CurrentStreak => { - todo!(); - } - SortByField::LongestStreak => { - todo!() - } - SortByField::TotalCheckins => { - todo!() - } + sort_field: SortByField, + sort_direction: SortByDirection, + ) -> Vec { + let mut streaks = self.streaks.clone(); + match (sort_field, sort_direction) { + (SortByField::Task, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.task.cmp(&b.task)) + } + (SortByField::Task, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.task.cmp(&a.task)) + } + (SortByField::Frequency, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.frequency.cmp(&b.frequency)) + } + (SortByField::Frequency, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.frequency.cmp(&a.frequency)) + } + (SortByField::LastCheckIn, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.last_checkin.cmp(&b.last_checkin)) + } + (SortByField::LastCheckIn, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.last_checkin.cmp(&a.last_checkin)) + } + (SortByField::CurrentStreak, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.current_streak.cmp(&b.current_streak)) + } + (SortByField::CurrentStreak, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.current_streak.cmp(&a.current_streak)) + } + (SortByField::LongestStreak, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.longest_streak.cmp(&b.longest_streak)) + } + (SortByField::LongestStreak, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.longest_streak.cmp(&a.longest_streak)) + } + (SortByField::TotalCheckins, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.total_checkins.cmp(&b.total_checkins)) + } + (SortByField::TotalCheckins, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.total_checkins.cmp(&a.total_checkins)) } } streaks } pub fn get_one(&mut self, id: Uuid) -> Option { - let streaks = self.streaks.lock().unwrap(); - let streak = streaks.get(&id); + let streak = self.streaks.iter().find(|s| s.id == id); match streak { Some(streak) => Some(streak.clone()), None => None, @@ -191,8 +196,7 @@ impl Database { } pub fn get_by_index(&mut self, index: usize) -> Option { - let streaks = self.streaks.lock().unwrap(); - let streak = streaks.values().nth(index); + let streak = self.streaks.iter().nth(index); match streak { Some(streak) => Some(streak.clone()), None => None, @@ -200,9 +204,9 @@ impl Database { } pub fn get_by_id(&mut self, ident: &str) -> Option { - let streaks = self.streaks.lock().unwrap(); - let streak = streaks - .values() + let streak = self + .streaks + .iter() .find(|s| s.id.to_string()[0..5].to_string() == ident); match streak { Some(streak) => Some(streak.clone()), @@ -214,7 +218,7 @@ impl Database { impl Default for Database { fn default() -> Self { Self { - streaks: Arc::new(Mutex::new(HashMap::::new())), + streaks: Vec::::new(), filename: "skidmarks.ron".to_string(), } } @@ -226,7 +230,7 @@ mod tests { use super::*; - const DATABASE_PRELOAD: &str = r#" {"00e8a16c-0edd-4e90-8c3f-2ee7aa6a2210":(id:"00e8a16c-0edd-4e90-8c3f-2ee7aa6a2210",task:"Poop",frequency:Daily,last_checkin:Some("2024-08-06"),total_checkins:1),"77cbbb3f-2690-45a9-9a30-94a53556d93e":(id:"77cbbb3f-2690-45a9-9a30-94a53556d93e",task:"Take a walk",frequency:Daily,last_checkin:Some("2024-08-06"),total_checkins:1),"af1f4cc5-87b1-40b1-9fa3-2e0344d35d3b":(id:"af1f4cc5-87b1-40b1-9fa3-2e0344d35d3b",task:"Eat brekkie",frequency:Daily,last_checkin:Some("2024-08-06"),total_checkins:1)}"#; + const DATABASE_PRELOAD: &str = r#"[(id:"00e8a16c-0edd-4e90-8c3f-2ee7aa6a2210",task:"Poop",frequency:Daily,last_checkin:Some("2024-08-06"),total_checkins:2),(id:"77cbbb3f-2690-45a9-9a30-94a53556d93e",task:"Take a walk",frequency:Daily,last_checkin:Some("2024-08-07"),total_checkins:1),(id:"af1f4cc5-87b1-40b1-9fa3-2e0344d35d3b",task:"Eat brekkie",frequency:Daily,last_checkin:Some("2024-08-05"),total_checkins:3)]"#; #[test] fn create_if_missing() { let temp = assert_fs::TempDir::new().unwrap(); @@ -289,8 +293,7 @@ mod tests { db.save().unwrap(); let expected_content = format!( - r#"{{"{}":(id:"{}",task:"{}",frequency:Daily,last_checkin:{:?},current_streak:{},longest_streak:{},total_checkins:{})}}"#, - streak.id, + r#"[(id:"{}",task:"{}",frequency:Daily,last_checkin:{:?},current_streak:{},longest_streak:{},total_checkins:{})]"#, streak.id, streak.task, streak.last_checkin, @@ -315,9 +318,9 @@ mod tests { let streak = Streak::new_daily("brush teeth".to_string()); db.add(streak.clone()).unwrap(); - let result = db.get_all(None).unwrap(); + let result = db.get_all().unwrap(); assert_eq!(result.len(), 1); - assert_eq!(result.get(&streak.id).unwrap(), &streak); + assert_eq!(result[0], streak); temp.close().unwrap(); } @@ -335,9 +338,9 @@ mod tests { streak.task = "floss".to_string(); db.update(streak.id, streak.clone()).unwrap(); - let result = db.get_all(None).unwrap(); + let result = db.get_all().unwrap(); assert_eq!(result.len(), 1); - assert_eq!(result.get(&streak.id).unwrap().task, "floss"); + assert_eq!(result[0].task, "floss"); temp.close().unwrap(); } @@ -351,11 +354,12 @@ mod tests { let mut db = Database::new(file_path).unwrap(); let streak = Streak::new_daily("brush teeth".to_string()); db.add(streak.clone()).unwrap(); + assert!(db.get_all().is_some()); db.delete(streak.id).unwrap(); - let result = db.get_all(None).unwrap(); - assert!(result.is_empty()); + let result = db.get_all(); + assert!(result.is_none()); temp.close().unwrap(); } @@ -372,10 +376,10 @@ mod tests { db.add(streak1.clone()).unwrap(); db.add(streak2.clone()).unwrap(); - let result = db.get_all(None).unwrap(); + let result = db.get_all().unwrap(); assert_eq!(result.len(), 2); - assert_eq!(result.get(&streak1.id).unwrap(), &streak1); - assert_eq!(result.get(&streak2.id).unwrap(), &streak2); + assert_eq!(result[0], streak1); + assert_eq!(result[1], streak2); temp.close().unwrap(); } @@ -406,9 +410,22 @@ mod tests { let mut db = Database::create_from_string(file_path, DATABASE_PRELOAD).unwrap(); let result = db.get_by_index(1).unwrap(); - let expected = db.streaks.lock().unwrap().values().nth(1).unwrap().clone(); + let expected = db.streaks.iter().nth(1).unwrap().clone(); assert_eq!(expected, result); temp.close().unwrap(); } + + #[test] + fn sort_by_task() { + let temp = assert_fs::TempDir::new().unwrap(); + let db_file = temp.child("test_sort_by_task.ron"); + let file_path = db_file.to_str().unwrap(); + + let db = Database::create_from_string(file_path, DATABASE_PRELOAD).unwrap(); + let result = db.get_sorted(SortByField::Task, SortByDirection::Ascending); + assert_ne!(db.streaks.clone()[..], result[..]); + + temp.close().unwrap(); + } } diff --git a/src/streak.rs b/src/streak.rs index 7e97517..77027fa 100644 --- a/src/streak.rs +++ b/src/streak.rs @@ -6,7 +6,9 @@ use clap::ValueEnum; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Clone, Debug, Default, PartialEq, ValueEnum, Serialize, Deserialize)] +#[derive( + Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd, ValueEnum, Serialize, Deserialize, +)] pub enum Frequency { #[default] Daily, @@ -39,7 +41,7 @@ impl Frequency { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum Status { Waiting, Done, @@ -56,7 +58,7 @@ impl Display for Status { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Streak { pub id: Uuid, #[serde(default)] diff --git a/src/tui.rs b/src/tui.rs index be1a5d8..937a47d 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -91,9 +91,8 @@ impl App { let mut db = Database::new(&get_database_url()).expect("Failed to load database"); let data_vec: Vec = db - .get_all(None) + .get_all() .unwrap_or_default() - .into_values() .into_iter() .map(Data::from) .collect(); @@ -118,9 +117,8 @@ impl App { pub fn refresh(&mut self) { let data_vec: Vec = self .db - .get_all(None) + .get_all() .unwrap_or_default() - .into_values() .into_iter() .map(Data::from) .collect(); @@ -169,15 +167,7 @@ impl App { pub fn remove(&mut self) { let selected = self.state.selected().unwrap(); - let streak = self - .db - .get_all(None) - .unwrap() - .into_values() - .collect::>() - .get(selected) - .unwrap() - .clone(); + let streak = self.db.get_all().unwrap().get(selected).unwrap().clone(); let _ = self.db.delete(streak.id); let _ = self.db.save();