From c19ec08fad35320c49a75db263e9fdf6be300737 Mon Sep 17 00:00:00 2001 From: Bodo Junglas Date: Sun, 2 Feb 2020 20:45:17 +0100 Subject: [PATCH] Layout missing commands --- cli/shell_completions/acari.fish | 31 +++++++++ cli/src/commands/mod.rs | 8 +++ cli/src/commands/projects_of_customer.rs | 2 +- cli/src/commands/set.rs | 14 ++++ cli/src/commands/start.rs | 14 ++++ cli/src/commands/stop.rs | 7 ++ cli/src/commands/tracking.rs | 7 ++ cli/src/config.rs | 4 +- cli/src/main.rs | 88 ++++++++++++++++++++---- lib/src/cached_client.rs | 10 ++- lib/src/lib.rs | 18 +++++ lib/src/query.rs | 2 +- lib/src/std_client.rs | 16 ++++- 13 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 cli/src/commands/set.rs create mode 100644 cli/src/commands/start.rs create mode 100644 cli/src/commands/stop.rs create mode 100644 cli/src/commands/tracking.rs diff --git a/cli/shell_completions/acari.fish b/cli/shell_completions/acari.fish index 5783874..06ffa8c 100644 --- a/cli/shell_completions/acari.fish +++ b/cli/shell_completions/acari.fish @@ -2,6 +2,11 @@ function __fish_is_arg_n --argument-names n test $n -eq (count (string match -v -- '-*' (commandline -poc))) end +function __fish_arg_n --argument-names n + set -l args (string match -v -- '-*' (commandline -poc)) + echo -n $args[(math "$n + 1")] +end + # options complete -c acari -s o -l output -a "flat json pretty" -d "set output format" complete -c acari -s h -l help -d "show help" @@ -12,8 +17,13 @@ complete -f -c acari -n "__fish_use_subcommand" -a init -d "initialize connectio complete -f -c acari -n "__fish_use_subcommand" -a check -d "check connection" complete -f -c acari -n "__fish_use_subcommand" -a clear-cache -d "Clear local cache" complete -f -c acari -n "__fish_use_subcommand" -a customers -d "list customers" +complete -f -c acari -n "__fish_use_subcommand" -a entries -d "list time entries" complete -f -c acari -n "__fish_use_subcommand" -a projects -d "list projects" complete -f -c acari -n "__fish_use_subcommand" -a services -d "list services" +complete -f -c acari -n "__fish_use_subcommand" -a set -d "set time entry" +complete -f -c acari -n "__fish_use_subcommand" -a start -d "start time tracking" +complete -f -c acari -n "__fish_use_subcommand" -a stop -d "stop time tracking" +complete -f -c acari -n "__fish_use_subcommand" -a tracking -d "show current time tracking" # check complete -f -c acari -n "__fish_seen_subcommand_from check" @@ -24,9 +34,30 @@ complete -f -c acari -n "__fish_seen_subcommand_from clear-cache" # customers complete -f -c acari -n "__fish_seen_subcommand_from customers" +# entries +complete -f -c acari -n "__fish_seen_subcommand_from entries" + # projects complete -f -c acari -n "__fish_seen_subcommand_from projects" complete -f -c acari -n "__fish_seen_subcommand_from projects; and __fish_is_arg_n 2" -a "(acari -oflat customers)" # services complete -f -c acari -n "__fish_seen_subcommand_from services" + +# set +complete -f -c acari -n "__fish_seen_subcommand_from set" +complete -f -c acari -n "__fish_seen_subcommand_from set; and __fish_is_arg_n 2" -a "(acari -oflat customers)" +complete -f -c acari -n "__fish_seen_subcommand_from set; and __fish_is_arg_n 3" -a "(acari -oflat projects (__fish_arg_n 2))" +complete -f -c acari -n "__fish_seen_subcommand_from set; and __fish_is_arg_n 4" -a "(acari -oflat services)" + +# start +complete -f -c acari -n "__fish_seen_subcommand_from start" +complete -f -c acari -n "__fish_seen_subcommand_from start; and __fish_is_arg_n 2" -a "(acari -oflat customers)" +complete -f -c acari -n "__fish_seen_subcommand_from start; and __fish_is_arg_n 3" -a "(acari -oflat projects (__fish_arg_n 2))" +complete -f -c acari -n "__fish_seen_subcommand_from start; and __fish_is_arg_n 4" -a "(acari -oflat services)" + +# stop +complete -f -c acari -n "__fish_seen_subcommand_from stop" + +# tracking +complete -f -c acari -n "__fish_seen_subcommand_from tracking" diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index e04e6b9..f4432f2 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -6,6 +6,10 @@ mod entries; mod init; mod projects_of_customer; mod services; +mod set; +mod start; +mod stop; +mod tracking; pub use all_projects::*; pub use check::*; @@ -15,6 +19,10 @@ pub use entries::*; pub use init::*; pub use projects_of_customer::*; pub use services::*; +pub use set::*; +pub use start::*; +pub use stop::*; +pub use tracking::*; use acari_lib::AcariError; diff --git a/cli/src/commands/projects_of_customer.rs b/cli/src/commands/projects_of_customer.rs index 31dc6de..d47ac8f 100644 --- a/cli/src/commands/projects_of_customer.rs +++ b/cli/src/commands/projects_of_customer.rs @@ -2,7 +2,7 @@ use super::OutputFormat; use acari_lib::{AcariError, Client, Project}; use prettytable::{cell, format, row, Table}; -pub fn projects_of_customer(client: &dyn Client, customer_name: &str, output_format: OutputFormat) -> Result<(), AcariError> { +pub fn projects_of_customer(client: &dyn Client, output_format: OutputFormat, customer_name: &str) -> Result<(), AcariError> { let mut projects = client.get_projects()?; projects.retain(|p| p.customer_name == customer_name); diff --git a/cli/src/commands/set.rs b/cli/src/commands/set.rs new file mode 100644 index 0000000..8f5cf03 --- /dev/null +++ b/cli/src/commands/set.rs @@ -0,0 +1,14 @@ +use super::OutputFormat; +use acari_lib::{AcariError, Client, Tracker}; +use prettytable::{cell, format, row, Table}; + +pub fn set( + client: &dyn Client, + output_format: OutputFormat, + customer_name: &str, + project_name: &str, + service_name: &str, + minutes: u32, +) -> Result<(), AcariError> { + Ok(()) +} diff --git a/cli/src/commands/start.rs b/cli/src/commands/start.rs new file mode 100644 index 0000000..8e843d2 --- /dev/null +++ b/cli/src/commands/start.rs @@ -0,0 +1,14 @@ +use super::OutputFormat; +use acari_lib::{AcariError, Client, Tracker}; +use prettytable::{cell, format, row, Table}; + +pub fn start( + client: &dyn Client, + output_format: OutputFormat, + customer_name: &str, + project_name: &str, + service_name: &str, + minutes_offset: u32, +) -> Result<(), AcariError> { + Ok(()) +} diff --git a/cli/src/commands/stop.rs b/cli/src/commands/stop.rs new file mode 100644 index 0000000..4ce5a99 --- /dev/null +++ b/cli/src/commands/stop.rs @@ -0,0 +1,7 @@ +use super::OutputFormat; +use acari_lib::{AcariError, Client, Tracker}; +use prettytable::{cell, format, row, Table}; + +pub fn stop(client: &dyn Client, output_format: OutputFormat) -> Result<(), AcariError> { + Ok(()) +} diff --git a/cli/src/commands/tracking.rs b/cli/src/commands/tracking.rs new file mode 100644 index 0000000..eb86410 --- /dev/null +++ b/cli/src/commands/tracking.rs @@ -0,0 +1,7 @@ +use super::OutputFormat; +use acari_lib::{AcariError, Client, Tracker}; +use prettytable::{cell, format, row, Table}; + +pub fn tracking(client: &dyn Client, output_format: OutputFormat) -> Result<(), AcariError> { + Ok(()) +} diff --git a/cli/src/config.rs b/cli/src/config.rs index ea24dcd..11f34a2 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -1,4 +1,4 @@ -use acari_lib::{AcariError, CachedClient, Client, StdClient}; +use acari_lib::{internal_error, AcariError, CachedClient, Client, StdClient}; use serde::{Deserialize, Serialize}; use std::fs::{self, File}; use std::io::{self, Read, Write}; @@ -46,7 +46,7 @@ impl Config { fs::create_dir_all( &config_file .parent() - .ok_or_else(|| AcariError::InternalError("Invalid config path".to_string()))?, + .ok_or_else(|| internal_error!("Invalid config path: {}", config_file.to_string_lossy()))?, )?; let mut file = File::create(&config_file)?; diff --git a/cli/src/main.rs b/cli/src/main.rs index 036293f..b7ba7e9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,5 @@ -use acari_lib::{AcariError, DateSpan}; -use clap::{App, Arg, SubCommand}; +use acari_lib::{user_error, AcariError, DateSpan}; +use clap::{App, Arg, ArgMatches, SubCommand}; mod commands; mod config; @@ -23,12 +23,6 @@ fn main() -> Result<(), Box> { .subcommand(SubCommand::with_name("check").about("Check connection to mite")) .subcommand(SubCommand::with_name("clear-cache").about("Clear the local cache")) .subcommand(SubCommand::with_name("customers").about("List all customers")) - .subcommand( - SubCommand::with_name("projects") - .arg(Arg::with_name("customer").help("Optional: List only projects of a specific customer")) - .about("List all projects"), - ) - .subcommand(SubCommand::with_name("services").about("List all services")) .subcommand( SubCommand::with_name("entries") .arg( @@ -37,7 +31,32 @@ fn main() -> Result<(), Box> { .help("Date span to query\n(today, yesterday, this-week, last-week,\n this-month, last-month, yyyy-mm-dd, yyyy-mm-dd|yyyy-mm-dd)"), ) .about("Query time entries"), - ); + ) + .subcommand( + SubCommand::with_name("projects") + .arg(Arg::with_name("customer").help("Optional: List only projects of a specific customer")) + .about("List all projects"), + ) + .subcommand(SubCommand::with_name("services").about("List all services")) + .subcommand( + SubCommand::with_name("set") + .arg(Arg::with_name("customer").required(true).help("Customer name")) + .arg(Arg::with_name("project").required(true).help("Project name")) + .arg(Arg::with_name("service").required(true).help("Service name")) + .arg(Arg::with_name("time").required(true).help("Time (minutes or hh:mm)")) + .about("Start tracking time"), + ) + .subcommand( + SubCommand::with_name("start") + .arg(Arg::with_name("customer").required(true).help("Customer name")) + .arg(Arg::with_name("project").required(true).help("Project name")) + .arg(Arg::with_name("service").required(true).help("Service name")) + .arg(Arg::with_name("offset").help("Optional: Starting offset (minutes or hh:mm)")) + .about("Start tracking time"), + ) + .subcommand(SubCommand::with_name("stop").about("Stop current time tracking")) + .subcommand(SubCommand::with_name("tracking").about("Show currently tracked time entry")); + let matches = app.get_matches(); let output_format = matches.value_of("output").map(OutputFormat::from_string).unwrap_or(Ok(OutputFormat::Pretty))?; @@ -50,17 +69,33 @@ fn main() -> Result<(), Box> { ("check", _) => commands::check(client.as_ref(), output_format)?, ("clear-cache", _) => commands::clear_cache()?, ("customers", _) => commands::customers(client.as_ref(), output_format)?, + ("entries", Some(sub_matches)) => { + let span = required_arg(sub_matches, "span")?; + commands::entries(client.as_ref(), output_format, DateSpan::from_string(span)?)?; + } ("projects", Some(sub_matches)) => match sub_matches.value_of("customer") { - Some(customer) => commands::projects_of_customer(client.as_ref(), customer, output_format)?, + Some(customer) => commands::projects_of_customer(client.as_ref(), output_format, customer)?, None => commands::all_projects(client.as_ref(), output_format)?, }, ("services", _) => commands::services(client.as_ref(), output_format)?, - ("entries", Some(sub_matches)) => { - let span_arg = sub_matches - .value_of("span") - .ok_or(AcariError::UserError("Missing argument".to_string()))?; - commands::entries(client.as_ref(), output_format, DateSpan::from_string(span_arg)?)?; + ("set", Some(sub_matches)) => { + let customer = required_arg(sub_matches, "customer")?; + let project = required_arg(sub_matches, "project")?; + let service = required_arg(sub_matches, "service")?; + let time = parse_minutes(required_arg(sub_matches, "time")?)?; + + commands::set(client.as_ref(), output_format, customer, project, service, time)?; } + ("start", Some(sub_matches)) => { + let customer = required_arg(sub_matches, "customer")?; + let project = required_arg(sub_matches, "project")?; + let service = required_arg(sub_matches, "service")?; + let offset = minutes_arg(sub_matches, "offset")?; + + commands::start(client.as_ref(), output_format, customer, project, service, offset.unwrap_or(0))?; + } + ("stop", _) => commands::stop(client.as_ref(), output_format)?, + ("tracking", _) => commands::tracking(client.as_ref(), output_format)?, (invalid, _) => Err(AcariError::UserError(format!("Unknown command: {}", invalid)))?, } } @@ -72,3 +107,26 @@ fn main() -> Result<(), Box> { Ok(()) } + +fn required_arg<'a>(matches: &'a ArgMatches, name: &str) -> Result<&'a str, AcariError> { + matches.value_of(name).ok_or_else(|| user_error!("Missing <{}> argument", name)) +} + +fn minutes_arg(matches: &ArgMatches, name: &str) -> Result, AcariError> { + match matches.value_of(name) { + Some(value) => parse_minutes(value).map(Some), + None => Ok(None), + } +} + +fn parse_minutes(expr: &str) -> Result { + match expr.find(":") { + Some(idx) => { + let hours = expr[..idx].parse::().map_err(|e| user_error!("Invalid time format: {}", e))?; + let minutes = expr[idx + 1..].parse::().map_err(|e| user_error!("Invalid time format: {}", e))?; + + Ok(hours * 60 + minutes) + } + None => expr.parse::().map_err(|e| user_error!("Invalid time format: {}", e)), + } +} diff --git a/lib/src/cached_client.rs b/lib/src/cached_client.rs index a99c902..7541b6d 100644 --- a/lib/src/cached_client.rs +++ b/lib/src/cached_client.rs @@ -1,5 +1,5 @@ use crate::error::AcariError; -use crate::model::{Account, Customer, Project, Service, TimeEntry, User}; +use crate::model::{Account, Customer, Project, Service, TimeEntry, Tracker, User}; use crate::query::DateSpan; use crate::std_client::StdClient; use crate::Client; @@ -81,9 +81,17 @@ impl Client for CachedClient { self.cache_data("services.json", || self.client.get_services()) } + fn get_time_entry(&self, entry_id: u32) -> Result { + self.client.get_time_entry(entry_id) // This should not be cached + } + fn get_time_entries(&self, date_span: DateSpan) -> Result, AcariError> { self.client.get_time_entries(date_span) // This should not be cached } + + fn get_tracker(&self) -> Result { + self.client.get_tracker() // This should not be cached + } } fn file_age(path: &PathBuf) -> Result, AcariError> { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index aa50aea..d64e7ae 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -21,5 +21,23 @@ pub trait Client { fn get_services(&self) -> Result, AcariError>; + fn get_time_entry(&self, entry_id: u32) -> Result; + fn get_time_entries(&self, date_span: DateSpan) -> Result, AcariError>; + + fn get_tracker(&self) -> Result; +} + +#[macro_export] +macro_rules! user_error { + ( $( $arg:expr ),* ) => { + AcariError::UserError(format!($($arg),*)) + } +} + +#[macro_export] +macro_rules! internal_error { + ( $( $arg:expr ),* ) => { + AcariError::InternalError(format!($($arg),*)) + } } diff --git a/lib/src/query.rs b/lib/src/query.rs index ac117f4..bb9764e 100644 --- a/lib/src/query.rs +++ b/lib/src/query.rs @@ -13,7 +13,7 @@ impl Day { match day.to_lowercase().as_str() { "today" | "now" => Ok(Day::Today), "yesterday" => Ok(Day::Yesterday), - date => Ok(Day::Date(NaiveDate::parse_from_str(date, "%Y-%m-%d")?)) + date => Ok(Day::Date(NaiveDate::parse_from_str(date, "%Y-%m-%d")?)), } } diff --git a/lib/src/std_client.rs b/lib/src/std_client.rs index 31e8208..e3a2146 100644 --- a/lib/src/std_client.rs +++ b/lib/src/std_client.rs @@ -1,5 +1,5 @@ use crate::error::AcariError; -use crate::model::{Account, Customer, MiteEntity, Project, Service, TimeEntry, User}; +use crate::model::{Account, Customer, MiteEntity, Project, Service, TimeEntry, Tracker, User}; use crate::query::DateSpan; use crate::Client; use serde::de::DeserializeOwned; @@ -91,6 +91,13 @@ impl Client for StdClient { ) } + fn get_time_entry(&self, entry_id: u32) -> Result { + match self.get(&format!("/time_entries/{}.json", entry_id))? { + MiteEntity::TimeEntry(time_entry) => Ok(time_entry), + response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))), + } + } + fn get_time_entries(&self, date_span: DateSpan) -> Result, AcariError> { Ok( self @@ -103,6 +110,13 @@ impl Client for StdClient { .collect(), ) } + + fn get_tracker(&self) -> Result { + match self.get("/tracker.json")? { + MiteEntity::Tracker(tracker) => Ok(tracker), + response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))), + } + } } fn handle_response(response: blocking::Response) -> Result {