use std::convert::TryFrom; use std::io::Write; use clap::ArgMatches; use chrono::{DateTime, Utc}; use crate::database::Database; use crate::error::{Error, Result}; use crate::editor; use crate::commands::Command; use crate::config::Config; use crate::timeparse::parse_time; use crate::old::{time_or_warning, warn_if_needed}; #[derive(Default)] pub struct Args { pub at: Option>, pub note: Option, } impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { type Error = Error; fn try_from(matches: &'a ArgMatches) -> Result { Ok(Args { at: matches.value_of("at").map(|s| parse_time(s)).transpose()?, note: matches.value_of("note").map(|s| s.into()), }) } } pub struct InCommand {} impl<'a> Command<'a> for InCommand { type Args = Args; fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> where D: Database, O: Write, E: Write, { let start = args.at.unwrap_or(now); let sheet = db.current_sheet()?.unwrap_or_else(|| "default".into()); if db.running_entry(&sheet)?.is_some() { writeln!(out, "Timer is already running for sheet '{}'", sheet)?; return Ok(()); } let note = if let Some(note) = args.note { Some(note) } else if !config.require_note { None } else { Some(editor::get_string(config.note_editor.as_ref())?) }; let (start, needs_warning) = time_or_warning(start, db)?; db.entry_insert(start, None, note.map(|n| n.trim().to_owned()), &sheet)?; writeln!(out, "Checked into sheet \"{}\".", sheet)?; warn_if_needed(err, needs_warning)?; Ok(()) } } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use ansi_term::Color::Yellow; use chrono::{TimeZone, Local}; use crate::test_utils::PrettyString; use crate::database::SqliteDatabase; use super::*; #[test] fn handles_new_entry() { let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola".into()), }; let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc::now(); d.init().unwrap(); assert_eq!(d.entries_full(None, None).unwrap().len(), 0); InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, Some("hola".into())); assert_eq!(e.start, now); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Checked into sheet \"default\".\n")); assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString("")); } #[test] fn test_handles_already_running_entry() { let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola".into()), }; let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc::now(); d.init().unwrap(); d.entry_insert(now, None, None, "default".into()).unwrap(); assert_eq!(d.entries_full(None, None).unwrap().len(), 1); InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), Utc::now()).unwrap(); assert_eq!(d.entries_full(None, None).unwrap().len(), 1); assert_eq!( PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Timer is already running for sheet 'default'\n") ); } #[test] fn no_note_and_no_mandatory_leaves_none() { let mut d = SqliteDatabase::from_memory().unwrap(); let args = Default::default(); let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc::now(); let config = Config { require_note: false, ..Default::default() }; d.init().unwrap(); assert_eq!(d.entries_full(None, None).unwrap().len(), 0); InCommand::handle(args, &mut d, &mut out, &mut err, &config, now).unwrap(); let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, None); assert_eq!(e.start, now); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); } #[test] fn warns_if_old_database() { let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola".into()), }; let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc::now(); d.init_old().unwrap(); assert_eq!(d.entries_full(None, None).unwrap().len(), 0); InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, Some("hola".into())); assert_eq!(e.start, Utc.from_utc_datetime(&now.with_timezone(&Local).naive_local())); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Checked into sheet \"default\".\n")); assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString(&format!( "{} You are using the old timetrap format, it is advised that \ you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")))); } #[test] fn notes_are_trimmed() { let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola\n".into()), }; let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc::now(); d.init().unwrap(); assert_eq!(d.entries_full(None, None).unwrap().len(), 0); InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, Some("hola".into())); assert_eq!(e.start, now); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Checked into sheet \"default\".\n")); assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString("")); } }