the resume command

This commit is contained in:
Abraham Toriz 2021-07-26 16:55:24 -05:00
parent 149aad7a3a
commit 4011cac6e9
No known key found for this signature in database
GPG Key ID: D5B4A746DB5DD42A
7 changed files with 187 additions and 3 deletions

View File

@ -17,6 +17,7 @@ pub mod week;
pub mod month;
pub mod list;
pub mod out;
pub mod resume;
pub trait Command<'a> {
type Args: TryFrom<&'a ArgMatches<'a>>;

View File

@ -14,8 +14,8 @@ use crate::old::{time_or_warning, warn_if_needed};
#[derive(Default)]
pub struct Args {
at: Option<DateTime<Utc>>,
note: Option<String>,
pub at: Option<DateTime<Utc>>,
pub note: Option<String>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {

View File

@ -42,6 +42,7 @@ impl<'a> Command<'a> for OutCommand {
writeln!(out, "Checked out of sheet \"{}\".", sheet)?;
db.entry_update(entry.id, entry.start, Some(end), entry.note, &entry.sheet)?;
db.set_last_checkout_id(entry.id)?;
} else {
writeln!(out, "No running entry on sheet \"{}\".", sheet)?;
}
@ -83,6 +84,7 @@ mod tests {
assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Checked out of sheet \"default\".\n"));
assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString(""));
assert_eq!(db.last_checkout_id().unwrap().unwrap(), 1);
}
#[test]

145
src/commands/resume.rs Normal file
View File

@ -0,0 +1,145 @@
use std::convert::TryFrom;
use std::io::Write;
use clap::ArgMatches;
use chrono::{DateTime, Utc};
use crate::error::{Error, Result};
use crate::timeparse::parse_time;
use crate::config::Config;
use crate::database::Database;
use super::{Command, r#in};
#[derive(Default)]
pub struct Args {
id: Option<u64>,
at: Option<DateTime<Utc>>,
}
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()?,
id: matches.value_of("id").map(|i| i.parse().unwrap()),
})
}
}
pub struct ResumeCommand;
impl<'a> Command<'a> for ResumeCommand {
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 entry_id = args.id.or(db.last_checkout_id()?);
if let Some(entry_id) = entry_id {
if let Some(entry) = db.entry_by_id(entry_id)? {
writeln!(out, "Resuming \"{}\" from entry #{}", entry.note.as_ref().unwrap_or(&"".to_owned()), entry.id)?;
r#in::InCommand::handle(r#in::Args {
at: args.at,
note: entry.note,
}, db, out, err, config, now)
} else {
writeln!(out, "The entry with id '{}' could not be found to be resumed. Perhaps it was deleted?", entry_id)?;
Ok(())
}
} else {
writeln!(out, "No entry to resume. Either no --id was given or no last_checkout_id was found in the database.
Perhaps start an entry with t in")?;
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use chrono::Duration;
use crate::test_utils::PrettyString;
use crate::database::SqliteDatabase;
use super::*;
#[test]
fn resume_an_entry() {
let args = Default::default();
let mut db = SqliteDatabase::from_memory().unwrap();
let mut out = Vec::new();
let mut err = Vec::new();
let now = Utc::now();
let one_hour_ago = now - Duration::hours(1);
let two_hours_ago = now - Duration::hours(2);
db.init().unwrap();
db.entry_insert(two_hours_ago, Some(one_hour_ago), Some("fake note".into()), "default").unwrap();
db.set_last_checkout_id(1).unwrap();
assert_eq!(db.entries_full(None, None).unwrap().len(), 1);
ResumeCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap();
let all_entries = db.entries_full(None, None).unwrap();
assert_eq!(all_entries.len(), 2);
assert_eq!(all_entries[1].id, 2);
assert_eq!(all_entries[1].start, now);
assert_eq!(all_entries[1].end, None);
assert_eq!(all_entries[1].note, Some("fake note".into()));
assert_eq!(all_entries[1].sheet, "default");
assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Resuming \"fake note\" from entry #1
Checked into sheet \"default\".\n"));
assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString(""));
}
#[test]
fn no_entries_to_resume() {
let args = Default::default();
let mut db = SqliteDatabase::from_memory().unwrap();
let mut out = Vec::new();
let mut err = Vec::new();
let now = Utc::now();
db.init().unwrap();
ResumeCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("\
No entry to resume. Either no --id was given or no last_checkout_id was found in the database.
Perhaps start an entry with t in
"));
}
#[test]
fn last_checkout_id_does_not_exist() {
let args = Default::default();
let mut db = SqliteDatabase::from_memory().unwrap();
let mut out = Vec::new();
let mut err = Vec::new();
let now = Utc::now();
db.init().unwrap();
db.set_last_checkout_id(23).unwrap();
ResumeCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("\
The entry with id '23' could not be found to be resumed. Perhaps it was deleted?
"));
}
}

View File

@ -157,6 +157,10 @@ pub trait Database {
])
}
fn entry_by_id(&self, id: u64) -> Result<Option<Entry>> {
Ok(self.entry_query("select * from entries where id=?1", &[&id])?.into_iter().next())
}
fn running_entry(&self, sheet: &str) -> Result<Option<Entry>> {
Ok(self.entry_query("select * from entries where end is null and sheet=?1", &[&sheet])?.into_iter().next())
}
@ -188,6 +192,24 @@ pub trait Database {
Ok(())
}
fn last_checkout_id(&self) -> Result<Option<u64>> {
let results = self.meta_query("select * from meta where key='last_checkout_id'", &[])?;
Ok(results.into_iter().next().map(|m| m.value.parse().map_err(|_| {
Error::CorruptedData(format!(
"Found value '{}' for key 'last_checkout_id' in meta table which is not a valid integer",
m.value
))
})).transpose()?)
}
fn set_last_checkout_id(&mut self, id: u64) -> Result<()> {
self.execute("DELETE FROM meta WHERE key='last_checkout_id'", &[])?;
self.execute("INSERT INTO meta (key, value) VALUES ('last_checkout_id', ?1)", &[&id])?;
Ok(())
}
fn version(&self) -> Result<DBVersion> {
let results = self.meta_query("select * from meta where key='database_version'", &[])?;

View File

@ -69,7 +69,11 @@ some human times, for now restricted to time ago:
#[error("CSV Error: {0}")]
CSVError(#[from] csv::Error),
#[error("Corrupted data found in the database: {0}")]
#[error("Corrupted data found in the database:
{0}
To fix this first find where your database is located using t configure and
query it using t backend or the sqlite3 command provided by your system.")]
CorruptedData(String),
#[error("Trying to parse {0} as a time in your timezone led to no results")]

View File

@ -4,6 +4,7 @@ use std::io;
use clap::{App, Arg, SubCommand, AppSettings, crate_version, ArgMatches};
use chrono::Utc;
use regex::Regex;
use tiempo::error;
use tiempo::database::SqliteDatabase;
@ -12,6 +13,7 @@ use tiempo::commands::{
Command, r#in::InCommand, display::DisplayCommand, sheet::SheetCommand,
today::TodayCommand, yesterday::YesterdayCommand, week::WeekCommand,
month::MonthCommand, list::ListCommand, out::OutCommand,
resume::ResumeCommand,
};
fn error_trap(matches: ArgMatches) -> error::Result<()> {
@ -25,6 +27,7 @@ fn error_trap(matches: ArgMatches) -> error::Result<()> {
match matches.subcommand() {
("in", Some(matches)) => InCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
("out", Some(matches)) => OutCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
("resume", Some(matches)) => ResumeCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
("display", Some(matches)) => DisplayCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
("today", Some(matches)) => TodayCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
@ -81,9 +84,16 @@ fn main() {
.takes_value(true).value_name("TIME")
.help("Use this time instead of now");
let num_re = Regex::new(r"^\d+$").unwrap();
let id_arg = Arg::with_name("id")
.long("id")
.takes_value(true).value_name("ID")
.validator(move |v| if num_re.is_match(&v) {
Ok(())
} else {
Err(format!("the --id arg must be a number. '{}' is not a valid number", v))
})
.help("Use entry with ID instead of the last entry");
// Now declar this app's cli