use std::convert::TryFrom; use std::io::{BufRead, Write}; use std::str::FromStr; use clap::ArgMatches; use chrono::{DateTime, Utc}; use regex::Regex; use crate::error::{Result, Error}; use crate::database::Database; use crate::formatters::Formatter; use crate::timeparse::parse_time; use crate::regex::parse_regex; use crate::old::{entries_or_warning, time_or_warning, warn_if_needed}; use crate::io::Streams; use super::{Command, Facts}; // ---------------------------------------------------------------- // Things that are general to all commands that display in some way // ---------------------------------------------------------------- pub enum Sheet { All, Full, Sheet(String), } impl FromStr for Sheet { type Err = Error; fn from_str(name: &str) -> Result { Ok(match name { "all" => Sheet::All, "full" => Sheet::Full, name => Sheet::Sheet(name.into()), }) } } #[allow(clippy::too_many_arguments)] pub fn entries_for_display( start: Option>, end: Option>, sheet: Option, streams: &mut Streams, format: Formatter, ids: bool, grep: Option, facts: &Facts, ) -> Result<()> where D: Database, I: BufRead, O: Write, E: Write, { let start = start.map(|s| time_or_warning(s, &streams.db)).transpose()?.map(|s| s.0); let end = end.map(|e| time_or_warning(e, &streams.db)).transpose()?.map(|e| e.0); let mut entries = match sheet { Some(Sheet::All) => streams.db.entries_all_visible(start, end)?, Some(Sheet::Full) => streams.db.entries_full(start, end)?, Some(Sheet::Sheet(name)) => streams.db.entries_by_sheet(&name, start, end)?, None => { let current_sheet = streams.db.current_sheet()?; streams.db.entries_by_sheet(¤t_sheet, start, end)? } }; if let Some(re) = grep { entries.retain(|e| re.is_match(&e.note.clone().unwrap_or_default())); } let (entries, needs_warning) = entries_or_warning(entries, &streams.db)?; format.print_formatted( entries, &mut streams.out, &mut streams.err, facts, ids, )?; warn_if_needed(&mut streams.err, needs_warning, &facts.env)?; Ok(()) } // ------------------------------------ // The actual implementation of display // ------------------------------------ #[derive(Default)] pub struct Args { ids: bool, start: Option>, end: Option>, format: Option, grep: Option, sheet: Option, } impl<'a> TryFrom<&'a ArgMatches> for Args { type Error = Error; fn try_from(matches: &'a ArgMatches) -> Result { Ok(Args { ids: matches.is_present("ids"), start: matches.value_of("start").map(parse_time).transpose()?, end: matches.value_of("end").map(parse_time).transpose()?, format: matches.value_of("format").map(|v| v.parse()).transpose()?, grep: matches.value_of("grep").map(parse_regex).transpose()?, sheet: matches.value_of("sheet").map(|s| s.parse()).transpose()?, }) } } pub struct DisplayCommand { } impl<'a> Command<'a> for DisplayCommand { type Args = Args; fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, I: BufRead, O: Write, E: Write, { entries_for_display( args.start, args.end, args.sheet, streams, args.format.unwrap_or_else(|| facts.config.commands.display.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()), args.ids, args.grep, facts ) } } #[cfg(test)] mod tests { use chrono::TimeZone; use pretty_assertions::{assert_eq, assert_str_eq}; use crate::database::SqliteDatabase; use crate::config::{Config, CommandsSettings, BaseCommandSettings}; use super::*; #[test] fn display_as_local_time_if_previous_version() { std::env::set_var("TZ", "CST+6"); let args = Default::default(); let mut streams = Streams::fake(b"").with_db( SqliteDatabase::from_path("assets/test_old_db.db").unwrap() ); let facts = Facts::new(); DisplayCommand::handle(args, &mut streams, &facts).unwrap(); assert_eq!(&String::from_utf8_lossy(&streams.out), "Timesheet: default Day Start End Duration Notes Tue Jun 29, 2021 06:26:49 - 07:26:52 1:00:03 lets do some rust 1:00:03 ----------------------------------------------------------------------- Total 1:00:03 "); assert_eq!( String::from_utf8_lossy(&streams.err), "[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n" ); } #[test] fn filter_by_start() { let args = Args { format: Some(Formatter::Csv), start: Some(Utc.with_ymd_and_hms(2021, 6, 30, 10, 5, 0).unwrap()), ..Default::default() }; let mut streams = Streams::fake(b""); let facts = Facts::new(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap(); DisplayCommand::handle(args, &mut streams, &facts).unwrap(); assert_eq!(&String::from_utf8_lossy(&streams.out), "start,end,note,sheet 2021-06-30T10:10:00.000000Z,,hola,default "); assert_eq!( String::from_utf8_lossy(&streams.err), String::new(), ); } #[test] fn filter_by_match() { let mut streams = Streams::fake(b""); let facts = Facts::new(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("adios".into()), "default").unwrap(); entries_for_display(None, None, None, &mut streams, Formatter::Csv, true, Some("io".parse().unwrap()), &facts).unwrap(); assert_eq!(&String::from_utf8_lossy(&streams.out), "id,start,end,note,sheet 2,2021-06-30T10:10:00.000000Z,,adios,default "); assert_eq!( String::from_utf8_lossy(&streams.err), String::new(), ); } #[test] fn entries_are_grouped_despite_database() { let args = Args { sheet: Some(Sheet::All), ..Default::default() }; let mut streams = Streams::fake(b""); let facts = Facts::new(); std::env::set_var("TZ", "CST+6"); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap()), None, "sheet1").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap()), None, "sheet2").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 13, 0, 0).unwrap()), None, "sheet1").unwrap(); DisplayCommand::handle(args, &mut streams, &facts).unwrap(); assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Timesheet: sheet1 Day Start End Duration Notes Wed Jun 30, 2021 04:00:00 - 05:00:00 1:00:00 06:00:00 - 07:00:00 1:00:00 2:00:00 ----------------------------------------------------------- Total 2:00:00 Timesheet: sheet2 Day Start End Duration Notes Wed Jun 30, 2021 05:00:00 - 06:00:00 1:00:00 1:00:00 ----------------------------------------------------------- Total 1:00:00 ----------------------------------------------------------- Grand total 3:00:00 "); } #[test] fn entries_are_grouped_despite_database_full() { let args = Args { sheet: Some(Sheet::Full), ..Default::default() }; let mut streams = Streams::fake(b""); let facts = Facts::new(); std::env::set_var("TZ", "CST+6"); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap()), None, "sheet1").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap()), None, "_sheet2").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 13, 0, 0).unwrap()), None, "sheet1").unwrap(); DisplayCommand::handle(args, &mut streams, &facts).unwrap(); assert_eq!(&String::from_utf8_lossy(&streams.out), "\ Timesheet: _sheet2 Day Start End Duration Notes Wed Jun 30, 2021 05:00:00 - 06:00:00 1:00:00 1:00:00 ----------------------------------------------------------- Total 1:00:00 Timesheet: sheet1 Day Start End Duration Notes Wed Jun 30, 2021 04:00:00 - 05:00:00 1:00:00 06:00:00 - 07:00:00 1:00:00 2:00:00 ----------------------------------------------------------- Total 2:00:00 ----------------------------------------------------------- Grand total 3:00:00 "); } #[test] fn filter_old_database() { std::env::set_var("TZ", "CST+6"); let args = Args { format: Some(Formatter::Csv), start: Some(Utc.with_ymd_and_hms(2021, 6, 29, 12, 0, 0).unwrap()), end: Some(Utc.with_ymd_and_hms(2021, 6, 29, 13, 0, 0).unwrap()), ..Default::default() }; let mut streams = Streams::fake(b"").with_db( SqliteDatabase::from_path("assets/test_old_db.db").unwrap() ); let facts = Facts::new(); // item in database: // start: 2021-06-29 06:26:49.580565 // end: 2021-06-29 07:26:52.816747 DisplayCommand::handle(args, &mut streams, &facts).unwrap(); assert_eq!(&String::from_utf8_lossy(&streams.out), "start,end,note,sheet 2021-06-29T12:26:49.580565Z,2021-06-29T13:26:52.816747Z,lets do some rust,default "); assert_eq!( String::from_utf8_lossy(&streams.err), "[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n" ); } #[test] fn respect_default_formatter() { std::env::set_var("TZ", "CST+6"); let args = Default::default(); let mut streams = Streams::fake(b""); let facts = Facts::new().with_config(Config { default_formatter: Formatter::Ids, ..Default::default() }); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap(); DisplayCommand::handle(args, &mut streams, &facts).unwrap(); assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n"); assert_eq!(String::from_utf8_lossy(&streams.err), ""); } #[test] fn respect_command_default_formatter() { std::env::set_var("TZ", "CST+6"); let args = Default::default(); let mut streams = Streams::fake(b""); let facts = Facts::new().with_config(Config { commands: CommandsSettings { display: BaseCommandSettings { default_formatter: Some(Formatter::Ids), }, ..Default::default() }, ..Default::default() }); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap(); streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap(); DisplayCommand::handle(args, &mut streams, &facts).unwrap(); assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n"); assert_eq!(String::from_utf8_lossy(&streams.err), ""); } }