tiempo-rs/src/commands/in.rs

223 lines
6.5 KiB
Rust

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<DateTime<Utc>>,
pub note: Option<String>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
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<D, O, E>(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> 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.trim().to_owned())
} else if !config.require_note {
None
} else {
Some(editor::get_string(config.note_editor.as_deref(), None)?)
};
let (start, needs_warning) = time_or_warning(start, db)?;
db.entry_insert(start, None, note, &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::Ps;
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!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked into sheet \"default\".\n"));
assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps(""));
}
#[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!(
Ps(&String::from_utf8_lossy(&out)),
Ps("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!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked into sheet \"default\".\n"));
assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps(&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!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked into sheet \"default\".\n"));
assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps(""));
}
}