the resume command
This commit is contained in:
parent
149aad7a3a
commit
4011cac6e9
|
@ -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>>;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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?
|
||||
"));
|
||||
}
|
||||
}
|
|
@ -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'", &[])?;
|
||||
|
||||
|
|
|
@ -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")]
|
||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue