From 5d9ee1a9c9accd0a3a9bd4fcb637c6865d04bb83 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Tue, 29 Dec 2015 21:00:14 +0700 Subject: [PATCH 01/16] wip(commands): commands moved into their own files --- TODO.md | 17 + src/cli/mod.rs | 1 + src/cli/parse.rs | 129 +++++++ src/command/do_nothing.rs | 4 + src/command/initialise.rs | 98 +++++ src/command/list_accounts.rs | 49 +++ src/command/list_balances.rs | 51 +++ src/command/list_counterparties.rs | 40 ++ src/command/list_incomings.rs | 23 ++ src/command/list_outgoings.rs | 23 ++ src/command/list_transactions.rs | 48 +++ src/command/mod.rs | 103 +++++ src/command/show_balance.rs | 22 ++ src/command/show_incoming.rs | 22 ++ src/command/show_outgoing.rs | 22 ++ src/command/show_usage.rs | 4 + src/config/mod.rs | 12 + src/{inquirer.rs => inquirer/ask.rs} | 1 - src/inquirer/mod.rs | 3 + src/main.rs | 548 +-------------------------- 20 files changed, 683 insertions(+), 537 deletions(-) create mode 100644 TODO.md create mode 100644 src/cli/mod.rs create mode 100644 src/cli/parse.rs create mode 100644 src/command/do_nothing.rs create mode 100644 src/command/initialise.rs create mode 100644 src/command/list_accounts.rs create mode 100644 src/command/list_balances.rs create mode 100644 src/command/list_counterparties.rs create mode 100644 src/command/list_incomings.rs create mode 100644 src/command/list_outgoings.rs create mode 100644 src/command/list_transactions.rs create mode 100644 src/command/mod.rs create mode 100644 src/command/show_balance.rs create mode 100644 src/command/show_incoming.rs create mode 100644 src/command/show_outgoing.rs create mode 100644 src/command/show_usage.rs rename src/{inquirer.rs => inquirer/ask.rs} (99%) create mode 100644 src/inquirer/mod.rs diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5546c7c --- /dev/null +++ b/TODO.md @@ -0,0 +1,17 @@ +Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). + +- [ ] Refactor `'client'`: + - [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. + - [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). +- [x] Refactor `'inquirer'`: + - [x] Into a module directory. +- [x] Refactor `'main'`: + - [x] `struct`s and `impl Decodable`s should remain where they are as are command definition related. + - [x] `pick_command` should be simpler: + - [x] Only destructure args to get out what you need to match. 'Additional destructuring](http://rustbyexample.com/flow_control/match/destructuring/destructure_structures.html) should happen before the underlying command is called. + - [ ] Only `get_config` once, if it is successful then pass the `&config` onto the commands, otherwise `configure_cli`. `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. + - [ ] `get_config` should belong to the `'config'` module. + - [ ] `init_config` should become `ask_questions_for_config`. Behind the scenes it should use `inquirer::ask_questions` and some kind of `find_answers*` function to get the answers out for the config. + - [x] Commands should be moved into a module `'command'`. This includes `configure_cli`, `list_accounts`, `show_balance`, `list_transactions`, etc. + - [ ] Table writing and other response writing should live in `'represent'`. This includes `get_account_alias_for_id`, `represent_list_accounts`, `represent_show_balance`, `represent_list_transactions`, `represent_list_amounts`, `represent_list_balances`, `represent_list_outgoings`, `represent_list_incomings`, etc. +- [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, early returns, `let expected_value = match thing { ... }`. diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..ea86848 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1 @@ +pub mod parse; diff --git a/src/cli/parse.rs b/src/cli/parse.rs new file mode 100644 index 0000000..77367ab --- /dev/null +++ b/src/cli/parse.rs @@ -0,0 +1,129 @@ +use client::{Interval, Timeframe}; +use rustc_serialize::{Decodable, Decoder}; + +#[derive(Debug, RustcDecodable)] +pub struct CliArgs { + cmd_init: bool, + cmd_list: bool, + cmd_show: bool, + cmd_accounts: bool, + cmd_transactions: bool, + cmd_counterparties: bool, + cmd_balances: bool, + cmd_outgoings: bool, + cmd_incomings: bool, + cmd_balance: bool, + cmd_outgoing: bool, + cmd_incoming: bool, + pub arg_account: AccountType, + pub flag_interval: Interval, + pub flag_timeframe: Timeframe, + pub flag_count: i64, + pub flag_show_description: bool, + pub flag_hide_currency: bool, + pub flag_output: OutputFormat, + flag_help: bool, + flag_version: bool, +} + +#[derive(Debug)] +pub enum AccountType { + Current, + Savings, + Business, + Unknown(String), + None +} + +impl Decodable for AccountType { + fn decode(d: &mut D) -> Result { + let s = try!(d.read_str()); + let default_acccount_type = AccountType::None; + Ok(match &*s { + "" => default_acccount_type, + "current" => AccountType::Current, + "savings" => AccountType::Savings, + "business" => AccountType::Business, + s => AccountType::Unknown(s.to_string()), + }) + } +} + +impl Decodable for Interval { + fn decode(d: &mut D) -> Result { + let s = try!(d.read_str()); + let default_interval = Interval::Monthly; + Ok(match &*s { + "" => default_interval, + "monthly" => Interval::Monthly, + _ => { + error!("teller-cli currently only suports an interval of monthly"); + default_interval + }, + }) + } +} + +impl Decodable for Timeframe { + fn decode(d: &mut D) -> Result { + let s = try!(d.read_str()); + let default_timeframe = Timeframe::SixMonths; + Ok(match &*s { + "year" => Timeframe::Year, + "6-months" => Timeframe::SixMonths, + "3-months" => Timeframe::ThreeMonths, + _ => default_timeframe, + }) + } +} + +#[derive(Debug)] +pub enum OutputFormat { + Spark, + Standard, +} + +impl Decodable for OutputFormat { + fn decode(d: &mut D) -> Result { + let s = try!(d.read_str()); + let default_output_format = OutputFormat::Standard; + Ok(match &*s { + "spark" => OutputFormat::Spark, + "standard" => OutputFormat::Standard, + _ => default_output_format, + }) + } +} + +#[derive(Debug)] +pub enum CommandType { + ShowUsage, + Initialise, + ListAccounts, + ShowBalance, + ShowOutgoing, + ShowIncoming, + ListTransactions, + ListCounterparties, + ListBalances, + ListOutgoings, + ListIncomings, + None, +} + +pub fn get_command_type(arguments: &CliArgs) -> CommandType { + match *arguments { + CliArgs { cmd_init, .. } if cmd_init == true => CommandType::Initialise, + CliArgs { cmd_accounts, .. } if cmd_accounts == true => CommandType::ListAccounts, + CliArgs { cmd_balance, .. } if cmd_balance == true => CommandType::ShowBalance, + CliArgs { cmd_outgoing, .. } if cmd_outgoing == true => CommandType::ShowOutgoing, + CliArgs { cmd_incoming, .. } if cmd_incoming == true => CommandType::ShowIncoming, + CliArgs { cmd_transactions, .. } if cmd_transactions == true => CommandType::ListTransactions, + CliArgs { cmd_counterparties, .. } if cmd_counterparties == true => CommandType::ListCounterparties, + CliArgs { cmd_balances, .. } if cmd_balances == true => CommandType::ListBalances, + CliArgs { cmd_incomings, .. } if cmd_incomings == true => CommandType::ListIncomings, + CliArgs { cmd_outgoings, .. } if cmd_outgoings == true => CommandType::ListOutgoings, + CliArgs { flag_help, flag_version, .. } if flag_help == true || flag_version == true => CommandType::None, + _ => CommandType::ShowUsage, + } +} diff --git a/src/command/do_nothing.rs b/src/command/do_nothing.rs new file mode 100644 index 0000000..5fe972b --- /dev/null +++ b/src/command/do_nothing.rs @@ -0,0 +1,4 @@ +pub fn do_nothing_command() -> i32 { + debug!("--help or --version were passed so we do nothing..."); + 0 +} diff --git a/src/command/initialise.rs b/src/command/initialise.rs new file mode 100644 index 0000000..2693fd2 --- /dev/null +++ b/src/command/initialise.rs @@ -0,0 +1,98 @@ +use std::path::PathBuf; +use config::{Config, get_config_path, get_config_file_to_write, write_config}; +use inquirer::{Question, Answer, ask_question}; + +use client::get_accounts; +use super::list_accounts::represent_list_accounts; + +pub fn configure_cli(config_file_path: &PathBuf) -> Option { + match ask_questions_for_config() { + None => None, + Some(config) => { + match get_config_file_to_write(&config_file_path) { + Ok(mut config_file) => { + let _ = write_config(&mut config_file, &config); + Some(config) + }, + Err(e) => panic!("ERROR: opening file to write: {}", e), + } + }, + } +} + +fn ask_questions_for_config() -> Option { + let get_auth_token_question = Question::new( + "auth_token", + "What is your `auth_token` on teller.io?", + ); + + let auth_token_answer = ask_question(&get_auth_token_question); + + let mut config = Config::new_with_auth_token_only(auth_token_answer.value); + + print!("\n"); + let accounts = match get_accounts(&config) { + Ok(accounts) => accounts, + Err(e) => panic!("Unable to list accounts: {}", e), + }; + represent_list_accounts(&accounts, &config); + + println!("Please type the row (e.g. 3) of the account you wish to place against an alias and press to set this in the config. Leave empty if irrelevant."); + print!("\n"); + + let questions = vec![ + Question::new( + "current", + "Which is your current account?", + ), + Question::new( + "savings", + "Which is your savings account?", + ), + Question::new( + "business", + "Which is your business account?", + ), + ]; + + let answers: Vec = questions.iter().map(ask_question).collect(); + let non_empty_answers: Vec<&Answer> = answers.iter().filter(|&answer| !answer.value.is_empty()).collect(); + let mut fa_iter = non_empty_answers.iter(); + + match fa_iter.find(|&answer| answer.name == "current") { + None => (), + Some(answer) => { + let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); + config.current = accounts[(row_number - 1) as usize].id.to_owned() + }, + }; + match fa_iter.find(|&answer| answer.name == "savings") { + None => (), + Some(answer) => { + let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); + config.savings = accounts[(row_number - 1) as usize].id.to_owned() + } + }; + match fa_iter.find(|&answer| answer.name == "business") { + None => (), + Some(answer) => { + let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); + config.business = accounts[(row_number - 1) as usize].id.to_owned() + } + }; + + if config.auth_token.is_empty() { + error!("`auth_token` was invalid so a config could not be created"); + None + } else { + Some(config) + } +} + +pub fn initialise_command() -> i32 { + let config_file_path = get_config_path(); + println!("To create the config ({}) we need to find out your `auth_token` and assign aliases to some common bank accounts.", config_file_path.display()); + print!("\n"); + configure_cli(&config_file_path); + 0 +} diff --git a/src/command/list_accounts.rs b/src/command/list_accounts.rs new file mode 100644 index 0000000..a45d5a0 --- /dev/null +++ b/src/command/list_accounts.rs @@ -0,0 +1,49 @@ +use config::Config; +use client::{Account, get_accounts}; + +use std::io::Write; +use tabwriter::TabWriter; + +fn get_account_alias_for_id<'a>(account_id: &str, config: &Config) -> &'a str { + if *account_id == config.current { + "(current)" + } else if *account_id == config.savings { + "(savings)" + } else if *account_id == config.business { + "(business)" + } else { + "" + } +} + +pub fn represent_list_accounts(accounts: &Vec, config: &Config) { + let mut accounts_table = String::new(); + accounts_table.push_str("row\taccount no.\tbalance\n"); + for (idx, account) in accounts.iter().enumerate() { + let row_number = (idx + 1) as u32; + let account_alias = get_account_alias_for_id(&account.id, &config); + let new_account_row = format!("{} {}\t****{}\t{}\t{}\n", row_number, account_alias, account.account_number_last_4, account.balance, account.currency); + accounts_table = accounts_table + &new_account_row; + } + + let mut tw = TabWriter::new(Vec::new()); + write!(&mut tw, "{}", accounts_table).unwrap(); + tw.flush().unwrap(); + + let accounts_str = String::from_utf8(tw.unwrap()).unwrap(); + + println!("{}", accounts_str) +} + +pub fn list_accounts_command(config: &Config) -> i32 { + match get_accounts(&config) { + Ok(accounts) => { + represent_list_accounts(&accounts, &config); + 0 + }, + Err(e) => { + error!("Unable to list accounts: {}", e); + 1 + }, + } +} diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs new file mode 100644 index 0000000..077ec42 --- /dev/null +++ b/src/command/list_balances.rs @@ -0,0 +1,51 @@ +use config::{Config, get_account_id}; +use client::{HistoricalAmountsWithCurrency, Balances, Interval, Timeframe, get_balances}; +use cli::parse::{AccountType, OutputFormat}; + +use std::io::Write; +use tabwriter::TabWriter; + +pub fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurrency, output: &OutputFormat) { + match *output { + OutputFormat::Spark => { + let balance_str = hac.historical_amounts.iter().map(|b| b.1.to_owned()).collect::>().join(" "); + println!("{}", balance_str) + }, + OutputFormat::Standard => { + let mut hac_table = String::new(); + let month_cols = hac.historical_amounts.iter().map(|historical_amount| historical_amount.0.to_owned()).collect::>().join("\t"); + hac_table.push_str(&format!("\t{}\n", month_cols)); + hac_table.push_str(&format!("{} ({})", amount_type, hac.currency)); + for historical_amount in hac.historical_amounts.iter() { + let new_amount = format!("\t{}", historical_amount.1); + hac_table = hac_table + &new_amount; + } + + let mut tw = TabWriter::new(Vec::new()); + write!(&mut tw, "{}", hac_table).unwrap(); + tw.flush().unwrap(); + + let hac_str = String::from_utf8(tw.unwrap()).unwrap(); + + println!("{}", hac_str) + }, + } +} + +fn represent_list_balances(hac: &Balances, output: &OutputFormat) { + represent_list_amounts("balance", &hac, &output) +} + +pub fn list_balances_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { + let account_id = get_account_id(&config, &account); + match get_balances(&config, &account_id, &interval, &timeframe) { + Ok(balances) => { + represent_list_balances(&balances, &output); + 0 + }, + Err(e) => { + error!("Unable to list balances: {}", e); + 1 + }, + } +} diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs new file mode 100644 index 0000000..ecb20a9 --- /dev/null +++ b/src/command/list_counterparties.rs @@ -0,0 +1,40 @@ +use config::{Config, get_account_id}; +use client::{Timeframe, get_counterparties}; +use cli::parse::AccountType; + +use std::io::Write; +use tabwriter::TabWriter; + +fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currency: &str, count: &i64) { + let mut counterparties_table = String::new(); + + counterparties_table.push_str(&format!("row\tcounterparty\tamount ({})\n", currency)); + let skip_n = counterparties.len() - (*count as usize); + for (idx, counterparty) in counterparties.iter().skip(skip_n).enumerate() { + let row_number = (idx + 1) as u32; + let new_counterparty_row = format!("{}\t{}\t{}\n", row_number, counterparty.0, counterparty.1); + counterparties_table = counterparties_table + &new_counterparty_row; + } + + let mut tw = TabWriter::new(Vec::new()); + write!(&mut tw, "{}", counterparties_table).unwrap(); + tw.flush().unwrap(); + + let counterparties_str = String::from_utf8(tw.unwrap()).unwrap(); + + println!("{}", counterparties_str) +} + +pub fn list_counterparties_command(config: &Config, account: &AccountType, timeframe: &Timeframe, count: &i64) -> i32 { + let account_id = get_account_id(&config, &account); + match get_counterparties(&config, &account_id, &timeframe) { + Ok(counterparties_with_currency) => { + represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, &count); + 0 + }, + Err(e) => { + error!("Unable to list counterparties: {}", e); + 1 + }, + } +} diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs new file mode 100644 index 0000000..4887f67 --- /dev/null +++ b/src/command/list_incomings.rs @@ -0,0 +1,23 @@ +use config::{Config, get_account_id}; +use client::{Incomings, Interval, Timeframe, get_incomings}; +use cli::parse::{AccountType, OutputFormat}; + +use super::list_balances::represent_list_amounts; + +fn represent_list_incomings(hac: &Incomings, output: &OutputFormat) { + represent_list_amounts("incoming", &hac, &output) +} + +pub fn list_incomings_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { + let account_id = get_account_id(&config, &account); + match get_incomings(&config, &account_id, &interval, &timeframe) { + Ok(incomings) => { + represent_list_incomings(&incomings, &output); + 0 + }, + Err(e) => { + error!("Unable to list incomings: {}", e); + 1 + }, + } +} diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs new file mode 100644 index 0000000..7841182 --- /dev/null +++ b/src/command/list_outgoings.rs @@ -0,0 +1,23 @@ +use config::{Config, get_account_id}; +use client::{Outgoings, Interval, Timeframe, get_outgoings}; +use cli::parse::{AccountType, OutputFormat}; + +use super::list_balances::represent_list_amounts; + +fn represent_list_outgoings(hac: &Outgoings, output: &OutputFormat) { + represent_list_amounts("outgoing", &hac, &output) +} + +pub fn list_outgoings_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { + let account_id = get_account_id(&config, &account); + match get_outgoings(&config, &account_id, &interval, &timeframe) { + Ok(outgoings) => { + represent_list_outgoings(&outgoings, &output); + 0 + }, + Err(e) => { + error!("Unable to list ougoings: {}", e); + 1 + }, + } +} diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs new file mode 100644 index 0000000..fab1006 --- /dev/null +++ b/src/command/list_transactions.rs @@ -0,0 +1,48 @@ +use config::{Config, get_account_id}; +use client::{Transaction, Timeframe, get_transactions_with_currency}; +use cli::parse::AccountType; + +use std::io::Write; +use tabwriter::TabWriter; + +fn represent_list_transactions(transactions: &Vec, currency: &str, show_description: &bool) { + let mut transactions_table = String::new(); + + if *show_description { + transactions_table.push_str(&format!("row\tdate\tcounterparty\tamount ({})\tdescription\n", currency)); + for (idx, transaction) in transactions.iter().enumerate() { + let row_number = (idx + 1) as u32; + let new_transaction_row = format!("{}\t{}\t{}\t{}\t{}\n", row_number, transaction.date, transaction.counterparty, transaction.amount, transaction.description); + transactions_table = transactions_table + &new_transaction_row; + } + } else { + transactions_table.push_str(&format!("row\tdate\tcounterparty\tamount ({})\n", currency)); + for (idx, transaction) in transactions.iter().enumerate() { + let row_number = (idx + 1) as u32; + let new_transaction_row = format!("{}\t{}\t{}\t{}\n", row_number, transaction.date, transaction.counterparty, transaction.amount); + transactions_table = transactions_table + &new_transaction_row; + } + } + + let mut tw = TabWriter::new(Vec::new()); + write!(&mut tw, "{}", transactions_table).unwrap(); + tw.flush().unwrap(); + + let transactions_str = String::from_utf8(tw.unwrap()).unwrap(); + + println!("{}", transactions_str) +} + +pub fn list_transactions_command(config: &Config, account: &AccountType, timeframe: &Timeframe, show_description: &bool) -> i32 { + let account_id = get_account_id(&config, &account); + match get_transactions_with_currency(&config, &account_id, &timeframe) { + Ok(transactions_with_currency) => { + represent_list_transactions(&transactions_with_currency.transactions, &transactions_with_currency.currency, &show_description); + 0 + }, + Err(e) => { + error!("Unable to list transactions: {}", e); + 1 + }, + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..8e475b9 --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1,103 @@ +mod do_nothing; +mod show_usage; +mod initialise; +mod list_accounts; +mod show_balance; +mod show_outgoing; +mod show_incoming; +mod list_transactions; +mod list_counterparties; +mod list_balances; +mod list_outgoings; +mod list_incomings; + +use cli::parse::{CommandType, CliArgs}; +use config::{Config, get_config_path, get_config_file, read_config}; +use self::initialise::configure_cli; + +use self::do_nothing::do_nothing_command; +use self::show_usage::show_usage_command; +use self::initialise::initialise_command; +use self::list_accounts::list_accounts_command; +use self::show_balance::show_balance_command; +use self::show_outgoing::show_outgoing_command; +use self::show_incoming::show_incoming_command; +use self::list_transactions::list_transactions_command; +use self::list_counterparties::list_counterparties_command; +use self::list_balances::list_balances_command; +use self::list_outgoings::list_outgoings_command; +use self::list_incomings::list_incomings_command; + +fn get_config() -> Option { + let config_file_path = get_config_path(); + match get_config_file(&config_file_path) { + None => { + println!("A config file could not be found at: {}", config_file_path.display()); + println!("You will need to set the `auth_token` and give aliases to your bank accounts"); + print!("\n"); + configure_cli(&config_file_path) + }, + Some(mut config_file) => { + match read_config(&mut config_file) { + Ok(config) => Some(config), + Err(e) => panic!("ERROR: attempting to read file {}: {}", config_file_path.display(), e), + } + }, + } +} + +pub fn execute(command_type: &CommandType, arguments: &CliArgs) -> i32 { + match *command_type { + CommandType::None => do_nothing_command(), + CommandType::ShowUsage => show_usage_command(), + CommandType::Initialise => initialise_command(), + _ => { + match get_config() { + None => { + error!("Configuration could not be found or created so command not executed"); + 1 + }, + Some(config) => { + match *command_type { + CommandType::ListAccounts => { + list_accounts_command(&config) + }, + CommandType::ShowBalance => { + let CliArgs { ref arg_account, flag_hide_currency, .. } = *arguments; + show_balance_command(&config, &arg_account, &flag_hide_currency) + }, + CommandType::ShowOutgoing => { + let CliArgs { ref arg_account, flag_hide_currency, .. } = *arguments; + show_outgoing_command(&config, &arg_account, &flag_hide_currency) + }, + CommandType::ShowIncoming => { + let CliArgs { ref arg_account, flag_hide_currency, .. } = *arguments; + show_incoming_command(&config, &arg_account, &flag_hide_currency) + }, + CommandType::ListTransactions => { + let CliArgs { ref arg_account, flag_show_description, ref flag_timeframe, .. } = *arguments; + list_transactions_command(&config, &arg_account, &flag_timeframe, &flag_show_description) + }, + CommandType::ListCounterparties => { + let CliArgs { ref arg_account, ref flag_timeframe, flag_count, .. } = *arguments; + list_counterparties_command(&config, &arg_account, &flag_timeframe, &flag_count) + }, + CommandType::ListBalances => { + let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; + list_balances_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) + }, + CommandType::ListOutgoings => { + let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; + list_outgoings_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) + }, + CommandType::ListIncomings => { + let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; + list_incomings_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) + }, + _ => panic!("TODO: This shouldn't be accessible"), + } + }, + } + }, + } +} diff --git a/src/command/show_balance.rs b/src/command/show_balance.rs new file mode 100644 index 0000000..7967511 --- /dev/null +++ b/src/command/show_balance.rs @@ -0,0 +1,22 @@ +use client::get_account_balance; +use config::{Config, get_account_id}; +use cli::parse::AccountType; +use client::Money; + +fn represent_money(money_with_currency: &Money, hide_currency: &bool) { + println!("{}", money_with_currency.get_balance_for_display(&hide_currency)) +} + +pub fn show_balance_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { + let account_id = get_account_id(&config, &account); + match get_account_balance(&config, &account_id) { + Ok(balance) => { + represent_money(&balance, &hide_currency); + 0 + }, + Err(e) => { + error!("Unable to get account balance: {}", e); + 1 + }, + } +} diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs new file mode 100644 index 0000000..b757d9c --- /dev/null +++ b/src/command/show_incoming.rs @@ -0,0 +1,22 @@ +use client::get_incoming; +use config::{Config, get_account_id}; +use cli::parse::AccountType; +use client::Money; + +fn represent_money(money_with_currency: &Money, hide_currency: &bool) { + println!("{}", money_with_currency.get_balance_for_display(&hide_currency)) +} + +pub fn show_incoming_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { + let account_id = get_account_id(&config, &account); + match get_incoming(&config, &account_id) { + Ok(incoming) => { + represent_money(&incoming, &hide_currency); + 0 + }, + Err(e) => { + error!("Unable to get incoming: {}", e); + 1 + }, + } +} diff --git a/src/command/show_outgoing.rs b/src/command/show_outgoing.rs new file mode 100644 index 0000000..2c4c7ff --- /dev/null +++ b/src/command/show_outgoing.rs @@ -0,0 +1,22 @@ +use client::get_outgoing; +use config::{Config, get_account_id}; +use cli::parse::AccountType; +use client::Money; + +fn represent_money(money_with_currency: &Money, hide_currency: &bool) { + println!("{}", money_with_currency.get_balance_for_display(&hide_currency)) +} + +pub fn show_outgoing_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { + let account_id = get_account_id(&config, &account); + match get_outgoing(&config, &account_id) { + Ok(outgoing) => { + represent_money(&outgoing, &hide_currency); + 0 + }, + Err(e) => { + error!("Unable to get outgoing: {}", e); + 1 + }, + } +} diff --git a/src/command/show_usage.rs b/src/command/show_usage.rs new file mode 100644 index 0000000..0fc97c2 --- /dev/null +++ b/src/command/show_usage.rs @@ -0,0 +1,4 @@ +pub fn show_usage_command() -> i32 { + println!("TODO: show usage should have expected behaviour"); + 0 +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 5109580..37b3394 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -11,6 +11,8 @@ use std::io::prelude::*; // Required for read_to_string use later. use self::error::ConfigError; +use cli::parse::AccountType; + #[derive(Debug, RustcEncodable, RustcDecodable)] pub struct Config { pub auth_token: String, @@ -87,3 +89,13 @@ pub fn write_config(config_file: &mut File, config: &Config) -> Result<(), Confi Ok(()) } + +pub fn get_account_id(config: &Config, account: &AccountType) -> String{ + let default_account_id = config.current.to_owned(); + match *account { + AccountType::Current => config.current.to_owned(), + AccountType::Savings => config.savings.to_owned(), + AccountType::Business => config.business.to_owned(), + _ => default_account_id, + } +} diff --git a/src/inquirer.rs b/src/inquirer/ask.rs similarity index 99% rename from src/inquirer.rs rename to src/inquirer/ask.rs index 3f69f8a..3f9ae13 100644 --- a/src/inquirer.rs +++ b/src/inquirer/ask.rs @@ -34,7 +34,6 @@ impl Answer { } } - pub fn ask_question(question: &Question) -> Answer { let question_name = question.name.to_owned(); println!("{}", question.message); diff --git a/src/inquirer/mod.rs b/src/inquirer/mod.rs new file mode 100644 index 0000000..ea45648 --- /dev/null +++ b/src/inquirer/mod.rs @@ -0,0 +1,3 @@ +pub mod ask; + +pub use self::ask::{Question, Answer, ask_question}; diff --git a/src/main.rs b/src/main.rs index c9ed328..7293a95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,25 +9,18 @@ extern crate hyper; extern crate tabwriter; extern crate itertools; +mod cli; +mod command; mod config; mod client; mod inquirer; -use client::{Account, Transaction, Money, HistoricalAmountsWithCurrency, Balances, Outgoings, Incomings, get_accounts, get_account_balance, get_transactions_with_currency, get_counterparties, get_balances, get_outgoings, get_incomings, get_outgoing, get_incoming}; -use client::{Interval, Timeframe}; - -use std::path::PathBuf; -use config::{Config, get_config_path, get_config_file, read_config, get_config_file_to_write, write_config}; - -use inquirer::{Question, Answer, ask_question}; - use docopt::Docopt; -use rustc_serialize::{Decodable, Decoder}; - -use std::io::Write; -use tabwriter::TabWriter; -use std::process::exit; +use cli::parse::get_command_type; +use command::execute; +use std::process; +// const NAME: &'static str = "teller"; // TODO: Can this be placed into the usage automatically? const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); const USAGE: &'static str = "Banking for the command line. @@ -67,536 +60,19 @@ Options: -o --output= Output in a particular format (e.g. spark). "; -#[derive(Debug, RustcDecodable)] -struct Args { - cmd_init: bool, - cmd_list: bool, - cmd_show: bool, - cmd_accounts: bool, - cmd_transactions: bool, - cmd_counterparties: bool, - cmd_balances: bool, - cmd_outgoings: bool, - cmd_incomings: bool, - cmd_balance: bool, - cmd_outgoing: bool, - cmd_incoming: bool, - arg_account: AccountType, - flag_interval: Interval, - flag_timeframe: Timeframe, - flag_count: i64, - flag_show_description: bool, - flag_hide_currency: bool, - flag_output: OutputFormat, - flag_help: bool, - flag_version: bool, -} - -#[derive(Debug)] -enum AccountType { - Current, - Savings, - Business, - Unknown(String), - None -} - -impl Decodable for AccountType { - fn decode(d: &mut D) -> Result { - let s = try!(d.read_str()); - let default_acccount_type = AccountType::None; - Ok(match &*s { - "" => default_acccount_type, - "current" => AccountType::Current, - "savings" => AccountType::Savings, - "business" => AccountType::Business, - s => AccountType::Unknown(s.to_string()), - }) - } -} - -impl Decodable for Interval { - fn decode(d: &mut D) -> Result { - let s = try!(d.read_str()); - let default_interval = Interval::Monthly; - Ok(match &*s { - "" => default_interval, - "monthly" => Interval::Monthly, - _ => { - error!("teller-cli currently only suports an interval of monthly"); - default_interval - }, - }) - } -} - -impl Decodable for Timeframe { - fn decode(d: &mut D) -> Result { - let s = try!(d.read_str()); - let default_timeframe = Timeframe::SixMonths; - Ok(match &*s { - "year" => Timeframe::Year, - "6-months" => Timeframe::SixMonths, - "3-months" => Timeframe::ThreeMonths, - _ => default_timeframe, - }) - } -} - -#[derive(Debug)] -enum OutputFormat { - Spark, - Standard, -} - -impl Decodable for OutputFormat { - fn decode(d: &mut D) -> Result { - let s = try!(d.read_str()); - let default_output_format = OutputFormat::Standard; - Ok(match &*s { - "spark" => OutputFormat::Spark, - "standard" => OutputFormat::Standard, - _ => default_output_format, - }) - } -} - -fn get_config() -> Option { - let config_file_path = get_config_path(); - match get_config_file(&config_file_path) { - None => { - println!("A config file could not be found at: {}", config_file_path.display()); - println!("You will need to set the `auth_token` and give aliases to your bank accounts"); - print!("\n"); - configure_cli(&config_file_path) - }, - Some(mut config_file) => { - match read_config(&mut config_file) { - Ok(config) => Some(config), - Err(e) => panic!("ERROR: attempting to read file {}: {}", config_file_path.display(), e), - } - }, - } -} - -fn configure_cli(config_file_path: &PathBuf) -> Option { - match init_config() { - None => None, - Some(config) => { - match get_config_file_to_write(&config_file_path) { - Ok(mut config_file) => { - let _ = write_config(&mut config_file, &config); - Some(config) - }, - Err(e) => panic!("ERROR: opening file to write: {}", e), - } - }, - } -} - -fn init_config() -> Option { - let get_auth_token_question = Question::new( - "auth_token", - "What is your `auth_token` on teller.io?", - ); - - let auth_token_answer = ask_question(&get_auth_token_question); - - let mut config = Config::new_with_auth_token_only(auth_token_answer.value); - - print!("\n"); - let accounts = match get_accounts(&config) { - Ok(accounts) => accounts, - Err(e) => panic!("Unable to list accounts: {}", e), - }; - represent_list_accounts(&accounts, &config); - - println!("Please type the row (e.g. 3) of the account you wish to place against an alias and press to set this in the config. Leave empty if irrelevant."); - print!("\n"); - - let questions = vec![ - Question::new( - "current", - "Which is your current account?", - ), - Question::new( - "savings", - "Which is your savings account?", - ), - Question::new( - "business", - "Which is your business account?", - ), - ]; - - let answers: Vec = questions.iter().map(ask_question).collect(); - let non_empty_answers: Vec<&Answer> = answers.iter().filter(|&answer| !answer.value.is_empty()).collect(); - let mut fa_iter = non_empty_answers.iter(); - - match fa_iter.find(|&answer| answer.name == "current") { - None => (), - Some(answer) => { - let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); - config.current = accounts[(row_number - 1) as usize].id.to_owned() - }, - }; - match fa_iter.find(|&answer| answer.name == "savings") { - None => (), - Some(answer) => { - let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); - config.savings = accounts[(row_number - 1) as usize].id.to_owned() - } - }; - match fa_iter.find(|&answer| answer.name == "business") { - None => (), - Some(answer) => { - let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); - config.business = accounts[(row_number - 1) as usize].id.to_owned() - } - }; - - if config.auth_token.is_empty() { - error!("`auth_token` was invalid so a config could not be created"); - None - } else { - Some(config) - } -} - -fn pick_command(arguments: Args) { - match arguments { - Args { cmd_init, .. } if cmd_init == true => { - let config_file_path = get_config_path(); - println!("To create the config ({}) we need to find out your `auth_token` and assign aliases to some common bank accounts.", config_file_path.display()); - print!("\n"); - configure_cli(&config_file_path); - () - }, - Args { cmd_accounts, .. } if cmd_accounts == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - }, - Some(config) => list_accounts(&config), - } - }, - Args { cmd_balance, ref arg_account, flag_hide_currency, .. } if cmd_balance == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => show_balance(&config, &arg_account, &flag_hide_currency), - } - }, - Args { cmd_outgoing, ref arg_account, flag_hide_currency, .. } if cmd_outgoing == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => show_outgoing(&config, &arg_account, &flag_hide_currency), - } - }, - Args { cmd_incoming, ref arg_account, flag_hide_currency, .. } if cmd_incoming == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => show_incoming(&config, &arg_account, &flag_hide_currency), - } - }, - Args { cmd_transactions, ref arg_account, flag_show_description, ref flag_timeframe, .. } if cmd_transactions == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => list_transactions(&config, &arg_account, &flag_timeframe, &flag_show_description), - } - }, - Args { cmd_counterparties, ref arg_account, ref flag_timeframe, flag_count, .. } if cmd_counterparties == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => list_counterparties(&config, &arg_account, &flag_timeframe, &flag_count), - } - }, - Args { cmd_balances, ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } if cmd_balances == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => list_balances(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output), - } - }, - Args { cmd_incomings, ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } if cmd_incomings == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => list_incomings(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output), - } - }, - Args { cmd_outgoings, ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } if cmd_outgoings == true => { - match get_config() { - None => { - error!("Configuration could not be found or created so command not executed"); - exit(1) - } - Some(config) => list_outgoings(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output), - } - }, - Args { flag_help, flag_version, .. } if flag_help == true || flag_version == true => (), - _ => println!("{}", USAGE), - } -} - -fn get_account_alias_for_id<'a>(account_id: &str, config: &Config) -> &'a str { - if *account_id == config.current { - "(current)" - } else if *account_id == config.savings { - "(savings)" - } else if *account_id == config.business { - "(business)" - } else { - "" - } -} - -fn represent_list_accounts(accounts: &Vec, config: &Config) { - let mut accounts_table = String::new(); - accounts_table.push_str("row\taccount no.\tbalance\n"); - for (idx, account) in accounts.iter().enumerate() { - let row_number = (idx + 1) as u32; - let account_alias = get_account_alias_for_id(&account.id, &config); - let new_account_row = format!("{} {}\t****{}\t{}\t{}\n", row_number, account_alias, account.account_number_last_4, account.balance, account.currency); - accounts_table = accounts_table + &new_account_row; - } - - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", accounts_table).unwrap(); - tw.flush().unwrap(); - - let accounts_str = String::from_utf8(tw.unwrap()).unwrap(); - - println!("{}", accounts_str) -} - -fn list_accounts(config: &Config) { - match get_accounts(&config) { - Ok(accounts) => represent_list_accounts(&accounts, &config), - Err(e) => { - error!("Unable to list accounts: {}", e); - exit(1) - }, - } -} - -fn represent_money(balance_with_currency: &Money, hide_currency: &bool) { - println!("{}", balance_with_currency.get_balance_for_display(&hide_currency)) -} - -fn get_account_id(config: &Config, account: &AccountType) -> String{ - let default_account_id = config.current.to_owned(); - match *account { - AccountType::Current => config.current.to_owned(), - AccountType::Savings => config.savings.to_owned(), - AccountType::Business => config.business.to_owned(), - _ => default_account_id, - } -} - -fn show_balance(config: &Config, account: &AccountType, hide_currency: &bool) { - let account_id = get_account_id(&config, &account); - match get_account_balance(&config, &account_id) { - Ok(balance) => represent_money(&balance, &hide_currency), - Err(e) => { - error!("Unable to get account balance: {}", e); - exit(1) - }, - } -} - -fn show_outgoing(config: &Config, account: &AccountType, hide_currency: &bool) { - let account_id = get_account_id(&config, &account); - match get_outgoing(&config, &account_id) { - Ok(outgoing) => represent_money(&outgoing, &hide_currency), - Err(e) => { - error!("Unable to get outgoing: {}", e); - exit(1) - }, - } -} - -fn show_incoming(config: &Config, account: &AccountType, hide_currency: &bool) { - let account_id = get_account_id(&config, &account); - match get_incoming(&config, &account_id) { - Ok(incoming) => represent_money(&incoming, &hide_currency), - Err(e) => { - error!("Unable to get incoming: {}", e); - exit(1) - }, - } -} - -fn represent_list_transactions(transactions: &Vec, currency: &str, show_description: &bool) { - let mut transactions_table = String::new(); - - if *show_description { - transactions_table.push_str(&format!("row\tdate\tcounterparty\tamount ({})\tdescription\n", currency)); - for (idx, transaction) in transactions.iter().enumerate() { - let row_number = (idx + 1) as u32; - let new_transaction_row = format!("{}\t{}\t{}\t{}\t{}\n", row_number, transaction.date, transaction.counterparty, transaction.amount, transaction.description); - transactions_table = transactions_table + &new_transaction_row; - } - } else { - transactions_table.push_str(&format!("row\tdate\tcounterparty\tamount ({})\n", currency)); - for (idx, transaction) in transactions.iter().enumerate() { - let row_number = (idx + 1) as u32; - let new_transaction_row = format!("{}\t{}\t{}\t{}\n", row_number, transaction.date, transaction.counterparty, transaction.amount); - transactions_table = transactions_table + &new_transaction_row; - } - } - - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", transactions_table).unwrap(); - tw.flush().unwrap(); - - let transactions_str = String::from_utf8(tw.unwrap()).unwrap(); - - println!("{}", transactions_str) -} - -fn list_transactions(config: &Config, account: &AccountType, timeframe: &Timeframe, show_description: &bool) { - let account_id = get_account_id(&config, &account); - match get_transactions_with_currency(&config, &account_id, &timeframe) { - Ok(transactions_with_currency) => represent_list_transactions(&transactions_with_currency.transactions, &transactions_with_currency.currency, &show_description), - Err(e) => { - error!("Unable to list transactions: {}", e); - exit(1) - }, - } -} - -fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currency: &str, count: &i64) { - let mut counterparties_table = String::new(); - - counterparties_table.push_str(&format!("row\tcounterparty\tamount ({})\n", currency)); - let skip_n = counterparties.len() - (*count as usize); - for (idx, counterparty) in counterparties.iter().skip(skip_n).enumerate() { - let row_number = (idx + 1) as u32; - let new_counterparty_row = format!("{}\t{}\t{}\n", row_number, counterparty.0, counterparty.1); - counterparties_table = counterparties_table + &new_counterparty_row; - } - - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", counterparties_table).unwrap(); - tw.flush().unwrap(); - - let counterparties_str = String::from_utf8(tw.unwrap()).unwrap(); - - println!("{}", counterparties_str) -} - -fn list_counterparties(config: &Config, account: &AccountType, timeframe: &Timeframe, count: &i64) { - let account_id = get_account_id(&config, &account); - match get_counterparties(&config, &account_id, &timeframe) { - Ok(counterparties_with_currency) => represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, &count), - Err(e) => { - error!("Unable to list counterparties: {}", e); - exit(1) - }, - } -} - -fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurrency, output: &OutputFormat) { - match *output { - OutputFormat::Spark => { - let balance_str = hac.historical_amounts.iter().map(|b| b.1.to_owned()).collect::>().join(" "); - println!("{}", balance_str) - }, - OutputFormat::Standard => { - let mut hac_table = String::new(); - let month_cols = hac.historical_amounts.iter().map(|historical_amount| historical_amount.0.to_owned()).collect::>().join("\t"); - hac_table.push_str(&format!("\t{}\n", month_cols)); - hac_table.push_str(&format!("{} ({})", amount_type, hac.currency)); - for historical_amount in hac.historical_amounts.iter() { - let new_amount = format!("\t{}", historical_amount.1); - hac_table = hac_table + &new_amount; - } - - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", hac_table).unwrap(); - tw.flush().unwrap(); - - let hac_str = String::from_utf8(tw.unwrap()).unwrap(); - - println!("{}", hac_str) - }, - } -} - -fn represent_list_balances(hac: &Balances, output: &OutputFormat) { - represent_list_amounts("balance", &hac, &output) -} - -fn list_balances(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) { - let account_id = get_account_id(&config, &account); - match get_balances(&config, &account_id, &interval, &timeframe) { - Ok(balances) => represent_list_balances(&balances, &output), - Err(e) => { - error!("Unable to list balances: {}", e); - exit(1) - }, - } -} - -fn represent_list_outgoings(hac: &Outgoings, output: &OutputFormat) { - represent_list_amounts("outgoing", &hac, &output) -} - -fn list_outgoings(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) { - let account_id = get_account_id(&config, &account); - match get_outgoings(&config, &account_id, &interval, &timeframe) { - Ok(outgoings) => represent_list_outgoings(&outgoings, &output), - Err(e) => { - error!("Unable to list ougoings: {}", e); - exit(1) - }, - } -} - -fn represent_list_incomings(hac: &Incomings, output: &OutputFormat) { - represent_list_amounts("incoming", &hac, &output) -} - -fn list_incomings(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) { - let account_id = get_account_id(&config, &account); - match get_incomings(&config, &account_id, &interval, &timeframe) { - Ok(incomings) => represent_list_incomings(&incomings, &output), - Err(e) => { - error!("Unable to list incomings: {}", e); - exit(1) - }, - } -} - fn main() { env_logger::init().unwrap(); - let arguments: Args = Docopt::new(USAGE) + let arguments = Docopt::new(USAGE) .and_then(|d| { d.version(VERSION.map(|v| v.to_string())) .decode() }) .unwrap_or_else(|e| e.exit()); - pick_command(arguments) + let command_type = get_command_type(&arguments); + + let return_code = execute(&command_type, &arguments); + + process::exit(return_code) } From b2e62e9a4e3d8c07d4a777bbbb3265555214e438 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 12:45:55 +0000 Subject: [PATCH 02/16] wip(refactor): wrote a todo describing rest of refactor --- TODO.md | 15 ++++++++++++++- src/cli/mod.rs | 2 ++ src/command/do_nothing.rs | 4 ---- src/command/list_outgoings.rs | 2 +- src/command/mod.rs | 17 ++++++++++------- src/command/show_usage.rs | 4 ++-- src/config/mod.rs | 4 ++-- src/main.rs | 8 ++++---- 8 files changed, 35 insertions(+), 21 deletions(-) delete mode 100644 src/command/do_nothing.rs diff --git a/TODO.md b/TODO.md index 5546c7c..84b9567 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,16 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). +- [ ] Decide how best to share represent list accounts/amounts. `shared_representations`? +- [ ] `OutputFormat` and `AccountType` should be defined within `TODO: arg_types`. Only parsing should remain within `parse`. +- [ ] Decide where to put `Interval` and `Timeframe`. +- [ ] Commands should all use `info!` to tell us what they're doing. +- [ ] `*_command`s should map rather than match for concision, and then convert success or error to 0 or 1 (with an `error!`) on unwrapping. +- [ ] `get_config` should belong to `config/*` and have a `configure_cli` function passed into it. +- [ ] `get_account_id` should be a method on the `Config` object. `get_account_alias_for_id` should also be a method of this. +- [ ] We should not always be storing code within the `mod.rs`. Vice versa. +- [ ] Can the CLI `NAME` be set in the usage automatically? +- [x] `show_usage_command` should read `USAGE`. +- [x] `parse` is messy as has too much or too little hierarchy. - [ ] Refactor `'client'`: - [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. - [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). @@ -9,9 +20,11 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comme - [x] `struct`s and `impl Decodable`s should remain where they are as are command definition related. - [x] `pick_command` should be simpler: - [x] Only destructure args to get out what you need to match. 'Additional destructuring](http://rustbyexample.com/flow_control/match/destructuring/destructure_structures.html) should happen before the underlying command is called. - - [ ] Only `get_config` once, if it is successful then pass the `&config` onto the commands, otherwise `configure_cli`. `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. + - [x] Only `get_config` once, if it is successful then pass the `&config` onto the commands, otherwise `configure_cli`. + - [ ] `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. - [ ] `get_config` should belong to the `'config'` module. - [ ] `init_config` should become `ask_questions_for_config`. Behind the scenes it should use `inquirer::ask_questions` and some kind of `find_answers*` function to get the answers out for the config. - [x] Commands should be moved into a module `'command'`. This includes `configure_cli`, `list_accounts`, `show_balance`, `list_transactions`, etc. - [ ] Table writing and other response writing should live in `'represent'`. This includes `get_account_alias_for_id`, `represent_list_accounts`, `represent_show_balance`, `represent_list_transactions`, `represent_list_amounts`, `represent_list_balances`, `represent_list_outgoings`, `represent_list_incomings`, etc. - [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, early returns, `let expected_value = match thing { ... }`. +- [ ] Use `rustfmt`. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index ea86848..8878180 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1 +1,3 @@ pub mod parse; + +pub use self::parse::{CommandType, CliArgs, get_command_type}; diff --git a/src/command/do_nothing.rs b/src/command/do_nothing.rs deleted file mode 100644 index 5fe972b..0000000 --- a/src/command/do_nothing.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub fn do_nothing_command() -> i32 { - debug!("--help or --version were passed so we do nothing..."); - 0 -} diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index 7841182..6c373f2 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -16,7 +16,7 @@ pub fn list_outgoings_command(config: &Config, account: &AccountType, interval: 0 }, Err(e) => { - error!("Unable to list ougoings: {}", e); + error!("Unable to list outgoings: {}", e); 1 }, } diff --git a/src/command/mod.rs b/src/command/mod.rs index 8e475b9..7cc333a 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,4 +1,3 @@ -mod do_nothing; mod show_usage; mod initialise; mod list_accounts; @@ -11,11 +10,10 @@ mod list_balances; mod list_outgoings; mod list_incomings; -use cli::parse::{CommandType, CliArgs}; +use cli::{CommandType, CliArgs}; use config::{Config, get_config_path, get_config_file, read_config}; use self::initialise::configure_cli; -use self::do_nothing::do_nothing_command; use self::show_usage::show_usage_command; use self::initialise::initialise_command; use self::list_accounts::list_accounts_command; @@ -46,15 +44,20 @@ fn get_config() -> Option { } } -pub fn execute(command_type: &CommandType, arguments: &CliArgs) -> i32 { +fn do_nothing_command() -> i32 { + debug!("--help or --version were passed in so we are not going to execute anything more..."); + 0 +} + +pub fn execute(usage: &str, command_type: &CommandType, arguments: &CliArgs) -> i32 { match *command_type { CommandType::None => do_nothing_command(), - CommandType::ShowUsage => show_usage_command(), + CommandType::ShowUsage => show_usage_command(usage), CommandType::Initialise => initialise_command(), _ => { match get_config() { None => { - error!("Configuration could not be found or created so command not executed"); + error!("The command was not executed since a config could not be found or created"); 1 }, Some(config) => { @@ -94,7 +97,7 @@ pub fn execute(command_type: &CommandType, arguments: &CliArgs) -> i32 { let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; list_incomings_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) }, - _ => panic!("TODO: This shouldn't be accessible"), + _ => panic!("This shoult not be accessible"), } }, } diff --git a/src/command/show_usage.rs b/src/command/show_usage.rs index 0fc97c2..16599fa 100644 --- a/src/command/show_usage.rs +++ b/src/command/show_usage.rs @@ -1,4 +1,4 @@ -pub fn show_usage_command() -> i32 { - println!("TODO: show usage should have expected behaviour"); +pub fn show_usage_command(usage: &str) -> i32 { + print!("{}", usage); 0 } diff --git a/src/config/mod.rs b/src/config/mod.rs index 37b3394..36de949 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -55,7 +55,7 @@ pub fn get_config_file(config_path: &PathBuf) -> Option { let config_file = File::open(&config_path); match config_file { Err(ref e) if ErrorKind::NotFound == e.kind() => { - debug!("no config file found"); + debug!("No config file found"); None }, Err(_) => panic!("Unable to read config!"), @@ -90,7 +90,7 @@ pub fn write_config(config_file: &mut File, config: &Config) -> Result<(), Confi Ok(()) } -pub fn get_account_id(config: &Config, account: &AccountType) -> String{ +pub fn get_account_id(config: &Config, account: &AccountType) -> String { let default_account_id = config.current.to_owned(); match *account { AccountType::Current => config.current.to_owned(), diff --git a/src/main.rs b/src/main.rs index 7293a95..4c157ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,15 +12,15 @@ extern crate itertools; mod cli; mod command; mod config; -mod client; mod inquirer; +mod client; use docopt::Docopt; -use cli::parse::get_command_type; +use cli::get_command_type; use command::execute; use std::process; -// const NAME: &'static str = "teller"; // TODO: Can this be placed into the usage automatically? +// const NAME: &'static str = "teller"; const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); const USAGE: &'static str = "Banking for the command line. @@ -72,7 +72,7 @@ fn main() { let command_type = get_command_type(&arguments); - let return_code = execute(&command_type, &arguments); + let return_code = execute(USAGE, &command_type, &arguments); process::exit(return_code) } From 89e9fb40e960e028682a0817cbf301de03541a9e Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 13:12:00 +0000 Subject: [PATCH 03/16] wip(refactor): moved data types that are related to what we want to the cli --- TODO.md | 23 +++-------- src/cli/arg_types.rs | 26 ++++++++++++ src/cli/mod.rs | 1 + src/cli/parse.rs | 18 +-------- src/client/mod.rs | 14 +------ src/command/initialise.rs | 2 +- src/command/list_accounts.rs | 36 +---------------- src/command/list_balances.rs | 34 ++-------------- src/command/list_counterparties.rs | 4 +- src/command/list_incomings.rs | 6 +-- src/command/list_outgoings.rs | 6 +-- src/command/list_transactions.rs | 4 +- src/command/mod.rs | 2 + src/command/representations.rs | 64 ++++++++++++++++++++++++++++++ src/command/show_balance.rs | 2 +- src/command/show_incoming.rs | 2 +- src/command/show_outgoing.rs | 2 +- src/config/mod.rs | 2 +- 18 files changed, 123 insertions(+), 125 deletions(-) create mode 100644 src/cli/arg_types.rs create mode 100644 src/command/representations.rs diff --git a/TODO.md b/TODO.md index 84b9567..714e17b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,30 +1,19 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). -- [ ] Decide how best to share represent list accounts/amounts. `shared_representations`? -- [ ] `OutputFormat` and `AccountType` should be defined within `TODO: arg_types`. Only parsing should remain within `parse`. -- [ ] Decide where to put `Interval` and `Timeframe`. +- [x] Decide how best to share represent list accounts/amounts. `shared_representations`? +- [x] `OutputFormat` and `AccountType` should be defined within `TODO: arg_types`. Only parsing should remain within `parse`. +- [x] Decide where to put `Interval` and `Timeframe`. TODO: Consider a move into inform? - [ ] Commands should all use `info!` to tell us what they're doing. - [ ] `*_command`s should map rather than match for concision, and then convert success or error to 0 or 1 (with an `error!`) on unwrapping. - [ ] `get_config` should belong to `config/*` and have a `configure_cli` function passed into it. - [ ] `get_account_id` should be a method on the `Config` object. `get_account_alias_for_id` should also be a method of this. - [ ] We should not always be storing code within the `mod.rs`. Vice versa. - [ ] Can the CLI `NAME` be set in the usage automatically? -- [x] `show_usage_command` should read `USAGE`. -- [x] `parse` is messy as has too much or too little hierarchy. - [ ] Refactor `'client'`: - [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. - [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). -- [x] Refactor `'inquirer'`: - - [x] Into a module directory. -- [x] Refactor `'main'`: - - [x] `struct`s and `impl Decodable`s should remain where they are as are command definition related. - - [x] `pick_command` should be simpler: - - [x] Only destructure args to get out what you need to match. 'Additional destructuring](http://rustbyexample.com/flow_control/match/destructuring/destructure_structures.html) should happen before the underlying command is called. - - [x] Only `get_config` once, if it is successful then pass the `&config` onto the commands, otherwise `configure_cli`. - - [ ] `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. - - [ ] `get_config` should belong to the `'config'` module. - - [ ] `init_config` should become `ask_questions_for_config`. Behind the scenes it should use `inquirer::ask_questions` and some kind of `find_answers*` function to get the answers out for the config. - - [x] Commands should be moved into a module `'command'`. This includes `configure_cli`, `list_accounts`, `show_balance`, `list_transactions`, etc. - - [ ] Table writing and other response writing should live in `'represent'`. This includes `get_account_alias_for_id`, `represent_list_accounts`, `represent_show_balance`, `represent_list_transactions`, `represent_list_amounts`, `represent_list_balances`, `represent_list_outgoings`, `represent_list_incomings`, etc. +- [ ] `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. +- [ ] `get_config` should belong to the `'config'` module. +- [ ] `ask_questions_for_config` should use `inquirer::ask_questions` and some kind of `find_answers*` function to get the answers out for the config. - [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, early returns, `let expected_value = match thing { ... }`. - [ ] Use `rustfmt`. diff --git a/src/cli/arg_types.rs b/src/cli/arg_types.rs new file mode 100644 index 0000000..2bcb4d3 --- /dev/null +++ b/src/cli/arg_types.rs @@ -0,0 +1,26 @@ +#[derive(Debug)] +pub enum AccountType { + Current, + Savings, + Business, + Unknown(String), + None +} + +#[derive(Debug)] +pub enum OutputFormat { + Spark, + Standard, +} + +#[derive(Debug)] +pub enum Interval { + Monthly, +} + +#[derive(Debug)] +pub enum Timeframe { + Year, + SixMonths, + ThreeMonths, +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8878180..c45ffe5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,3 +1,4 @@ +pub mod arg_types; pub mod parse; pub use self::parse::{CommandType, CliArgs, get_command_type}; diff --git a/src/cli/parse.rs b/src/cli/parse.rs index 77367ab..42c4215 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -1,6 +1,7 @@ -use client::{Interval, Timeframe}; use rustc_serialize::{Decodable, Decoder}; +use super::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; + #[derive(Debug, RustcDecodable)] pub struct CliArgs { cmd_init: bool, @@ -26,15 +27,6 @@ pub struct CliArgs { flag_version: bool, } -#[derive(Debug)] -pub enum AccountType { - Current, - Savings, - Business, - Unknown(String), - None -} - impl Decodable for AccountType { fn decode(d: &mut D) -> Result { let s = try!(d.read_str()); @@ -77,12 +69,6 @@ impl Decodable for Timeframe { } } -#[derive(Debug)] -pub enum OutputFormat { - Spark, - Standard, -} - impl Decodable for OutputFormat { fn decode(d: &mut D) -> Result { let s = try!(d.read_str()); diff --git a/src/client/mod.rs b/src/client/mod.rs index f566e0c..ea482ba 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,5 +1,7 @@ pub mod error; +use cli::arg_types::{Interval, Timeframe}; + use config::Config; use hyper::{Client, Url}; @@ -16,18 +18,6 @@ use std::str::FromStr; use self::error::TellerClientError; -#[derive(Debug)] -pub enum Interval { - Monthly, -} - -#[derive(Debug)] -pub enum Timeframe { - Year, - SixMonths, - ThreeMonths, -} - pub type ApiServiceResult = Result; pub type IntervalAmount = (String, String); pub type Balances = HistoricalAmountsWithCurrency; diff --git a/src/command/initialise.rs b/src/command/initialise.rs index 2693fd2..590d648 100644 --- a/src/command/initialise.rs +++ b/src/command/initialise.rs @@ -3,7 +3,7 @@ use config::{Config, get_config_path, get_config_file_to_write, write_config}; use inquirer::{Question, Answer, ask_question}; use client::get_accounts; -use super::list_accounts::represent_list_accounts; +use super::representations::represent_list_accounts; pub fn configure_cli(config_file_path: &PathBuf) -> Option { match ask_questions_for_config() { diff --git a/src/command/list_accounts.rs b/src/command/list_accounts.rs index a45d5a0..26ce826 100644 --- a/src/command/list_accounts.rs +++ b/src/command/list_accounts.rs @@ -1,39 +1,7 @@ use config::Config; -use client::{Account, get_accounts}; +use client::get_accounts; -use std::io::Write; -use tabwriter::TabWriter; - -fn get_account_alias_for_id<'a>(account_id: &str, config: &Config) -> &'a str { - if *account_id == config.current { - "(current)" - } else if *account_id == config.savings { - "(savings)" - } else if *account_id == config.business { - "(business)" - } else { - "" - } -} - -pub fn represent_list_accounts(accounts: &Vec, config: &Config) { - let mut accounts_table = String::new(); - accounts_table.push_str("row\taccount no.\tbalance\n"); - for (idx, account) in accounts.iter().enumerate() { - let row_number = (idx + 1) as u32; - let account_alias = get_account_alias_for_id(&account.id, &config); - let new_account_row = format!("{} {}\t****{}\t{}\t{}\n", row_number, account_alias, account.account_number_last_4, account.balance, account.currency); - accounts_table = accounts_table + &new_account_row; - } - - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", accounts_table).unwrap(); - tw.flush().unwrap(); - - let accounts_str = String::from_utf8(tw.unwrap()).unwrap(); - - println!("{}", accounts_str) -} +use super::representations::represent_list_accounts; pub fn list_accounts_command(config: &Config) -> i32 { match get_accounts(&config) { diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index 077ec42..82ebe5b 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -1,36 +1,8 @@ use config::{Config, get_account_id}; -use client::{HistoricalAmountsWithCurrency, Balances, Interval, Timeframe, get_balances}; -use cli::parse::{AccountType, OutputFormat}; +use client::{Balances, get_balances}; +use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; -use std::io::Write; -use tabwriter::TabWriter; - -pub fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurrency, output: &OutputFormat) { - match *output { - OutputFormat::Spark => { - let balance_str = hac.historical_amounts.iter().map(|b| b.1.to_owned()).collect::>().join(" "); - println!("{}", balance_str) - }, - OutputFormat::Standard => { - let mut hac_table = String::new(); - let month_cols = hac.historical_amounts.iter().map(|historical_amount| historical_amount.0.to_owned()).collect::>().join("\t"); - hac_table.push_str(&format!("\t{}\n", month_cols)); - hac_table.push_str(&format!("{} ({})", amount_type, hac.currency)); - for historical_amount in hac.historical_amounts.iter() { - let new_amount = format!("\t{}", historical_amount.1); - hac_table = hac_table + &new_amount; - } - - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", hac_table).unwrap(); - tw.flush().unwrap(); - - let hac_str = String::from_utf8(tw.unwrap()).unwrap(); - - println!("{}", hac_str) - }, - } -} +use super::representations::represent_list_amounts; fn represent_list_balances(hac: &Balances, output: &OutputFormat) { represent_list_amounts("balance", &hac, &output) diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index ecb20a9..5c18a4d 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -1,6 +1,6 @@ use config::{Config, get_account_id}; -use client::{Timeframe, get_counterparties}; -use cli::parse::AccountType; +use client::get_counterparties; +use cli::arg_types::{AccountType, Timeframe}; use std::io::Write; use tabwriter::TabWriter; diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index 4887f67..2c9bfd2 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -1,8 +1,8 @@ use config::{Config, get_account_id}; -use client::{Incomings, Interval, Timeframe, get_incomings}; -use cli::parse::{AccountType, OutputFormat}; +use client::{Incomings, get_incomings}; +use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; -use super::list_balances::represent_list_amounts; +use super::representations::represent_list_amounts; fn represent_list_incomings(hac: &Incomings, output: &OutputFormat) { represent_list_amounts("incoming", &hac, &output) diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index 6c373f2..e43b511 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -1,8 +1,8 @@ use config::{Config, get_account_id}; -use client::{Outgoings, Interval, Timeframe, get_outgoings}; -use cli::parse::{AccountType, OutputFormat}; +use client::{Outgoings, get_outgoings}; +use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; -use super::list_balances::represent_list_amounts; +use super::representations::represent_list_amounts; fn represent_list_outgoings(hac: &Outgoings, output: &OutputFormat) { represent_list_amounts("outgoing", &hac, &output) diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index fab1006..361c1eb 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -1,6 +1,6 @@ use config::{Config, get_account_id}; -use client::{Transaction, Timeframe, get_transactions_with_currency}; -use cli::parse::AccountType; +use client::{Transaction, get_transactions_with_currency}; +use cli::arg_types::{AccountType, Timeframe}; use std::io::Write; use tabwriter::TabWriter; diff --git a/src/command/mod.rs b/src/command/mod.rs index 7cc333a..068e8d6 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,3 +1,5 @@ +mod representations; + mod show_usage; mod initialise; mod list_accounts; diff --git a/src/command/representations.rs b/src/command/representations.rs new file mode 100644 index 0000000..cf918ce --- /dev/null +++ b/src/command/representations.rs @@ -0,0 +1,64 @@ +use std::io::Write; +use tabwriter::TabWriter; + +use config::Config; +use client::{HistoricalAmountsWithCurrency, Account}; +use cli::arg_types::OutputFormat; + +fn get_account_alias_for_id<'a>(account_id: &str, config: &Config) -> &'a str { + if *account_id == config.current { + "(current)" + } else if *account_id == config.savings { + "(savings)" + } else if *account_id == config.business { + "(business)" + } else { + "" + } +} + +pub fn represent_list_accounts(accounts: &Vec, config: &Config) { + let mut accounts_table = String::new(); + accounts_table.push_str("row\taccount no.\tbalance\n"); + for (idx, account) in accounts.iter().enumerate() { + let row_number = (idx + 1) as u32; + let account_alias = get_account_alias_for_id(&account.id, &config); + let new_account_row = format!("{} {}\t****{}\t{}\t{}\n", row_number, account_alias, account.account_number_last_4, account.balance, account.currency); + accounts_table = accounts_table + &new_account_row; + } + + let mut tw = TabWriter::new(Vec::new()); + write!(&mut tw, "{}", accounts_table).unwrap(); + tw.flush().unwrap(); + + let accounts_str = String::from_utf8(tw.unwrap()).unwrap(); + + println!("{}", accounts_str) +} + +pub fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurrency, output: &OutputFormat) { + match *output { + OutputFormat::Spark => { + let balance_str = hac.historical_amounts.iter().map(|b| b.1.to_owned()).collect::>().join(" "); + println!("{}", balance_str) + }, + OutputFormat::Standard => { + let mut hac_table = String::new(); + let month_cols = hac.historical_amounts.iter().map(|historical_amount| historical_amount.0.to_owned()).collect::>().join("\t"); + hac_table.push_str(&format!("\t{}\n", month_cols)); + hac_table.push_str(&format!("{} ({})", amount_type, hac.currency)); + for historical_amount in hac.historical_amounts.iter() { + let new_amount = format!("\t{}", historical_amount.1); + hac_table = hac_table + &new_amount; + } + + let mut tw = TabWriter::new(Vec::new()); + write!(&mut tw, "{}", hac_table).unwrap(); + tw.flush().unwrap(); + + let hac_str = String::from_utf8(tw.unwrap()).unwrap(); + + println!("{}", hac_str) + }, + } +} diff --git a/src/command/show_balance.rs b/src/command/show_balance.rs index 7967511..d0e43cd 100644 --- a/src/command/show_balance.rs +++ b/src/command/show_balance.rs @@ -1,6 +1,6 @@ use client::get_account_balance; use config::{Config, get_account_id}; -use cli::parse::AccountType; +use cli::arg_types::AccountType; use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs index b757d9c..9ddeb16 100644 --- a/src/command/show_incoming.rs +++ b/src/command/show_incoming.rs @@ -1,6 +1,6 @@ use client::get_incoming; use config::{Config, get_account_id}; -use cli::parse::AccountType; +use cli::arg_types::AccountType; use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { diff --git a/src/command/show_outgoing.rs b/src/command/show_outgoing.rs index 2c4c7ff..4ac39bf 100644 --- a/src/command/show_outgoing.rs +++ b/src/command/show_outgoing.rs @@ -1,6 +1,6 @@ use client::get_outgoing; use config::{Config, get_account_id}; -use cli::parse::AccountType; +use cli::arg_types::AccountType; use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { diff --git a/src/config/mod.rs b/src/config/mod.rs index 36de949..b282fdb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -11,7 +11,7 @@ use std::io::prelude::*; // Required for read_to_string use later. use self::error::ConfigError; -use cli::parse::AccountType; +use cli::arg_types::AccountType; #[derive(Debug, RustcEncodable, RustcDecodable)] pub struct Config { From 3e47d81de3bf32952b101d3801ca8dbc9bffe47a Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 13:55:29 +0000 Subject: [PATCH 04/16] wip(refactor): concision in commands with map and unwrap_or_else --- TODO.md | 7 ++----- src/command/initialise.rs | 1 + src/command/list_accounts.rs | 18 ++++++++---------- src/command/list_balances.rs | 18 ++++++++---------- src/command/list_counterparties.rs | 18 ++++++++---------- src/command/list_incomings.rs | 18 ++++++++---------- src/command/list_outgoings.rs | 18 ++++++++---------- src/command/list_transactions.rs | 18 ++++++++---------- src/command/show_balance.rs | 18 ++++++++---------- src/command/show_incoming.rs | 18 ++++++++---------- src/command/show_outgoing.rs | 18 ++++++++---------- src/command/show_usage.rs | 1 + 12 files changed, 76 insertions(+), 95 deletions(-) diff --git a/TODO.md b/TODO.md index 714e17b..33ce6f8 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,7 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). -- [x] Decide how best to share represent list accounts/amounts. `shared_representations`? -- [x] `OutputFormat` and `AccountType` should be defined within `TODO: arg_types`. Only parsing should remain within `parse`. -- [x] Decide where to put `Interval` and `Timeframe`. TODO: Consider a move into inform? -- [ ] Commands should all use `info!` to tell us what they're doing. -- [ ] `*_command`s should map rather than match for concision, and then convert success or error to 0 or 1 (with an `error!`) on unwrapping. +- [x] Commands should all use `info!` to tell us what they're doing. +- [x] `*_command`s should map rather than match for concision, and then convert success or error to 0 or 1 (with an `error!`) on unwrapping. - [ ] `get_config` should belong to `config/*` and have a `configure_cli` function passed into it. - [ ] `get_account_id` should be a method on the `Config` object. `get_account_alias_for_id` should also be a method of this. - [ ] We should not always be storing code within the `mod.rs`. Vice versa. diff --git a/src/command/initialise.rs b/src/command/initialise.rs index 590d648..ee2401a 100644 --- a/src/command/initialise.rs +++ b/src/command/initialise.rs @@ -90,6 +90,7 @@ fn ask_questions_for_config() -> Option { } pub fn initialise_command() -> i32 { + info!("Calling the initialise command"); let config_file_path = get_config_path(); println!("To create the config ({}) we need to find out your `auth_token` and assign aliases to some common bank accounts.", config_file_path.display()); print!("\n"); diff --git a/src/command/list_accounts.rs b/src/command/list_accounts.rs index 26ce826..c32c33e 100644 --- a/src/command/list_accounts.rs +++ b/src/command/list_accounts.rs @@ -4,14 +4,12 @@ use client::get_accounts; use super::representations::represent_list_accounts; pub fn list_accounts_command(config: &Config) -> i32 { - match get_accounts(&config) { - Ok(accounts) => { - represent_list_accounts(&accounts, &config); - 0 - }, - Err(e) => { - error!("Unable to list accounts: {}", e); - 1 - }, - } + info!("Calling the list accounts command"); + get_accounts(&config).map(|accounts| { + represent_list_accounts(&accounts, &config); + 0 + }).unwrap_or_else(|err| { + error!("Unable to list accounts: {}", err); + 1 + }) } diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index 82ebe5b..87b4a76 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -9,15 +9,13 @@ fn represent_list_balances(hac: &Balances, output: &OutputFormat) { } pub fn list_balances_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { + info!("Calling the list balances command"); let account_id = get_account_id(&config, &account); - match get_balances(&config, &account_id, &interval, &timeframe) { - Ok(balances) => { - represent_list_balances(&balances, &output); - 0 - }, - Err(e) => { - error!("Unable to list balances: {}", e); - 1 - }, - } + get_balances(&config, &account_id, &interval, &timeframe).map(|balances| { + represent_list_balances(&balances, &output); + 0 + }).unwrap_or_else(|err| { + error!("Unable to list balances: {}", err); + 1 + }) } diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index 5c18a4d..2f4ec20 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -26,15 +26,13 @@ fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currenc } pub fn list_counterparties_command(config: &Config, account: &AccountType, timeframe: &Timeframe, count: &i64) -> i32 { + info!("Calling the list counterparties command"); let account_id = get_account_id(&config, &account); - match get_counterparties(&config, &account_id, &timeframe) { - Ok(counterparties_with_currency) => { - represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, &count); - 0 - }, - Err(e) => { - error!("Unable to list counterparties: {}", e); - 1 - }, - } + get_counterparties(&config, &account_id, &timeframe).map(|counterparties_with_currency| { + represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, &count); + 0 + }).unwrap_or_else(|err| { + error!("Unable to list counterparties: {}", err); + 1 + }) } diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index 2c9bfd2..82af3d7 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -9,15 +9,13 @@ fn represent_list_incomings(hac: &Incomings, output: &OutputFormat) { } pub fn list_incomings_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { + info!("Calling the list incomings command"); let account_id = get_account_id(&config, &account); - match get_incomings(&config, &account_id, &interval, &timeframe) { - Ok(incomings) => { - represent_list_incomings(&incomings, &output); - 0 - }, - Err(e) => { - error!("Unable to list incomings: {}", e); - 1 - }, - } + get_incomings(&config, &account_id, &interval, &timeframe).map(|incomings| { + represent_list_incomings(&incomings, &output); + 0 + }).unwrap_or_else(|err| { + error!("Unable to list incomings: {}", err); + 1 + }) } diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index e43b511..d72fc4d 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -9,15 +9,13 @@ fn represent_list_outgoings(hac: &Outgoings, output: &OutputFormat) { } pub fn list_outgoings_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { + info!("Calling the list outgoings command"); let account_id = get_account_id(&config, &account); - match get_outgoings(&config, &account_id, &interval, &timeframe) { - Ok(outgoings) => { - represent_list_outgoings(&outgoings, &output); - 0 - }, - Err(e) => { - error!("Unable to list outgoings: {}", e); - 1 - }, - } + get_outgoings(&config, &account_id, &interval, &timeframe).map(|outgoings| { + represent_list_outgoings(&outgoings, &output); + 0 + }).unwrap_or_else(|err| { + error!("Unable to list outgoings: {}", err); + 1 + }) } diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index 361c1eb..67e7020 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -34,15 +34,13 @@ fn represent_list_transactions(transactions: &Vec, currency: &str, } pub fn list_transactions_command(config: &Config, account: &AccountType, timeframe: &Timeframe, show_description: &bool) -> i32 { + info!("Calling the list transactions command"); let account_id = get_account_id(&config, &account); - match get_transactions_with_currency(&config, &account_id, &timeframe) { - Ok(transactions_with_currency) => { - represent_list_transactions(&transactions_with_currency.transactions, &transactions_with_currency.currency, &show_description); - 0 - }, - Err(e) => { - error!("Unable to list transactions: {}", e); - 1 - }, - } + get_transactions_with_currency(&config, &account_id, &timeframe).map(|transactions_with_currency| { + represent_list_transactions(&transactions_with_currency.transactions, &transactions_with_currency.currency, &show_description); + 0 + }).unwrap_or_else(|err| { + error!("Unable to list transactions: {}", err); + 1 + }) } diff --git a/src/command/show_balance.rs b/src/command/show_balance.rs index d0e43cd..17455b1 100644 --- a/src/command/show_balance.rs +++ b/src/command/show_balance.rs @@ -8,15 +8,13 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { } pub fn show_balance_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { + info!("Calling the show balance command"); let account_id = get_account_id(&config, &account); - match get_account_balance(&config, &account_id) { - Ok(balance) => { - represent_money(&balance, &hide_currency); - 0 - }, - Err(e) => { - error!("Unable to get account balance: {}", e); - 1 - }, - } + get_account_balance(&config, &account_id).map(|balance| { + represent_money(&balance, &hide_currency); + 0 + }).unwrap_or_else(|err| { + error!("Unable to get account balance: {}", err); + 1 + }) } diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs index 9ddeb16..e11dfd2 100644 --- a/src/command/show_incoming.rs +++ b/src/command/show_incoming.rs @@ -8,15 +8,13 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { } pub fn show_incoming_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { + info!("Calling the show incoming command"); let account_id = get_account_id(&config, &account); - match get_incoming(&config, &account_id) { - Ok(incoming) => { - represent_money(&incoming, &hide_currency); - 0 - }, - Err(e) => { - error!("Unable to get incoming: {}", e); - 1 - }, - } + get_incoming(&config, &account_id).map(|incoming| { + represent_money(&incoming, &hide_currency); + 0 + }).unwrap_or_else(|err| { + error!("Unable to get incoming: {}", err); + 1 + }) } diff --git a/src/command/show_outgoing.rs b/src/command/show_outgoing.rs index 4ac39bf..dd79f67 100644 --- a/src/command/show_outgoing.rs +++ b/src/command/show_outgoing.rs @@ -8,15 +8,13 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { } pub fn show_outgoing_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { + info!("Calling the show outgoing command"); let account_id = get_account_id(&config, &account); - match get_outgoing(&config, &account_id) { - Ok(outgoing) => { - represent_money(&outgoing, &hide_currency); - 0 - }, - Err(e) => { - error!("Unable to get outgoing: {}", e); - 1 - }, - } + get_outgoing(&config, &account_id).map(|outgoing| { + represent_money(&outgoing, &hide_currency); + 0 + }).unwrap_or_else(|err| { + error!("Unable to get outgoing: {}", err); + 1 + }) } diff --git a/src/command/show_usage.rs b/src/command/show_usage.rs index 16599fa..bb959e5 100644 --- a/src/command/show_usage.rs +++ b/src/command/show_usage.rs @@ -1,4 +1,5 @@ pub fn show_usage_command(usage: &str) -> i32 { + info!("Calling the show usage command"); print!("{}", usage); 0 } From a0d82b60f80776353a68ac8cc5919118b5d21e72 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 14:39:46 +0000 Subject: [PATCH 05/16] refactor(answers): set config from answers in more consise way --- TODO.md | 9 +-------- src/command/initialise.rs | 31 +++++++++++++------------------ src/inquirer/ask.rs | 6 ++++++ src/inquirer/mod.rs | 2 +- src/main.rs | 1 - 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/TODO.md b/TODO.md index 33ce6f8..7ecf635 100644 --- a/TODO.md +++ b/TODO.md @@ -1,16 +1,9 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). -- [x] Commands should all use `info!` to tell us what they're doing. -- [x] `*_command`s should map rather than match for concision, and then convert success or error to 0 or 1 (with an `error!`) on unwrapping. -- [ ] `get_config` should belong to `config/*` and have a `configure_cli` function passed into it. +- [ ] `get_config` should belong to `config/*` and have a `configure_cli` function passed into it. `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. `get_config` should probably belong to the `'config'` module. - [ ] `get_account_id` should be a method on the `Config` object. `get_account_alias_for_id` should also be a method of this. -- [ ] We should not always be storing code within the `mod.rs`. Vice versa. -- [ ] Can the CLI `NAME` be set in the usage automatically? - [ ] Refactor `'client'`: - [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. - [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). -- [ ] `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. -- [ ] `get_config` should belong to the `'config'` module. -- [ ] `ask_questions_for_config` should use `inquirer::ask_questions` and some kind of `find_answers*` function to get the answers out for the config. - [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, early returns, `let expected_value = match thing { ... }`. - [ ] Use `rustfmt`. diff --git a/src/command/initialise.rs b/src/command/initialise.rs index ee2401a..ae981be 100644 --- a/src/command/initialise.rs +++ b/src/command/initialise.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; use config::{Config, get_config_path, get_config_file_to_write, write_config}; -use inquirer::{Question, Answer, ask_question}; +use inquirer::{Question, Answer, ask_question, ask_questions}; use client::get_accounts; use super::representations::represent_list_accounts; @@ -55,30 +55,25 @@ fn ask_questions_for_config() -> Option { ), ]; - let answers: Vec = questions.iter().map(ask_question).collect(); - let non_empty_answers: Vec<&Answer> = answers.iter().filter(|&answer| !answer.value.is_empty()).collect(); + let non_empty_answers = ask_questions(&questions); let mut fa_iter = non_empty_answers.iter(); - match fa_iter.find(|&answer| answer.name == "current") { + let to_account_id = |answer: &Answer| { + let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); + accounts[(row_number - 1) as usize].id.to_owned() + }; + + match fa_iter.find(|&answer| answer.name == "current").map(&to_account_id) { None => (), - Some(answer) => { - let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); - config.current = accounts[(row_number - 1) as usize].id.to_owned() - }, + Some(account_id) => config.current = account_id, }; - match fa_iter.find(|&answer| answer.name == "savings") { + match fa_iter.find(|&answer| answer.name == "savings").map(&to_account_id) { None => (), - Some(answer) => { - let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); - config.savings = accounts[(row_number - 1) as usize].id.to_owned() - } + Some(account_id) => config.savings = account_id, }; - match fa_iter.find(|&answer| answer.name == "business") { + match fa_iter.find(|&answer| answer.name == "business").map(&to_account_id) { None => (), - Some(answer) => { - let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); - config.business = accounts[(row_number - 1) as usize].id.to_owned() - } + Some(account_id) => config.business = account_id, }; if config.auth_token.is_empty() { diff --git a/src/inquirer/ask.rs b/src/inquirer/ask.rs index 3f9ae13..74cba42 100644 --- a/src/inquirer/ask.rs +++ b/src/inquirer/ask.rs @@ -43,3 +43,9 @@ pub fn ask_question(question: &Question) -> Answer { Err(error) => panic!("Unable to read line for {}: {}", question_name, error), } } + +pub fn ask_questions(questions: &Vec) -> Vec { + let answers: Vec = questions.iter().map(ask_question).collect(); + let non_empty_answers: Vec = answers.into_iter().filter(|answer| !answer.value.is_empty()).collect(); + non_empty_answers +} diff --git a/src/inquirer/mod.rs b/src/inquirer/mod.rs index ea45648..8e50225 100644 --- a/src/inquirer/mod.rs +++ b/src/inquirer/mod.rs @@ -1,3 +1,3 @@ pub mod ask; -pub use self::ask::{Question, Answer, ask_question}; +pub use self::ask::{Question, Answer, ask_question, ask_questions}; diff --git a/src/main.rs b/src/main.rs index 4c157ff..4868b1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,6 @@ use cli::get_command_type; use command::execute; use std::process; -// const NAME: &'static str = "teller"; const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); const USAGE: &'static str = "Banking for the command line. From ea88aff9b5bfa409412b6a5fa4ebae1d9fce2637 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 15:09:36 +0000 Subject: [PATCH 06/16] refactor(config): ensure config exists by getting it or generating it from some questions --- TODO.md | 8 +++----- src/command/mod.rs | 31 ++++++++++++------------------- src/config/mod.rs | 13 +++++++++++++ 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/TODO.md b/TODO.md index 7ecf635..d690dd7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,7 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). -- [ ] `get_config` should belong to `config/*` and have a `configure_cli` function passed into it. `get_config` should not be concerned with execution of `configure_cli` itself. This will mean you will attempt to detect whether `cmd_init` was picked first, separately to the rest of the other commands prior to `get_config` being executed. `get_config` should probably belong to the `'config'` module. - [ ] `get_account_id` should be a method on the `Config` object. `get_account_alias_for_id` should also be a method of this. -- [ ] Refactor `'client'`: - - [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. - - [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). -- [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, early returns, `let expected_value = match thing { ... }`. +- [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. +- [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). +- [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, `unwrap_*`, early returns, `let expected_value = match thing { ... }`. - [ ] Use `rustfmt`. diff --git a/src/command/mod.rs b/src/command/mod.rs index 068e8d6..7f40afc 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -13,7 +13,8 @@ mod list_outgoings; mod list_incomings; use cli::{CommandType, CliArgs}; -use config::{Config, get_config_path, get_config_file, read_config}; + +use config::{Config, get_config, get_config_path}; use self::initialise::configure_cli; use self::show_usage::show_usage_command; @@ -28,22 +29,14 @@ use self::list_balances::list_balances_command; use self::list_outgoings::list_outgoings_command; use self::list_incomings::list_incomings_command; -fn get_config() -> Option { - let config_file_path = get_config_path(); - match get_config_file(&config_file_path) { - None => { - println!("A config file could not be found at: {}", config_file_path.display()); - println!("You will need to set the `auth_token` and give aliases to your bank accounts"); - print!("\n"); - configure_cli(&config_file_path) - }, - Some(mut config_file) => { - match read_config(&mut config_file) { - Ok(config) => Some(config), - Err(e) => panic!("ERROR: attempting to read file {}: {}", config_file_path.display(), e), - } - }, - } +fn ensure_config() -> Option { + get_config().or_else(|| { + let config_file_path = get_config_path(); + println!("A config file could not be found at: {}", config_file_path.display()); + println!("You will need to set the `auth_token` and give aliases to your bank accounts"); + print!("\n"); + configure_cli(&config_file_path) + }) } fn do_nothing_command() -> i32 { @@ -57,7 +50,7 @@ pub fn execute(usage: &str, command_type: &CommandType, arguments: &CliArgs) -> CommandType::ShowUsage => show_usage_command(usage), CommandType::Initialise => initialise_command(), _ => { - match get_config() { + match ensure_config() { None => { error!("The command was not executed since a config could not be found or created"); 1 @@ -99,7 +92,7 @@ pub fn execute(usage: &str, command_type: &CommandType, arguments: &CliArgs) -> let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; list_incomings_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) }, - _ => panic!("This shoult not be accessible"), + _ => panic!("This should not have been executable but for some reason was"), } }, } diff --git a/src/config/mod.rs b/src/config/mod.rs index b282fdb..9adbd97 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -71,6 +71,19 @@ pub fn get_config_file_to_write(config_path: &PathBuf) -> Result Option { + let config_file_path = get_config_path(); + match get_config_file(&config_file_path) { + None => None, + Some(mut config_file) => { + match read_config(&mut config_file) { + Ok(config) => Some(config), + Err(e) => panic!("ERROR: attempting to read file {}: {}", config_file_path.display(), e), + } + }, + } +} + pub fn read_config(config_file: &mut File) -> Result { let mut content_str = String::new(); try!(config_file.read_to_string(&mut content_str)); From 46a037de570eaf8c0d8088c4b3d0fa511c80f514 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 15:32:52 +0000 Subject: [PATCH 07/16] style(rustfmt): ran rustfmt; don't agree with all of it but oh well --- TODO.md | 2 +- src/cli/arg_types.rs | 2 +- src/cli/parse.rs | 24 +-- src/client/mod.rs | 331 ++++++++++++++++++----------- src/command/initialise.rs | 22 +- src/command/list_accounts.rs | 16 +- src/command/list_balances.rs | 23 +- src/command/list_counterparties.rs | 33 ++- src/command/list_incomings.rs | 23 +- src/command/list_outgoings.rs | 23 +- src/command/list_transactions.rs | 45 ++-- src/command/mod.rs | 98 ++++++--- src/command/representations.rs | 27 ++- src/command/show_balance.rs | 19 +- src/command/show_incoming.rs | 19 +- src/command/show_outgoing.rs | 19 +- src/config/mod.rs | 28 ++- src/inquirer/ask.rs | 4 +- src/main.rs | 83 +++++--- 19 files changed, 543 insertions(+), 298 deletions(-) diff --git a/TODO.md b/TODO.md index d690dd7..d11ca52 100644 --- a/TODO.md +++ b/TODO.md @@ -4,4 +4,4 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comme - [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. - [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). - [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, `unwrap_*`, early returns, `let expected_value = match thing { ... }`. -- [ ] Use `rustfmt`. +- [x] Use `rustfmt`. diff --git a/src/cli/arg_types.rs b/src/cli/arg_types.rs index 2bcb4d3..0f014a1 100644 --- a/src/cli/arg_types.rs +++ b/src/cli/arg_types.rs @@ -4,7 +4,7 @@ pub enum AccountType { Savings, Business, Unknown(String), - None + None, } #[derive(Debug)] diff --git a/src/cli/parse.rs b/src/cli/parse.rs index 42c4215..39186d5 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -51,7 +51,7 @@ impl Decodable for Interval { _ => { error!("teller-cli currently only suports an interval of monthly"); default_interval - }, + } }) } } @@ -99,17 +99,17 @@ pub enum CommandType { pub fn get_command_type(arguments: &CliArgs) -> CommandType { match *arguments { - CliArgs { cmd_init, .. } if cmd_init == true => CommandType::Initialise, - CliArgs { cmd_accounts, .. } if cmd_accounts == true => CommandType::ListAccounts, - CliArgs { cmd_balance, .. } if cmd_balance == true => CommandType::ShowBalance, - CliArgs { cmd_outgoing, .. } if cmd_outgoing == true => CommandType::ShowOutgoing, - CliArgs { cmd_incoming, .. } if cmd_incoming == true => CommandType::ShowIncoming, - CliArgs { cmd_transactions, .. } if cmd_transactions == true => CommandType::ListTransactions, - CliArgs { cmd_counterparties, .. } if cmd_counterparties == true => CommandType::ListCounterparties, - CliArgs { cmd_balances, .. } if cmd_balances == true => CommandType::ListBalances, - CliArgs { cmd_incomings, .. } if cmd_incomings == true => CommandType::ListIncomings, - CliArgs { cmd_outgoings, .. } if cmd_outgoings == true => CommandType::ListOutgoings, - CliArgs { flag_help, flag_version, .. } if flag_help == true || flag_version == true => CommandType::None, + CliArgs { cmd_init, .. } if cmd_init => CommandType::Initialise, + CliArgs { cmd_accounts, .. } if cmd_accounts => CommandType::ListAccounts, + CliArgs { cmd_balance, .. } if cmd_balance => CommandType::ShowBalance, + CliArgs { cmd_outgoing, .. } if cmd_outgoing => CommandType::ShowOutgoing, + CliArgs { cmd_incoming, .. } if cmd_incoming => CommandType::ShowIncoming, + CliArgs { cmd_transactions, .. } if cmd_transactions => CommandType::ListTransactions, + CliArgs { cmd_counterparties, .. } if cmd_counterparties => CommandType::ListCounterparties, + CliArgs { cmd_balances, .. } if cmd_balances => CommandType::ListBalances, + CliArgs { cmd_incomings, .. } if cmd_incomings => CommandType::ListIncomings, + CliArgs { cmd_outgoings, .. } if cmd_outgoings => CommandType::ListOutgoings, + CliArgs { flag_help, flag_version, .. } if flag_help || flag_version => CommandType::None, _ => CommandType::ShowUsage, } } diff --git a/src/client/mod.rs b/src/client/mod.rs index ea482ba..0d54ee2 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -23,6 +23,7 @@ pub type IntervalAmount = (String, String); pub type Balances = HistoricalAmountsWithCurrency; pub type Outgoings = HistoricalAmountsWithCurrency; pub type Incomings = HistoricalAmountsWithCurrency; +type DateStringToTransactions = (String, Vec); #[derive(Debug)] pub struct Money { @@ -31,7 +32,6 @@ pub struct Money { } impl Money { - pub fn new>(amount: S, currency: S) -> Money { Money { amount: amount.into(), @@ -47,7 +47,6 @@ impl Money { balance_with_currency.to_owned() } } - } #[derive(Debug, RustcDecodable)] @@ -90,7 +89,9 @@ pub struct HistoricalAmountsWithCurrency { } impl HistoricalAmountsWithCurrency { - pub fn new>(historical_amounts: Vec, currency: S) -> HistoricalAmountsWithCurrency { + pub fn new>(historical_amounts: Vec, + currency: S) + -> HistoricalAmountsWithCurrency { HistoricalAmountsWithCurrency { historical_amounts: historical_amounts, currency: currency.into(), @@ -105,7 +106,9 @@ pub struct TransactionsWithCurrrency { } impl TransactionsWithCurrrency { - pub fn new>(transactions: Vec, currency: S) -> TransactionsWithCurrrency { + pub fn new>(transactions: Vec, + currency: S) + -> TransactionsWithCurrrency { TransactionsWithCurrrency { transactions: transactions, currency: currency.into(), @@ -120,7 +123,9 @@ pub struct CounterpartiesWithCurrrency { } impl CounterpartiesWithCurrrency { - pub fn new>(counterparties: Vec<(String, String)>, currency: S) -> CounterpartiesWithCurrrency { + pub fn new>(counterparties: Vec<(String, String)>, + currency: S) + -> CounterpartiesWithCurrrency { CounterpartiesWithCurrrency { counterparties: counterparties, currency: currency.into(), @@ -129,21 +134,15 @@ impl CounterpartiesWithCurrrency { } fn get_auth_header(auth_token: &str) -> Authorization { - Authorization( - Bearer { - token: auth_token.to_string(), - } - ) + Authorization(Bearer { token: auth_token.to_string() }) } pub fn get_accounts(config: &Config) -> ApiServiceResult> { let client = Client::new(); - let mut res = try!( - client.get("https://api.teller.io/accounts") - .header(get_auth_header(&config.auth_token)) - .send() - ); + let mut res = try!(client.get("https://api.teller.io/accounts") + .header(get_auth_header(&config.auth_token)) + .send()); if res.status.is_client_error() { return Err(TellerClientError::AuthenticationError); } @@ -161,11 +160,9 @@ pub fn get_accounts(config: &Config) -> ApiServiceResult> { pub fn get_account(config: &Config, account_id: &str) -> ApiServiceResult { let client = Client::new(); - let mut res = try!( - client.get(&format!("https://api.teller.io/accounts/{}", account_id)) - .header(get_auth_header(&config.auth_token)) - .send() - ); + let mut res = try!(client.get(&format!("https://api.teller.io/accounts/{}", account_id)) + .header(get_auth_header(&config.auth_token)) + .send()); if res.status.is_client_error() { return Err(TellerClientError::AuthenticationError); } @@ -185,20 +182,24 @@ pub fn get_account_balance(config: &Config, account_id: &str) -> ApiServiceResul get_account(&config, &account_id).map(to_money) } -pub fn raw_transactions(config: &Config, account_id: &str, count: u32, page: u32) -> ApiServiceResult> { - let mut url = Url::parse(&format!("https://api.teller.io/accounts/{}/transactions", account_id)).unwrap(); +pub fn raw_transactions(config: &Config, + account_id: &str, + count: u32, + page: u32) + -> ApiServiceResult> { + let mut url = Url::parse(&format!("https://api.teller.io/accounts/{}/transactions", + account_id)) + .unwrap(); const COUNT: &'static str = "count"; const PAGE: &'static str = "page"; - let query = vec![ (COUNT, count.to_string()), (PAGE, page.to_string()) ]; + let query = vec![(COUNT, count.to_string()), (PAGE, page.to_string())]; url.set_query_from_pairs(query.into_iter()); let client = Client::new(); - let mut res = try!( - client.get(url) - .header(get_auth_header(&config.auth_token)) - .send() - ); + let mut res = try!(client.get(url) + .header(get_auth_header(&config.auth_token)) + .send()); if res.status.is_client_error() { return Err(TellerClientError::AuthenticationError); } @@ -220,7 +221,10 @@ fn parse_utc_date_from_transaction(t: &Transaction) -> Date { past_transaction_date } -pub fn get_transactions(config: &Config, account_id: &str, timeframe: &Timeframe) -> ApiServiceResult> { +pub fn get_transactions(config: &Config, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult> { let page_through_transactions = |from| -> ApiServiceResult> { let mut all_transactions = vec![]; @@ -233,23 +237,26 @@ pub fn get_transactions(config: &Config, account_id: &str, timeframe: &Timeframe None => { // If there are no transactions left, do not fetch forever... fetching = false - }, + } Some(past_transaction) => { let past_transaction_date = parse_utc_date_from_transaction(&past_transaction); if past_transaction_date < from { fetching = false; } - }, + } }; all_transactions.append(&mut transactions); page = page + 1; } - all_transactions = all_transactions.into_iter().filter(|t| { - let transaction_date = parse_utc_date_from_transaction(&t); - transaction_date > from - }).collect(); + all_transactions = all_transactions.into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); all_transactions.reverse(); Ok(all_transactions) @@ -261,23 +268,26 @@ pub fn get_transactions(config: &Config, account_id: &str, timeframe: &Timeframe let from = to - Duration::days(91); // close enough... 😅 page_through_transactions(from) - }, + } Timeframe::SixMonths => { let to = UTC::today(); let from = to - Duration::days(183); page_through_transactions(from) - }, + } Timeframe::Year => { let to = UTC::today(); let from = to - Duration::days(365); page_through_transactions(from) - }, + } } } -pub fn get_transactions_with_currency(config: &Config, account_id: &str, timeframe: &Timeframe) -> ApiServiceResult { +pub fn get_transactions_with_currency(config: &Config, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult { let transactions = try!(get_transactions(&config, &account_id, &timeframe)); let account = try!(get_account(&config, &account_id)); @@ -286,74 +296,113 @@ pub fn get_transactions_with_currency(config: &Config, account_id: &str, timefra Ok(TransactionsWithCurrrency::new(transactions, currency)) } -fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec) -> HashMap> { - let grouped_counterparties = transactions.iter().fold(HashMap::new(), |mut acc: HashMap>, t: &'a Transaction| { - let counterparty = t.counterparty.to_owned(); - if acc.contains_key(&counterparty) { - if let Some(txs) = acc.get_mut(&counterparty) { - txs.push(t); - } - } else { - let mut txs: Vec<&'a Transaction> = vec![]; - txs.push(t); - acc.insert(counterparty, txs); - } - - acc - }); +fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec) + -> HashMap> { + let grouped_counterparties = transactions.iter() + .fold(HashMap::new(), |mut acc: HashMap>, + t: &'a Transaction| { + let counterparty = t.counterparty.to_owned(); + if acc.contains_key(&counterparty) { + if let Some(txs) = acc.get_mut(&counterparty) { + txs.push(t); + } + } else { + let mut txs: Vec<&'a Transaction> = vec![]; + txs.push(t); + acc.insert(counterparty, txs); + } + + acc + }); grouped_counterparties.into_iter().fold(HashMap::new(), |mut acc, (counterparty, txs)| { - let date_amount_tuples = txs.into_iter().map(|tx| (tx.date.to_owned(), tx.amount.to_owned())).collect(); + let date_amount_tuples = txs.into_iter() + .map(|tx| (tx.date.to_owned(), tx.amount.to_owned())) + .collect(); acc.insert(counterparty.to_string(), date_amount_tuples); acc }) } -pub fn get_counterparties(config: &Config, account_id: &str, timeframe: &Timeframe) -> ApiServiceResult { - let transactions_with_currency = try!(get_transactions_with_currency(&config, &account_id, &timeframe)); +pub fn get_counterparties(config: &Config, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult { + let transactions_with_currency = try!(get_transactions_with_currency(&config, + &account_id, + &timeframe)); - let to_cent_integer = |amount: &str| { - (f64::from_str(&amount).unwrap() * 100f64).round() as i64 - }; + let to_cent_integer = |amount: &str| (f64::from_str(&amount).unwrap() * 100f64).round() as i64; let from_cent_integer_to_float_string = |amount: &i64| { format!("{:.2}", *amount as f64 / 100f64) }; - let transactions: Vec = transactions_with_currency.transactions.into_iter().filter(|tx| to_cent_integer(&tx.amount) < 0).collect(); + let transactions: Vec = transactions_with_currency.transactions + .into_iter() + .filter(|tx| { + to_cent_integer(&tx.amount) < + 0 + }) + .collect(); let currency = transactions_with_currency.currency; - let counterparty_to_date_amount_list = convert_to_counterparty_to_date_amount_list(&transactions); - let sorted_counterparties = counterparty_to_date_amount_list.into_iter().map(|(counterparty, date_amount_tuples)| { - let amount = date_amount_tuples.iter().fold(0i64, |acc, dat| acc + to_cent_integer(&dat.1)); - (counterparty, amount.abs()) - }).sort_by(|&(_, amount_a), &(_, amount_b)| { - amount_a.cmp(&amount_b) - }); - let counterparties = sorted_counterparties.into_iter().map(|(counterparty, amount)| { - (counterparty, from_cent_integer_to_float_string(&amount)) - }).collect(); + let counterparty_to_date_amount_list = + convert_to_counterparty_to_date_amount_list(&transactions); + let sorted_counterparties = + counterparty_to_date_amount_list.into_iter() + .map(|(counterparty, date_amount_tuples)| { + let amount = + date_amount_tuples.iter().fold(0i64, |acc, dat| { + acc + to_cent_integer(&dat.1) + }); + (counterparty, amount.abs()) + }) + .sort_by(|&(_, amount_a), &(_, amount_b)| { + amount_a.cmp(&amount_b) + }); + let counterparties = sorted_counterparties.into_iter() + .map(|(counterparty, amount)| { + (counterparty, + from_cent_integer_to_float_string(&amount)) + }) + .collect(); Ok(CounterpartiesWithCurrrency::new(counterparties, currency)) } -fn get_grouped_transaction_aggregates(config: &Config, account_id: &str, interval: &Interval, timeframe: &Timeframe, aggregate_txs: &Fn((String, Vec)) -> (String, i64)) -> ApiServiceResult> { - let transactions: Vec = get_transactions(&config, &account_id, &timeframe).unwrap_or(vec![]); - - let mut month_year_grouped_transactions: Vec<(String, i64)> = transactions.into_iter().group_by(|t| { - let transaction_date = parse_utc_date_from_transaction(&t); - match *interval { - Interval::Monthly => { - let group_name = transaction_date.format("%m-%Y").to_string(); - group_name - } - } - }).map(aggregate_txs).collect(); +fn get_grouped_transaction_aggregates(config: &Config, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe, + aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) + -> ApiServiceResult> { + let transactions: Vec = get_transactions(&config, &account_id, &timeframe) + .unwrap_or(vec![]); + + let mut month_year_grouped_transactions: Vec<(String, i64)> = + transactions.into_iter() + .group_by(|t| { + let transaction_date = parse_utc_date_from_transaction(&t); + match *interval { + Interval::Monthly => { + let group_name = transaction_date.format("%m-%Y").to_string(); + group_name + } + } + }) + .map(aggregate_txs) + .collect(); month_year_grouped_transactions.reverse(); Ok(month_year_grouped_transactions) } -pub fn get_balances(config: &Config, account_id: &str, interval: &Interval, timeframe: &Timeframe) -> ApiServiceResult { +pub fn get_balances(config: &Config, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { let sum_all = |myt: (String, Vec)| { let to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 @@ -364,81 +413,105 @@ pub fn get_balances(config: &Config, account_id: &str, interval: &Interval, time (group_name, amount) }; - let month_year_total_transactions = try!(get_grouped_transaction_aggregates(&config, &account_id, &interval, &timeframe, &sum_all)); + let month_year_total_transactions = try!(get_grouped_transaction_aggregates(&config, + &account_id, + &interval, + &timeframe, + &sum_all)); let account = try!(get_account(&config, &account_id)); let current_balance = (f64::from_str(&account.balance).unwrap() * 100f64).round() as i64; let currency = account.currency; let mut historical_amounts: Vec = vec![]; - historical_amounts.push(("current".to_string(), format!("{:.2}", current_balance as f64 / 100f64))); + historical_amounts.push(("current".to_string(), + format!("{:.2}", current_balance as f64 / 100f64))); let mut last_balance = current_balance; for mytt in month_year_total_transactions { last_balance = last_balance - mytt.1; - historical_amounts.push((mytt.0.to_string(), format!("{:.2}", last_balance as f64 / 100f64))); + historical_amounts.push((mytt.0.to_string(), + format!("{:.2}", last_balance as f64 / 100f64))); } historical_amounts.reverse(); Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } -pub fn get_outgoings(config: &Config, account_id: &str, interval: &Interval, timeframe: &Timeframe) -> ApiServiceResult { +pub fn get_outgoings(config: &Config, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { let sum_outgoings = |myt: (String, Vec)| { let to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 }; let group_name = myt.0; - let amount = myt.1.iter().map(to_cent_integer).filter(|ci| { - *ci < 0 - }).fold(0i64, |sum, v| sum + v); + let amount = myt.1 + .iter() + .map(to_cent_integer) + .filter(|ci| *ci < 0) + .fold(0i64, |sum, v| sum + v); (group_name, amount) }; - let month_year_total_outgoing = try!(get_grouped_transaction_aggregates(&config, &account_id, &interval, &timeframe, &sum_outgoings)); + let month_year_total_outgoing = try!(get_grouped_transaction_aggregates(&config, + &account_id, + &interval, + &timeframe, + &sum_outgoings)); let account = try!(get_account(&config, &account_id)); let currency = account.currency; - let from_cent_integer_to_float_string = |amount: i64| { - format!("{:.2}", amount as f64 / 100f64) - }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); let mut historical_amounts: Vec = vec![]; for mytt in month_year_total_outgoing { - historical_amounts.push((mytt.0.to_string(), from_cent_integer_to_float_string(mytt.1.abs()))); + historical_amounts.push((mytt.0.to_string(), + from_cent_integer_to_float_string(mytt.1.abs()))); } historical_amounts.reverse(); Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } -pub fn get_incomings(config: &Config, account_id: &str, interval: &Interval, timeframe: &Timeframe) -> ApiServiceResult { +pub fn get_incomings(config: &Config, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { let sum_incomings = |myt: (String, Vec)| { let to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 }; let group_name = myt.0; - let amount = myt.1.iter().map(to_cent_integer).filter(|ci| { - *ci > 0 - }).fold(0i64, |sum, v| sum + v); + let amount = myt.1 + .iter() + .map(to_cent_integer) + .filter(|ci| *ci > 0) + .fold(0i64, |sum, v| sum + v); (group_name, amount) }; - let month_year_total_incoming = try!(get_grouped_transaction_aggregates(&config, &account_id, &interval, &timeframe, &sum_incomings)); + let month_year_total_incoming = try!(get_grouped_transaction_aggregates(&config, + &account_id, + &interval, + &timeframe, + &sum_incomings)); let account = try!(get_account(&config, &account_id)); let currency = account.currency; - let from_cent_integer_to_float_string = |amount: i64| { - format!("{:.2}", amount as f64 / 100f64) - }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); let mut historical_amounts: Vec = vec![]; for mytt in month_year_total_incoming { - historical_amounts.push((mytt.0.to_string(), from_cent_integer_to_float_string(mytt.1))); + historical_amounts.push((mytt.0.to_string(), + from_cent_integer_to_float_string(mytt.1))); } historical_amounts.reverse(); @@ -450,21 +523,25 @@ pub fn get_outgoing(config: &Config, account_id: &str) -> ApiServiceResult = raw_transactions(&config, &account_id, 250, 1).unwrap_or(vec![]).into_iter().filter(|t| { - let transaction_date = parse_utc_date_from_transaction(&t); - transaction_date > from - }).collect(); + let transactions: Vec = raw_transactions(&config, &account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); let from_float_string_to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 }; - let from_cent_integer_to_float_string = |amount: i64| { - format!("{:.2}", amount as f64 / 100f64) - }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - let outgoing = transactions.iter().map(from_float_string_to_cent_integer).filter(|ci| { - *ci < 0 - }).fold(0i64, |sum, v| sum + v); + let outgoing = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci < 0) + .fold(0i64, |sum, v| sum + v); Ok(Money::new(from_cent_integer_to_float_string(outgoing.abs()), currency)) } @@ -474,21 +551,25 @@ pub fn get_incoming(config: &Config, account_id: &str) -> ApiServiceResult = raw_transactions(&config, &account_id, 250, 1).unwrap_or(vec![]).into_iter().filter(|t| { - let transaction_date = parse_utc_date_from_transaction(&t); - transaction_date > from - }).collect(); + let transactions: Vec = raw_transactions(&config, &account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); let from_float_string_to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 }; - let from_cent_integer_to_float_string = |amount: i64| { - format!("{:.2}", amount as f64 / 100f64) - }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - let incoming = transactions.iter().map(from_float_string_to_cent_integer).filter(|ci| { - *ci > 0 - }).fold(0i64, |sum, v| sum + v); + let incoming = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci > 0) + .fold(0i64, |sum, v| sum + v); Ok(Money::new(from_cent_integer_to_float_string(incoming), currency)) } diff --git a/src/command/initialise.rs b/src/command/initialise.rs index ae981be..6dc766e 100644 --- a/src/command/initialise.rs +++ b/src/command/initialise.rs @@ -13,18 +13,16 @@ pub fn configure_cli(config_file_path: &PathBuf) -> Option { Ok(mut config_file) => { let _ = write_config(&mut config_file, &config); Some(config) - }, + } Err(e) => panic!("ERROR: opening file to write: {}", e), } - }, + } } } fn ask_questions_for_config() -> Option { - let get_auth_token_question = Question::new( - "auth_token", - "What is your `auth_token` on teller.io?", - ); + let get_auth_token_question = Question::new("auth_token", + "What is your `auth_token` on teller.io?"); let auth_token_answer = ask_question(&get_auth_token_question); @@ -37,7 +35,8 @@ fn ask_questions_for_config() -> Option { }; represent_list_accounts(&accounts, &config); - println!("Please type the row (e.g. 3) of the account you wish to place against an alias and press to set this in the config. Leave empty if irrelevant."); + println!("Please type the row (e.g. 3) of the account you wish to place against an alias and \ + press to set this in the config. Leave empty if irrelevant."); print!("\n"); let questions = vec![ @@ -59,7 +58,10 @@ fn ask_questions_for_config() -> Option { let mut fa_iter = non_empty_answers.iter(); let to_account_id = |answer: &Answer| { - let row_number: u32 = answer.value.parse().expect(&format!("ERROR: {:?} did not contain a number", answer)); + let row_number: u32 = answer.value + .parse() + .expect(&format!("ERROR: {:?} did not contain a number", + answer)); accounts[(row_number - 1) as usize].id.to_owned() }; @@ -87,7 +89,9 @@ fn ask_questions_for_config() -> Option { pub fn initialise_command() -> i32 { info!("Calling the initialise command"); let config_file_path = get_config_path(); - println!("To create the config ({}) we need to find out your `auth_token` and assign aliases to some common bank accounts.", config_file_path.display()); + println!("To create the config ({}) we need to find out your `auth_token` and assign aliases \ + to some common bank accounts.", + config_file_path.display()); print!("\n"); configure_cli(&config_file_path); 0 diff --git a/src/command/list_accounts.rs b/src/command/list_accounts.rs index c32c33e..d0f7761 100644 --- a/src/command/list_accounts.rs +++ b/src/command/list_accounts.rs @@ -5,11 +5,13 @@ use super::representations::represent_list_accounts; pub fn list_accounts_command(config: &Config) -> i32 { info!("Calling the list accounts command"); - get_accounts(&config).map(|accounts| { - represent_list_accounts(&accounts, &config); - 0 - }).unwrap_or_else(|err| { - error!("Unable to list accounts: {}", err); - 1 - }) + get_accounts(&config) + .map(|accounts| { + represent_list_accounts(&accounts, &config); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list accounts: {}", err); + 1 + }) } diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index 87b4a76..4f8a4a9 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -8,14 +8,21 @@ fn represent_list_balances(hac: &Balances, output: &OutputFormat) { represent_list_amounts("balance", &hac, &output) } -pub fn list_balances_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { +pub fn list_balances_command(config: &Config, + account: &AccountType, + interval: &Interval, + timeframe: &Timeframe, + output: &OutputFormat) + -> i32 { info!("Calling the list balances command"); let account_id = get_account_id(&config, &account); - get_balances(&config, &account_id, &interval, &timeframe).map(|balances| { - represent_list_balances(&balances, &output); - 0 - }).unwrap_or_else(|err| { - error!("Unable to list balances: {}", err); - 1 - }) + get_balances(&config, &account_id, &interval, &timeframe) + .map(|balances| { + represent_list_balances(&balances, &output); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list balances: {}", err); + 1 + }) } diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index 2f4ec20..1a110bc 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -5,14 +5,19 @@ use cli::arg_types::{AccountType, Timeframe}; use std::io::Write; use tabwriter::TabWriter; -fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currency: &str, count: &i64) { +fn represent_list_counterparties(counterparties: &Vec<(String, String)>, + currency: &str, + count: &i64) { let mut counterparties_table = String::new(); counterparties_table.push_str(&format!("row\tcounterparty\tamount ({})\n", currency)); let skip_n = counterparties.len() - (*count as usize); for (idx, counterparty) in counterparties.iter().skip(skip_n).enumerate() { let row_number = (idx + 1) as u32; - let new_counterparty_row = format!("{}\t{}\t{}\n", row_number, counterparty.0, counterparty.1); + let new_counterparty_row = format!("{}\t{}\t{}\n", + row_number, + counterparty.0, + counterparty.1); counterparties_table = counterparties_table + &new_counterparty_row; } @@ -25,14 +30,22 @@ fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currenc println!("{}", counterparties_str) } -pub fn list_counterparties_command(config: &Config, account: &AccountType, timeframe: &Timeframe, count: &i64) -> i32 { +pub fn list_counterparties_command(config: &Config, + account: &AccountType, + timeframe: &Timeframe, + count: &i64) + -> i32 { info!("Calling the list counterparties command"); let account_id = get_account_id(&config, &account); - get_counterparties(&config, &account_id, &timeframe).map(|counterparties_with_currency| { - represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, &count); - 0 - }).unwrap_or_else(|err| { - error!("Unable to list counterparties: {}", err); - 1 - }) + get_counterparties(&config, &account_id, &timeframe) + .map(|counterparties_with_currency| { + represent_list_counterparties(&counterparties_with_currency.counterparties, + &counterparties_with_currency.currency, + &count); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list counterparties: {}", err); + 1 + }) } diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index 82af3d7..647220c 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -8,14 +8,21 @@ fn represent_list_incomings(hac: &Incomings, output: &OutputFormat) { represent_list_amounts("incoming", &hac, &output) } -pub fn list_incomings_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { +pub fn list_incomings_command(config: &Config, + account: &AccountType, + interval: &Interval, + timeframe: &Timeframe, + output: &OutputFormat) + -> i32 { info!("Calling the list incomings command"); let account_id = get_account_id(&config, &account); - get_incomings(&config, &account_id, &interval, &timeframe).map(|incomings| { - represent_list_incomings(&incomings, &output); - 0 - }).unwrap_or_else(|err| { - error!("Unable to list incomings: {}", err); - 1 - }) + get_incomings(&config, &account_id, &interval, &timeframe) + .map(|incomings| { + represent_list_incomings(&incomings, &output); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list incomings: {}", err); + 1 + }) } diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index d72fc4d..d83d0dc 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -8,14 +8,21 @@ fn represent_list_outgoings(hac: &Outgoings, output: &OutputFormat) { represent_list_amounts("outgoing", &hac, &output) } -pub fn list_outgoings_command(config: &Config, account: &AccountType, interval: &Interval, timeframe: &Timeframe, output: &OutputFormat) -> i32 { +pub fn list_outgoings_command(config: &Config, + account: &AccountType, + interval: &Interval, + timeframe: &Timeframe, + output: &OutputFormat) + -> i32 { info!("Calling the list outgoings command"); let account_id = get_account_id(&config, &account); - get_outgoings(&config, &account_id, &interval, &timeframe).map(|outgoings| { - represent_list_outgoings(&outgoings, &output); - 0 - }).unwrap_or_else(|err| { - error!("Unable to list outgoings: {}", err); - 1 - }) + get_outgoings(&config, &account_id, &interval, &timeframe) + .map(|outgoings| { + represent_list_outgoings(&outgoings, &output); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list outgoings: {}", err); + 1 + }) } diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index 67e7020..be886c5 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -5,21 +5,34 @@ use cli::arg_types::{AccountType, Timeframe}; use std::io::Write; use tabwriter::TabWriter; -fn represent_list_transactions(transactions: &Vec, currency: &str, show_description: &bool) { +fn represent_list_transactions(transactions: &Vec, + currency: &str, + show_description: &bool) { let mut transactions_table = String::new(); if *show_description { - transactions_table.push_str(&format!("row\tdate\tcounterparty\tamount ({})\tdescription\n", currency)); + transactions_table.push_str(&format!("row\tdate\tcounterparty\tamount \ + ({})\tdescription\n", + currency)); for (idx, transaction) in transactions.iter().enumerate() { let row_number = (idx + 1) as u32; - let new_transaction_row = format!("{}\t{}\t{}\t{}\t{}\n", row_number, transaction.date, transaction.counterparty, transaction.amount, transaction.description); + let new_transaction_row = format!("{}\t{}\t{}\t{}\t{}\n", + row_number, + transaction.date, + transaction.counterparty, + transaction.amount, + transaction.description); transactions_table = transactions_table + &new_transaction_row; } } else { transactions_table.push_str(&format!("row\tdate\tcounterparty\tamount ({})\n", currency)); for (idx, transaction) in transactions.iter().enumerate() { let row_number = (idx + 1) as u32; - let new_transaction_row = format!("{}\t{}\t{}\t{}\n", row_number, transaction.date, transaction.counterparty, transaction.amount); + let new_transaction_row = format!("{}\t{}\t{}\t{}\n", + row_number, + transaction.date, + transaction.counterparty, + transaction.amount); transactions_table = transactions_table + &new_transaction_row; } } @@ -33,14 +46,22 @@ fn represent_list_transactions(transactions: &Vec, currency: &str, println!("{}", transactions_str) } -pub fn list_transactions_command(config: &Config, account: &AccountType, timeframe: &Timeframe, show_description: &bool) -> i32 { +pub fn list_transactions_command(config: &Config, + account: &AccountType, + timeframe: &Timeframe, + show_description: &bool) + -> i32 { info!("Calling the list transactions command"); let account_id = get_account_id(&config, &account); - get_transactions_with_currency(&config, &account_id, &timeframe).map(|transactions_with_currency| { - represent_list_transactions(&transactions_with_currency.transactions, &transactions_with_currency.currency, &show_description); - 0 - }).unwrap_or_else(|err| { - error!("Unable to list transactions: {}", err); - 1 - }) + get_transactions_with_currency(&config, &account_id, &timeframe) + .map(|transactions_with_currency| { + represent_list_transactions(&transactions_with_currency.transactions, + &transactions_with_currency.currency, + &show_description); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list transactions: {}", err); + 1 + }) } diff --git a/src/command/mod.rs b/src/command/mod.rs index 7f40afc..ee03788 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -32,7 +32,8 @@ use self::list_incomings::list_incomings_command; fn ensure_config() -> Option { get_config().or_else(|| { let config_file_path = get_config_path(); - println!("A config file could not be found at: {}", config_file_path.display()); + println!("A config file could not be found at: {}", + config_file_path.display()); println!("You will need to set the `auth_token` and give aliases to your bank accounts"); print!("\n"); configure_cli(&config_file_path) @@ -52,50 +53,95 @@ pub fn execute(usage: &str, command_type: &CommandType, arguments: &CliArgs) -> _ => { match ensure_config() { None => { - error!("The command was not executed since a config could not be found or created"); + error!("The command was not executed since a config could not be found or \ + generated"); 1 - }, + } Some(config) => { match *command_type { - CommandType::ListAccounts => { - list_accounts_command(&config) - }, + CommandType::ListAccounts => list_accounts_command(&config), CommandType::ShowBalance => { let CliArgs { ref arg_account, flag_hide_currency, .. } = *arguments; show_balance_command(&config, &arg_account, &flag_hide_currency) - }, + } CommandType::ShowOutgoing => { let CliArgs { ref arg_account, flag_hide_currency, .. } = *arguments; show_outgoing_command(&config, &arg_account, &flag_hide_currency) - }, + } CommandType::ShowIncoming => { let CliArgs { ref arg_account, flag_hide_currency, .. } = *arguments; show_incoming_command(&config, &arg_account, &flag_hide_currency) - }, + } CommandType::ListTransactions => { - let CliArgs { ref arg_account, flag_show_description, ref flag_timeframe, .. } = *arguments; - list_transactions_command(&config, &arg_account, &flag_timeframe, &flag_show_description) - }, + let CliArgs { + ref arg_account, + flag_show_description, + ref flag_timeframe, + .. + } = *arguments; + list_transactions_command(&config, + &arg_account, + &flag_timeframe, + &flag_show_description) + } CommandType::ListCounterparties => { - let CliArgs { ref arg_account, ref flag_timeframe, flag_count, .. } = *arguments; - list_counterparties_command(&config, &arg_account, &flag_timeframe, &flag_count) - }, + let CliArgs { + ref arg_account, + ref flag_timeframe, + flag_count, + .. + } = *arguments; + list_counterparties_command(&config, + &arg_account, + &flag_timeframe, + &flag_count) + } CommandType::ListBalances => { - let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; - list_balances_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) - }, + let CliArgs { + ref arg_account, + ref flag_interval, + ref flag_timeframe, + ref flag_output, + .. + } = *arguments; + list_balances_command(&config, + &arg_account, + &flag_interval, + &flag_timeframe, + &flag_output) + } CommandType::ListOutgoings => { - let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; - list_outgoings_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) - }, + let CliArgs { + ref arg_account, + ref flag_interval, + ref flag_timeframe, + ref flag_output, + .. + } = *arguments; + list_outgoings_command(&config, + &arg_account, + &flag_interval, + &flag_timeframe, + &flag_output) + } CommandType::ListIncomings => { - let CliArgs { ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } = *arguments; - list_incomings_command(&config, &arg_account, &flag_interval, &flag_timeframe, &flag_output) - }, + let CliArgs { + ref arg_account, + ref flag_interval, + ref flag_timeframe, + ref flag_output, + .. + } = *arguments; + list_incomings_command(&config, + &arg_account, + &flag_interval, + &flag_timeframe, + &flag_output) + } _ => panic!("This should not have been executable but for some reason was"), } - }, + } } - }, + } } } diff --git a/src/command/representations.rs b/src/command/representations.rs index cf918ce..68bbd31 100644 --- a/src/command/representations.rs +++ b/src/command/representations.rs @@ -23,7 +23,12 @@ pub fn represent_list_accounts(accounts: &Vec, config: &Config) { for (idx, account) in accounts.iter().enumerate() { let row_number = (idx + 1) as u32; let account_alias = get_account_alias_for_id(&account.id, &config); - let new_account_row = format!("{} {}\t****{}\t{}\t{}\n", row_number, account_alias, account.account_number_last_4, account.balance, account.currency); + let new_account_row = format!("{} {}\t****{}\t{}\t{}\n", + row_number, + account_alias, + account.account_number_last_4, + account.balance, + account.currency); accounts_table = accounts_table + &new_account_row; } @@ -36,15 +41,25 @@ pub fn represent_list_accounts(accounts: &Vec, config: &Config) { println!("{}", accounts_str) } -pub fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurrency, output: &OutputFormat) { +pub fn represent_list_amounts(amount_type: &str, + hac: &HistoricalAmountsWithCurrency, + output: &OutputFormat) { match *output { OutputFormat::Spark => { - let balance_str = hac.historical_amounts.iter().map(|b| b.1.to_owned()).collect::>().join(" "); + let balance_str = hac.historical_amounts + .iter() + .map(|b| b.1.to_owned()) + .collect::>() + .join(" "); println!("{}", balance_str) - }, + } OutputFormat::Standard => { let mut hac_table = String::new(); - let month_cols = hac.historical_amounts.iter().map(|historical_amount| historical_amount.0.to_owned()).collect::>().join("\t"); + let month_cols = hac.historical_amounts + .iter() + .map(|historical_amount| historical_amount.0.to_owned()) + .collect::>() + .join("\t"); hac_table.push_str(&format!("\t{}\n", month_cols)); hac_table.push_str(&format!("{} ({})", amount_type, hac.currency)); for historical_amount in hac.historical_amounts.iter() { @@ -59,6 +74,6 @@ pub fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurr let hac_str = String::from_utf8(tw.unwrap()).unwrap(); println!("{}", hac_str) - }, + } } } diff --git a/src/command/show_balance.rs b/src/command/show_balance.rs index 17455b1..0cbd40b 100644 --- a/src/command/show_balance.rs +++ b/src/command/show_balance.rs @@ -4,17 +4,20 @@ use cli::arg_types::AccountType; use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { - println!("{}", money_with_currency.get_balance_for_display(&hide_currency)) + println!("{}", + money_with_currency.get_balance_for_display(&hide_currency)) } pub fn show_balance_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show balance command"); let account_id = get_account_id(&config, &account); - get_account_balance(&config, &account_id).map(|balance| { - represent_money(&balance, &hide_currency); - 0 - }).unwrap_or_else(|err| { - error!("Unable to get account balance: {}", err); - 1 - }) + get_account_balance(&config, &account_id) + .map(|balance| { + represent_money(&balance, &hide_currency); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to get account balance: {}", err); + 1 + }) } diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs index e11dfd2..42abe92 100644 --- a/src/command/show_incoming.rs +++ b/src/command/show_incoming.rs @@ -4,17 +4,20 @@ use cli::arg_types::AccountType; use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { - println!("{}", money_with_currency.get_balance_for_display(&hide_currency)) + println!("{}", + money_with_currency.get_balance_for_display(&hide_currency)) } pub fn show_incoming_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show incoming command"); let account_id = get_account_id(&config, &account); - get_incoming(&config, &account_id).map(|incoming| { - represent_money(&incoming, &hide_currency); - 0 - }).unwrap_or_else(|err| { - error!("Unable to get incoming: {}", err); - 1 - }) + get_incoming(&config, &account_id) + .map(|incoming| { + represent_money(&incoming, &hide_currency); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to get incoming: {}", err); + 1 + }) } diff --git a/src/command/show_outgoing.rs b/src/command/show_outgoing.rs index dd79f67..5571c65 100644 --- a/src/command/show_outgoing.rs +++ b/src/command/show_outgoing.rs @@ -4,17 +4,20 @@ use cli::arg_types::AccountType; use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { - println!("{}", money_with_currency.get_balance_for_display(&hide_currency)) + println!("{}", + money_with_currency.get_balance_for_display(&hide_currency)) } pub fn show_outgoing_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show outgoing command"); let account_id = get_account_id(&config, &account); - get_outgoing(&config, &account_id).map(|outgoing| { - represent_money(&outgoing, &hide_currency); - 0 - }).unwrap_or_else(|err| { - error!("Unable to get outgoing: {}", err); - 1 - }) + get_outgoing(&config, &account_id) + .map(|outgoing| { + represent_money(&outgoing, &hide_currency); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to get outgoing: {}", err); + 1 + }) } diff --git a/src/config/mod.rs b/src/config/mod.rs index 9adbd97..1db0748 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -32,12 +32,10 @@ impl Config { } pub fn new_with_auth_token_only>(auth_token: S) -> Config { - Config::new( - auth_token.into(), - "".to_string(), - "".to_string(), - "".to_string(), - ) + Config::new(auth_token.into(), + "".to_string(), + "".to_string(), + "".to_string()) } } @@ -51,13 +49,15 @@ pub fn get_config_path() -> PathBuf { } pub fn get_config_file(config_path: &PathBuf) -> Option { - info!("Checking whether config file within {} exists", config_path.to_str().unwrap()); + let config_path_str = config_path.to_str().unwrap_or("[error: config_path#to_str fails]"); + info!("Checking whether config file within {} exists", + config_path_str); let config_file = File::open(&config_path); match config_file { Err(ref e) if ErrorKind::NotFound == e.kind() => { debug!("No config file found"); None - }, + } Err(_) => panic!("Unable to read config!"), Ok(config_file) => Some(config_file), } @@ -66,7 +66,9 @@ pub fn get_config_file(config_path: &PathBuf) -> Option { pub fn get_config_file_to_write(config_path: &PathBuf) -> Result { let config_file = File::create(&config_path); match config_file { - Err(ref e) if ErrorKind::PermissionDenied == e.kind() => panic!("Permission to read config denied"), + Err(ref e) if ErrorKind::PermissionDenied == e.kind() => { + panic!("Permission to read config denied") + } _ => config_file, } } @@ -78,9 +80,13 @@ pub fn get_config() -> Option { Some(mut config_file) => { match read_config(&mut config_file) { Ok(config) => Some(config), - Err(e) => panic!("ERROR: attempting to read file {}: {}", config_file_path.display(), e), + Err(e) => { + panic!("ERROR: attempting to read file {}: {}", + config_file_path.display(), + e) + } } - }, + } } } diff --git a/src/inquirer/ask.rs b/src/inquirer/ask.rs index 74cba42..d495ae5 100644 --- a/src/inquirer/ask.rs +++ b/src/inquirer/ask.rs @@ -46,6 +46,8 @@ pub fn ask_question(question: &Question) -> Answer { pub fn ask_questions(questions: &Vec) -> Vec { let answers: Vec = questions.iter().map(ask_question).collect(); - let non_empty_answers: Vec = answers.into_iter().filter(|answer| !answer.value.is_empty()).collect(); + let non_empty_answers: Vec = answers.into_iter() + .filter(|answer| !answer.value.is_empty()) + .collect(); non_empty_answers } diff --git a/src/main.rs b/src/main.rs index 4868b1f..de6f4fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,49 +25,74 @@ const USAGE: &'static str = "Banking for the command line. Usage: teller init - teller [list] accounts - teller [list] transactions [ --timeframe= --show-description] - teller [list] counterparties [ --timeframe= --count=] - teller [list] (balances|outgoings|incomings) [ --interval= --timeframe= --output=] - teller [show] balance [ --hide-currency] - teller [show] outgoing [ --hide-currency] - teller [show] incoming [ --hide-currency] + teller \ + [list] accounts + teller [list] transactions [ \ + --timeframe= --show-description] + teller [list] \ + counterparties [ --timeframe= --count=] + teller \ + [list] (balances|outgoings|incomings) [ --interval= \ + --timeframe= --output=] + teller [show] balance [ \ + --hide-currency] + teller [show] outgoing [ \ + --hide-currency] + teller [show] incoming [ \ + --hide-currency] teller [--help | --version] Commands: - init Configure. + init \ + Configure. list accounts List accounts. - list transactions List transactions. - list counterparties List outgoing amounts grouped by counterparties. - list balances List balances during a timeframe. - list outgoings List outgoings during a timeframe. - list incomings List incomings during a timeframe. - show balance Show the current balance. - show outgoing Show the current outgoing. + list \ + transactions List transactions. + list counterparties \ + List outgoing amounts grouped by counterparties. + list balances \ + List balances during a timeframe. + list outgoings List \ + outgoings during a timeframe. + list incomings List \ + incomings during a timeframe. + show balance Show the \ + current balance. + show outgoing Show the current \ + outgoing. show incoming Show the current incoming. - NOTE: By default commands are applied to the 'current' . + \ + NOTE: By default commands are applied to the 'current' . -Options: +\ + Options: -h --help Show this screen. - -V --version Show version. - -i --interval= Group by an interval of time [default: monthly]. - -t --timeframe= Operate upon a named period of time [default: 6-months]. - -c --count= Only the top N elements [default: 10]. - -d --show-description Show descriptions against transactions. - -c --hide-currency Show money without currency codes. - -o --output= Output in a particular format (e.g. spark). + -V \ + --version Show version. + -i --interval= Group \ + by an interval of time [default: monthly]. + -t --timeframe= \ + Operate upon a named period of time [default: 6-months]. + -c \ + --count= Only the top N elements [default: 10]. + -d \ + --show-description Show descriptions against transactions. + -c \ + --hide-currency Show money without currency codes. + -o \ + --output= Output in a particular format (e.g. spark). "; fn main() { env_logger::init().unwrap(); let arguments = Docopt::new(USAGE) - .and_then(|d| { - d.version(VERSION.map(|v| v.to_string())) - .decode() - }) - .unwrap_or_else(|e| e.exit()); + .and_then(|d| { + d.version(VERSION.map(|v| v.to_string())) + .decode() + }) + .unwrap_or_else(|e| e.exit()); let command_type = get_command_type(&arguments); From 8d8208e55864df89bb9dbe9ba83714f0fc430da6 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 15:51:56 +0000 Subject: [PATCH 08/16] refactor(config): now contains helper methods --- TODO.md | 4 ++-- src/client/mod.rs | 14 +++++++++++++ src/command/list_balances.rs | 4 ++-- src/command/list_counterparties.rs | 4 ++-- src/command/list_incomings.rs | 4 ++-- src/command/list_outgoings.rs | 4 ++-- src/command/list_transactions.rs | 4 ++-- src/command/representations.rs | 14 +------------ src/command/show_balance.rs | 4 ++-- src/command/show_incoming.rs | 4 ++-- src/command/show_outgoing.rs | 4 ++-- src/config/mod.rs | 32 ++++++++++++++++++++---------- 12 files changed, 55 insertions(+), 41 deletions(-) diff --git a/TODO.md b/TODO.md index d11ca52..d78d319 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). -- [ ] `get_account_id` should be a method on the `Config` object. `get_account_alias_for_id` should also be a method of this. +- [x] `get_account_id` should be a method on the `Config` object. +- [x] `get_account_alias_for_id` should also be a method of this. - [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. - [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). - [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, `unwrap_*`, early returns, `let expected_value = match thing { ... }`. -- [x] Use `rustfmt`. diff --git a/src/client/mod.rs b/src/client/mod.rs index 0d54ee2..e4c66ba 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -133,6 +133,20 @@ impl CounterpartiesWithCurrrency { } } +/* +pub struct TellerClient<'b> { + auth_token: &'b str, +} + +impl<'b> TellerClient<'b> { + pub fn new(auth_token: &'b str) -> TellerClient { + TellerClient { + auth_token: auth_token, + } + } +} +*/ + fn get_auth_header(auth_token: &str) -> Authorization { Authorization(Bearer { token: auth_token.to_string() }) } diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index 4f8a4a9..9203f31 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -1,4 +1,4 @@ -use config::{Config, get_account_id}; +use config::Config; use client::{Balances, get_balances}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; @@ -15,7 +15,7 @@ pub fn list_balances_command(config: &Config, output: &OutputFormat) -> i32 { info!("Calling the list balances command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_balances(&config, &account_id, &interval, &timeframe) .map(|balances| { represent_list_balances(&balances, &output); diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index 1a110bc..3f47052 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -1,4 +1,4 @@ -use config::{Config, get_account_id}; +use config::Config; use client::get_counterparties; use cli::arg_types::{AccountType, Timeframe}; @@ -36,7 +36,7 @@ pub fn list_counterparties_command(config: &Config, count: &i64) -> i32 { info!("Calling the list counterparties command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_counterparties(&config, &account_id, &timeframe) .map(|counterparties_with_currency| { represent_list_counterparties(&counterparties_with_currency.counterparties, diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index 647220c..a47aa8b 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -1,4 +1,4 @@ -use config::{Config, get_account_id}; +use config::Config; use client::{Incomings, get_incomings}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; @@ -15,7 +15,7 @@ pub fn list_incomings_command(config: &Config, output: &OutputFormat) -> i32 { info!("Calling the list incomings command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_incomings(&config, &account_id, &interval, &timeframe) .map(|incomings| { represent_list_incomings(&incomings, &output); diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index d83d0dc..751d223 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -1,4 +1,4 @@ -use config::{Config, get_account_id}; +use config::Config; use client::{Outgoings, get_outgoings}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; @@ -15,7 +15,7 @@ pub fn list_outgoings_command(config: &Config, output: &OutputFormat) -> i32 { info!("Calling the list outgoings command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_outgoings(&config, &account_id, &interval, &timeframe) .map(|outgoings| { represent_list_outgoings(&outgoings, &output); diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index be886c5..a66d2c8 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -1,4 +1,4 @@ -use config::{Config, get_account_id}; +use config::Config; use client::{Transaction, get_transactions_with_currency}; use cli::arg_types::{AccountType, Timeframe}; @@ -52,7 +52,7 @@ pub fn list_transactions_command(config: &Config, show_description: &bool) -> i32 { info!("Calling the list transactions command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_transactions_with_currency(&config, &account_id, &timeframe) .map(|transactions_with_currency| { represent_list_transactions(&transactions_with_currency.transactions, diff --git a/src/command/representations.rs b/src/command/representations.rs index 68bbd31..888ca7c 100644 --- a/src/command/representations.rs +++ b/src/command/representations.rs @@ -5,24 +5,12 @@ use config::Config; use client::{HistoricalAmountsWithCurrency, Account}; use cli::arg_types::OutputFormat; -fn get_account_alias_for_id<'a>(account_id: &str, config: &Config) -> &'a str { - if *account_id == config.current { - "(current)" - } else if *account_id == config.savings { - "(savings)" - } else if *account_id == config.business { - "(business)" - } else { - "" - } -} - pub fn represent_list_accounts(accounts: &Vec, config: &Config) { let mut accounts_table = String::new(); accounts_table.push_str("row\taccount no.\tbalance\n"); for (idx, account) in accounts.iter().enumerate() { let row_number = (idx + 1) as u32; - let account_alias = get_account_alias_for_id(&account.id, &config); + let account_alias = config.get_account_alias_for_id(&account.id); let new_account_row = format!("{} {}\t****{}\t{}\t{}\n", row_number, account_alias, diff --git a/src/command/show_balance.rs b/src/command/show_balance.rs index 0cbd40b..2e5ac49 100644 --- a/src/command/show_balance.rs +++ b/src/command/show_balance.rs @@ -1,5 +1,5 @@ use client::get_account_balance; -use config::{Config, get_account_id}; +use config::Config; use cli::arg_types::AccountType; use client::Money; @@ -10,7 +10,7 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { pub fn show_balance_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show balance command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_account_balance(&config, &account_id) .map(|balance| { represent_money(&balance, &hide_currency); diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs index 42abe92..bb37da7 100644 --- a/src/command/show_incoming.rs +++ b/src/command/show_incoming.rs @@ -1,5 +1,5 @@ use client::get_incoming; -use config::{Config, get_account_id}; +use config::Config; use cli::arg_types::AccountType; use client::Money; @@ -10,7 +10,7 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { pub fn show_incoming_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show incoming command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_incoming(&config, &account_id) .map(|incoming| { represent_money(&incoming, &hide_currency); diff --git a/src/command/show_outgoing.rs b/src/command/show_outgoing.rs index 5571c65..de25caa 100644 --- a/src/command/show_outgoing.rs +++ b/src/command/show_outgoing.rs @@ -1,5 +1,5 @@ use client::get_outgoing; -use config::{Config, get_account_id}; +use config::Config; use cli::arg_types::AccountType; use client::Money; @@ -10,7 +10,7 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { pub fn show_outgoing_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show outgoing command"); - let account_id = get_account_id(&config, &account); + let account_id = config.get_account_id(&account); get_outgoing(&config, &account_id) .map(|outgoing| { represent_money(&outgoing, &hide_currency); diff --git a/src/config/mod.rs b/src/config/mod.rs index 1db0748..eac05e2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -37,6 +37,28 @@ impl Config { "".to_string(), "".to_string()) } + + pub fn get_account_id(&self, account: &AccountType) -> String { + let default_account_id = self.current.to_owned(); + match *account { + AccountType::Current => self.current.to_owned(), + AccountType::Savings => self.savings.to_owned(), + AccountType::Business => self.business.to_owned(), + _ => default_account_id, + } + } + + pub fn get_account_alias_for_id<'a>(&self, account_id: &str) -> &'a str { + if *account_id == self.current { + "(current)" + } else if *account_id == self.savings { + "(savings)" + } else if *account_id == self.business { + "(business)" + } else { + "" + } + } } pub fn get_config_path() -> PathBuf { @@ -108,13 +130,3 @@ pub fn write_config(config_file: &mut File, config: &Config) -> Result<(), Confi Ok(()) } - -pub fn get_account_id(config: &Config, account: &AccountType) -> String { - let default_account_id = config.current.to_owned(); - match *account { - AccountType::Current => config.current.to_owned(), - AccountType::Savings => config.savings.to_owned(), - AccountType::Business => config.business.to_owned(), - _ => default_account_id, - } -} From dfe0f3384a8f0bd06e1b2549f61884c08fa09f63 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 16:59:04 +0000 Subject: [PATCH 09/16] refactor(client): moved all of the client methods into a struct --- TODO.md | 7 +- src/client/mod.rs | 788 ++++++++++++++--------------- src/command/initialise.rs | 11 +- src/command/list_accounts.rs | 21 +- src/command/list_balances.rs | 5 +- src/command/list_counterparties.rs | 5 +- src/command/list_incomings.rs | 5 +- src/command/list_outgoings.rs | 5 +- src/command/list_transactions.rs | 10 +- src/command/show_balance.rs | 21 +- src/command/show_incoming.rs | 5 +- src/command/show_outgoing.rs | 21 +- 12 files changed, 445 insertions(+), 459 deletions(-) diff --git a/TODO.md b/TODO.md index d78d319..85e536c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,6 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). -- [x] `get_account_id` should be a method on the `Config` object. -- [x] `get_account_alias_for_id` should also be a method of this. -- [ ] Create a struct that receives an `authToken` on instantiation and implements basic methods to fetch data - each of these should use some kind of underlying `auth_request` method. This is instead of everything receiving `&Config` (a class that belongs to another module). Move to a separate crate `teller_api`. -- [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). - [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, `unwrap_*`, early returns, `let expected_value = match thing { ... }`. +- [ ] Data types from within TellerClient should be encapsulated depending on what they belong to. +- [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). +- [ ] Move client to a separate crate `teller_api`. diff --git a/src/client/mod.rs b/src/client/mod.rs index e4c66ba..cacc811 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -2,8 +2,6 @@ pub mod error; use cli::arg_types::{Interval, Timeframe}; -use config::Config; - use hyper::{Client, Url}; use hyper::header::{Authorization, Bearer}; use rustc_serialize::json; @@ -19,36 +17,13 @@ use std::str::FromStr; use self::error::TellerClientError; pub type ApiServiceResult = Result; + pub type IntervalAmount = (String, String); pub type Balances = HistoricalAmountsWithCurrency; pub type Outgoings = HistoricalAmountsWithCurrency; pub type Incomings = HistoricalAmountsWithCurrency; type DateStringToTransactions = (String, Vec); -#[derive(Debug)] -pub struct Money { - amount: String, - currency: String, -} - -impl Money { - pub fn new>(amount: S, currency: S) -> Money { - Money { - amount: amount.into(), - currency: currency.into(), - } - } - - pub fn get_balance_for_display(&self, hide_currency: &bool) -> String { - if *hide_currency { - self.amount.to_owned() - } else { - let balance_with_currency = format!("{} {}", self.amount, self.currency); - balance_with_currency.to_owned() - } - } -} - #[derive(Debug, RustcDecodable)] struct AccountResponse { data: Account, @@ -133,99 +108,32 @@ impl CounterpartiesWithCurrrency { } } -/* -pub struct TellerClient<'b> { - auth_token: &'b str, +#[derive(Debug)] +pub struct Money { + amount: String, + currency: String, } -impl<'b> TellerClient<'b> { - pub fn new(auth_token: &'b str) -> TellerClient { - TellerClient { - auth_token: auth_token, +impl Money { + pub fn new>(amount: S, currency: S) -> Money { + Money { + amount: amount.into(), + currency: currency.into(), } } -} -*/ - -fn get_auth_header(auth_token: &str) -> Authorization { - Authorization(Bearer { token: auth_token.to_string() }) -} - -pub fn get_accounts(config: &Config) -> ApiServiceResult> { - let client = Client::new(); - - let mut res = try!(client.get("https://api.teller.io/accounts") - .header(get_auth_header(&config.auth_token)) - .send()); - if res.status.is_client_error() { - return Err(TellerClientError::AuthenticationError); - } - - let mut body = String::new(); - try!(res.read_to_string(&mut body)); - - debug!("GET /accounts response: {}", body); - - let accounts_response: AccountsResponse = try!(json::decode(&body)); - - Ok(accounts_response.data) -} -pub fn get_account(config: &Config, account_id: &str) -> ApiServiceResult { - let client = Client::new(); - - let mut res = try!(client.get(&format!("https://api.teller.io/accounts/{}", account_id)) - .header(get_auth_header(&config.auth_token)) - .send()); - if res.status.is_client_error() { - return Err(TellerClientError::AuthenticationError); + pub fn get_balance_for_display(&self, hide_currency: &bool) -> String { + if *hide_currency { + self.amount.to_owned() + } else { + let balance_with_currency = format!("{} {}", self.amount, self.currency); + balance_with_currency.to_owned() + } } - - let mut body = String::new(); - try!(res.read_to_string(&mut body)); - - debug!("GET /account/:id response: {}", body); - - let account_response: AccountResponse = try!(json::decode(&body)); - - Ok(account_response.data) -} - -pub fn get_account_balance(config: &Config, account_id: &str) -> ApiServiceResult { - let to_money = |a: Account| Money::new(a.balance, a.currency); - get_account(&config, &account_id).map(to_money) } -pub fn raw_transactions(config: &Config, - account_id: &str, - count: u32, - page: u32) - -> ApiServiceResult> { - let mut url = Url::parse(&format!("https://api.teller.io/accounts/{}/transactions", - account_id)) - .unwrap(); - - const COUNT: &'static str = "count"; - const PAGE: &'static str = "page"; - let query = vec![(COUNT, count.to_string()), (PAGE, page.to_string())]; - url.set_query_from_pairs(query.into_iter()); - - let client = Client::new(); - let mut res = try!(client.get(url) - .header(get_auth_header(&config.auth_token)) - .send()); - if res.status.is_client_error() { - return Err(TellerClientError::AuthenticationError); - } - - let mut body = String::new(); - try!(res.read_to_string(&mut body)); - - debug!("GET /account/:id/transactions response: {}", body); - - let transactions_response: TransactionsResponse = try!(json::decode(&body)); - - Ok(transactions_response.data) +fn get_auth_header(auth_token: &str) -> Authorization { + Authorization(Bearer { token: auth_token.to_string() }) } fn parse_utc_date_from_transaction(t: &Transaction) -> Date { @@ -235,81 +143,6 @@ fn parse_utc_date_from_transaction(t: &Transaction) -> Date { past_transaction_date } -pub fn get_transactions(config: &Config, - account_id: &str, - timeframe: &Timeframe) - -> ApiServiceResult> { - let page_through_transactions = |from| -> ApiServiceResult> { - let mut all_transactions = vec![]; - - let mut fetching = true; - let mut page = 1; - let count = 250; - while fetching { - let mut transactions = try!(raw_transactions(config, &account_id, count, page)); - match transactions.last() { - None => { - // If there are no transactions left, do not fetch forever... - fetching = false - } - Some(past_transaction) => { - let past_transaction_date = parse_utc_date_from_transaction(&past_transaction); - if past_transaction_date < from { - fetching = false; - } - } - }; - - all_transactions.append(&mut transactions); - page = page + 1; - } - - all_transactions = all_transactions.into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - all_transactions.reverse(); - Ok(all_transactions) - }; - - match *timeframe { - Timeframe::ThreeMonths => { - let to = UTC::today(); - let from = to - Duration::days(91); // close enough... 😅 - - page_through_transactions(from) - } - Timeframe::SixMonths => { - let to = UTC::today(); - let from = to - Duration::days(183); - - page_through_transactions(from) - } - Timeframe::Year => { - let to = UTC::today(); - let from = to - Duration::days(365); - - page_through_transactions(from) - } - } -} - -pub fn get_transactions_with_currency(config: &Config, - account_id: &str, - timeframe: &Timeframe) - -> ApiServiceResult { - let transactions = try!(get_transactions(&config, &account_id, &timeframe)); - - let account = try!(get_account(&config, &account_id)); - let currency = account.currency; - - Ok(TransactionsWithCurrrency::new(transactions, currency)) -} - fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec) -> HashMap> { let grouped_counterparties = transactions.iter() @@ -339,251 +172,394 @@ fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec ApiServiceResult { - let transactions_with_currency = try!(get_transactions_with_currency(&config, - &account_id, - &timeframe)); - - let to_cent_integer = |amount: &str| (f64::from_str(&amount).unwrap() * 100f64).round() as i64; - let from_cent_integer_to_float_string = |amount: &i64| { - format!("{:.2}", *amount as f64 / 100f64) - }; - - let transactions: Vec = transactions_with_currency.transactions - .into_iter() - .filter(|tx| { - to_cent_integer(&tx.amount) < - 0 - }) - .collect(); - let currency = transactions_with_currency.currency; - - let counterparty_to_date_amount_list = - convert_to_counterparty_to_date_amount_list(&transactions); - let sorted_counterparties = - counterparty_to_date_amount_list.into_iter() - .map(|(counterparty, date_amount_tuples)| { - let amount = - date_amount_tuples.iter().fold(0i64, |acc, dat| { - acc + to_cent_integer(&dat.1) - }); - (counterparty, amount.abs()) - }) - .sort_by(|&(_, amount_a), &(_, amount_b)| { - amount_a.cmp(&amount_b) - }); - let counterparties = sorted_counterparties.into_iter() - .map(|(counterparty, amount)| { - (counterparty, - from_cent_integer_to_float_string(&amount)) - }) - .collect(); - - Ok(CounterpartiesWithCurrrency::new(counterparties, currency)) +const TELLER_API_SERVER_URL: &'static str = "https://api.teller.io"; + +pub struct TellerClient<'a> { + auth_token: &'a str, } -fn get_grouped_transaction_aggregates(config: &Config, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe, - aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) - -> ApiServiceResult> { - let transactions: Vec = get_transactions(&config, &account_id, &timeframe) - .unwrap_or(vec![]); - - let mut month_year_grouped_transactions: Vec<(String, i64)> = - transactions.into_iter() - .group_by(|t| { - let transaction_date = parse_utc_date_from_transaction(&t); - match *interval { - Interval::Monthly => { - let group_name = transaction_date.format("%m-%Y").to_string(); - group_name - } +impl<'a> TellerClient<'a> { + pub fn new(auth_token: &'a str) -> TellerClient { + TellerClient { + auth_token: auth_token, + } + } + + fn get_body(&self, url: &str) -> ApiServiceResult { + let client = Client::new(); + let mut res = try!(client.get(url) + .header(get_auth_header(&self.auth_token)) + .send()); + if res.status.is_client_error() { + return Err(TellerClientError::AuthenticationError); + } + + let mut body = String::new(); + try!(res.read_to_string(&mut body)); + + debug!("GET {} response: {}", url, body); + + Ok(body) + } + + pub fn get_accounts(&self) -> ApiServiceResult> { + let body = try!(self.get_body(&format!("{}/accounts", TELLER_API_SERVER_URL))); + let accounts_response: AccountsResponse = try!(json::decode(&body)); + + Ok(accounts_response.data) + } + + pub fn get_account(&self, account_id: &str) -> ApiServiceResult { + let body = try!(self.get_body(&format!("{}/accounts/{}", TELLER_API_SERVER_URL, account_id))); + let account_response: AccountResponse = try!(json::decode(&body)); + + Ok(account_response.data) + } + + // TODO: INFORM: Move elsewhere. + pub fn get_account_balance(&self, account_id: &str) -> ApiServiceResult { + let to_money = |a: Account| Money::new(a.balance, a.currency); + self.get_account(&account_id).map(to_money) + } + + pub fn raw_transactions(&self, + account_id: &str, + count: u32, + page: u32) + -> ApiServiceResult> { + let mut url = Url::parse(&format!("{}/accounts/{}/transactions", + TELLER_API_SERVER_URL, + account_id)).unwrap(); + + const COUNT: &'static str = "count"; + const PAGE: &'static str = "page"; + let query = vec![(COUNT, count.to_string()), (PAGE, page.to_string())]; + url.set_query_from_pairs(query.into_iter()); + + let body = try!(self.get_body(&url.serialize())); + let transactions_response: TransactionsResponse = try!(json::decode(&body)); + + Ok(transactions_response.data) + } + + pub fn get_transactions(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult> { + let page_through_transactions = |from| -> ApiServiceResult> { + let mut all_transactions = vec![]; + + let mut fetching = true; + let mut page = 1; + let count = 250; + while fetching { + let mut transactions = try!(self.raw_transactions(&account_id, count, page)); + match transactions.last() { + None => { + // If there are no transactions left, do not fetch forever... + fetching = false + } + Some(past_transaction) => { + let past_transaction_date = parse_utc_date_from_transaction(&past_transaction); + if past_transaction_date < from { + fetching = false; } - }) - .map(aggregate_txs) - .collect(); - month_year_grouped_transactions.reverse(); + } + }; + + all_transactions.append(&mut transactions); + page = page + 1; + } + + all_transactions = all_transactions.into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + all_transactions.reverse(); + Ok(all_transactions) + }; - Ok(month_year_grouped_transactions) -} + match *timeframe { + Timeframe::ThreeMonths => { + let to = UTC::today(); + let from = to - Duration::days(91); // close enough... 😅 + + page_through_transactions(from) + } + Timeframe::SixMonths => { + let to = UTC::today(); + let from = to - Duration::days(183); + + page_through_transactions(from) + } + Timeframe::Year => { + let to = UTC::today(); + let from = to - Duration::days(365); + + page_through_transactions(from) + } + } + } -pub fn get_balances(config: &Config, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { - let sum_all = |myt: (String, Vec)| { - let to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + pub fn get_transactions_with_currency(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult { + let transactions = try!(self.get_transactions(&account_id, &timeframe)); + + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + Ok(TransactionsWithCurrrency::new(transactions, currency)) + } + + pub fn get_counterparties(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult { + let transactions_with_currency = try!(self.get_transactions_with_currency(&account_id, + &timeframe)); + + let to_cent_integer = |amount: &str| (f64::from_str(&amount).unwrap() * 100f64).round() as i64; + let from_cent_integer_to_float_string = |amount: &i64| { + format!("{:.2}", *amount as f64 / 100f64) }; - let group_name = myt.0; - let amount = myt.1.iter().map(to_cent_integer).fold(0i64, |sum, v| sum + v); - (group_name, amount) - }; + let transactions: Vec = transactions_with_currency.transactions + .into_iter() + .filter(|tx| { + to_cent_integer(&tx.amount) < + 0 + }) + .collect(); + let currency = transactions_with_currency.currency; + + let counterparty_to_date_amount_list = + convert_to_counterparty_to_date_amount_list(&transactions); + let sorted_counterparties = + counterparty_to_date_amount_list.into_iter() + .map(|(counterparty, date_amount_tuples)| { + let amount = + date_amount_tuples.iter().fold(0i64, |acc, dat| { + acc + to_cent_integer(&dat.1) + }); + (counterparty, amount.abs()) + }) + .sort_by(|&(_, amount_a), &(_, amount_b)| { + amount_a.cmp(&amount_b) + }); + let counterparties = sorted_counterparties.into_iter() + .map(|(counterparty, amount)| { + (counterparty, + from_cent_integer_to_float_string(&amount)) + }) + .collect(); + + Ok(CounterpartiesWithCurrrency::new(counterparties, currency)) + } + + fn get_grouped_transaction_aggregates(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe, + aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) + -> ApiServiceResult> { + let transactions: Vec = self.get_transactions(&account_id, &timeframe) + .unwrap_or(vec![]); + + let mut month_year_grouped_transactions: Vec<(String, i64)> = + transactions.into_iter() + .group_by(|t| { + let transaction_date = parse_utc_date_from_transaction(&t); + match *interval { + Interval::Monthly => { + let group_name = transaction_date.format("%m-%Y").to_string(); + group_name + } + } + }) + .map(aggregate_txs) + .collect(); + month_year_grouped_transactions.reverse(); + + Ok(month_year_grouped_transactions) + } + + pub fn get_balances(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { + let sum_all = |myt: (String, Vec)| { + let to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + + let group_name = myt.0; + let amount = myt.1.iter().map(to_cent_integer).fold(0i64, |sum, v| sum + v); + (group_name, amount) + }; + + let month_year_total_transactions = try!(self.get_grouped_transaction_aggregates(&account_id, + &interval, + &timeframe, + &sum_all)); + + let account = try!(self.get_account(&account_id)); + let current_balance = (f64::from_str(&account.balance).unwrap() * 100f64).round() as i64; + let currency = account.currency; + + let mut historical_amounts: Vec = vec![]; + historical_amounts.push(("current".to_string(), + format!("{:.2}", current_balance as f64 / 100f64))); + + let mut last_balance = current_balance; + for mytt in month_year_total_transactions { + last_balance = last_balance - mytt.1; + historical_amounts.push((mytt.0.to_string(), + format!("{:.2}", last_balance as f64 / 100f64))); + } + historical_amounts.reverse(); + + Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) + } + + pub fn get_outgoings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { + let sum_outgoings = |myt: (String, Vec)| { + let to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + + let group_name = myt.0; + let amount = myt.1 + .iter() + .map(to_cent_integer) + .filter(|ci| *ci < 0) + .fold(0i64, |sum, v| sum + v); + (group_name, amount) + }; - let month_year_total_transactions = try!(get_grouped_transaction_aggregates(&config, - &account_id, + let month_year_total_outgoing = try!(self.get_grouped_transaction_aggregates(&account_id, &interval, &timeframe, - &sum_all)); + &sum_outgoings)); - let account = try!(get_account(&config, &account_id)); - let current_balance = (f64::from_str(&account.balance).unwrap() * 100f64).round() as i64; - let currency = account.currency; + let account = try!(self.get_account(&account_id)); + let currency = account.currency; - let mut historical_amounts: Vec = vec![]; - historical_amounts.push(("current".to_string(), - format!("{:.2}", current_balance as f64 / 100f64))); + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - let mut last_balance = current_balance; - for mytt in month_year_total_transactions { - last_balance = last_balance - mytt.1; - historical_amounts.push((mytt.0.to_string(), - format!("{:.2}", last_balance as f64 / 100f64))); + let mut historical_amounts: Vec = vec![]; + for mytt in month_year_total_outgoing { + historical_amounts.push((mytt.0.to_string(), + from_cent_integer_to_float_string(mytt.1.abs()))); + } + historical_amounts.reverse(); + + Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } - historical_amounts.reverse(); - Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) -} + pub fn get_incomings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { + let sum_incomings = |myt: (String, Vec)| { + let to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; -pub fn get_outgoings(config: &Config, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { - let sum_outgoings = |myt: (String, Vec)| { - let to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + let group_name = myt.0; + let amount = myt.1 + .iter() + .map(to_cent_integer) + .filter(|ci| *ci > 0) + .fold(0i64, |sum, v| sum + v); + (group_name, amount) }; - let group_name = myt.0; - let amount = myt.1 - .iter() - .map(to_cent_integer) - .filter(|ci| *ci < 0) - .fold(0i64, |sum, v| sum + v); - (group_name, amount) - }; - - let month_year_total_outgoing = try!(get_grouped_transaction_aggregates(&config, - &account_id, - &interval, - &timeframe, - &sum_outgoings)); - - let account = try!(get_account(&config, &account_id)); - let currency = account.currency; - - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let mut historical_amounts: Vec = vec![]; - for mytt in month_year_total_outgoing { - historical_amounts.push((mytt.0.to_string(), - from_cent_integer_to_float_string(mytt.1.abs()))); - } - historical_amounts.reverse(); + let month_year_total_incoming = try!(self.get_grouped_transaction_aggregates(&account_id, + &interval, + &timeframe, + &sum_incomings)); - Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) -} + let account = try!(self.get_account(&account_id)); + let currency = account.currency; -pub fn get_incomings(config: &Config, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { - let sum_incomings = |myt: (String, Vec)| { - let to_cent_integer = |t: &Transaction| { + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); + + let mut historical_amounts: Vec = vec![]; + for mytt in month_year_total_incoming { + historical_amounts.push((mytt.0.to_string(), + from_cent_integer_to_float_string(mytt.1))); + } + historical_amounts.reverse(); + + Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) + } + + pub fn get_outgoing(&self, account_id: &str) -> ApiServiceResult { + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from = UTC::today().with_day(1).unwrap(); + let transactions: Vec = self.raw_transactions(&account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + let from_float_string_to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - let group_name = myt.0; - let amount = myt.1 - .iter() - .map(to_cent_integer) - .filter(|ci| *ci > 0) - .fold(0i64, |sum, v| sum + v); - (group_name, amount) - }; - - let month_year_total_incoming = try!(get_grouped_transaction_aggregates(&config, - &account_id, - &interval, - &timeframe, - &sum_incomings)); - - let account = try!(get_account(&config, &account_id)); - let currency = account.currency; - - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let mut historical_amounts: Vec = vec![]; - for mytt in month_year_total_incoming { - historical_amounts.push((mytt.0.to_string(), - from_cent_integer_to_float_string(mytt.1))); + let outgoing = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci < 0) + .fold(0i64, |sum, v| sum + v); + + Ok(Money::new(from_cent_integer_to_float_string(outgoing.abs()), currency)) } - historical_amounts.reverse(); - Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) -} + pub fn get_incoming(&self, account_id: &str) -> ApiServiceResult { + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from = UTC::today().with_day(1).unwrap(); + let transactions: Vec = self.raw_transactions(&account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + let from_float_string_to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); -pub fn get_outgoing(config: &Config, account_id: &str) -> ApiServiceResult { - let account = try!(get_account(&config, &account_id)); - let currency = account.currency; - - let from = UTC::today().with_day(1).unwrap(); - let transactions: Vec = raw_transactions(&config, &account_id, 250, 1) - .unwrap_or(vec![]) - .into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - let from_float_string_to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let outgoing = transactions.iter() - .map(from_float_string_to_cent_integer) - .filter(|ci| *ci < 0) - .fold(0i64, |sum, v| sum + v); - - Ok(Money::new(from_cent_integer_to_float_string(outgoing.abs()), currency)) -} + let incoming = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci > 0) + .fold(0i64, |sum, v| sum + v); + + Ok(Money::new(from_cent_integer_to_float_string(incoming), currency)) + } -pub fn get_incoming(config: &Config, account_id: &str) -> ApiServiceResult { - let account = try!(get_account(&config, &account_id)); - let currency = account.currency; - - let from = UTC::today().with_day(1).unwrap(); - let transactions: Vec = raw_transactions(&config, &account_id, 250, 1) - .unwrap_or(vec![]) - .into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - let from_float_string_to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let incoming = transactions.iter() - .map(from_float_string_to_cent_integer) - .filter(|ci| *ci > 0) - .fold(0i64, |sum, v| sum + v); - - Ok(Money::new(from_cent_integer_to_float_string(incoming), currency)) } diff --git a/src/command/initialise.rs b/src/command/initialise.rs index 6dc766e..d646e5b 100644 --- a/src/command/initialise.rs +++ b/src/command/initialise.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use config::{Config, get_config_path, get_config_file_to_write, write_config}; use inquirer::{Question, Answer, ask_question, ask_questions}; -use client::get_accounts; +use client::TellerClient; use super::representations::represent_list_accounts; pub fn configure_cli(config_file_path: &PathBuf) -> Option { @@ -29,9 +29,12 @@ fn ask_questions_for_config() -> Option { let mut config = Config::new_with_auth_token_only(auth_token_answer.value); print!("\n"); - let accounts = match get_accounts(&config) { - Ok(accounts) => accounts, - Err(e) => panic!("Unable to list accounts: {}", e), + let accounts = { + let teller = TellerClient::new(&config.auth_token); + match teller.get_accounts() { + Ok(accounts) => accounts, + Err(e) => panic!("Unable to list accounts: {}", e), + } }; represent_list_accounts(&accounts, &config); diff --git a/src/command/list_accounts.rs b/src/command/list_accounts.rs index d0f7761..157e4e0 100644 --- a/src/command/list_accounts.rs +++ b/src/command/list_accounts.rs @@ -1,17 +1,18 @@ use config::Config; -use client::get_accounts; +use client::TellerClient; use super::representations::represent_list_accounts; pub fn list_accounts_command(config: &Config) -> i32 { info!("Calling the list accounts command"); - get_accounts(&config) - .map(|accounts| { - represent_list_accounts(&accounts, &config); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to list accounts: {}", err); - 1 - }) + let teller = TellerClient::new(&config.auth_token); + teller.get_accounts() + .map(|accounts| { + represent_list_accounts(&accounts, &config); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list accounts: {}", err); + 1 + }) } diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index 9203f31..dc3f224 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -1,5 +1,5 @@ use config::Config; -use client::{Balances, get_balances}; +use client::{Balances, TellerClient}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; @@ -16,7 +16,8 @@ pub fn list_balances_command(config: &Config, -> i32 { info!("Calling the list balances command"); let account_id = config.get_account_id(&account); - get_balances(&config, &account_id, &interval, &timeframe) + let teller = TellerClient::new(&config.auth_token); + teller.get_balances(&account_id, &interval, &timeframe) .map(|balances| { represent_list_balances(&balances, &output); 0 diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index 3f47052..7728f67 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -1,5 +1,5 @@ use config::Config; -use client::get_counterparties; +use client::TellerClient; use cli::arg_types::{AccountType, Timeframe}; use std::io::Write; @@ -37,7 +37,8 @@ pub fn list_counterparties_command(config: &Config, -> i32 { info!("Calling the list counterparties command"); let account_id = config.get_account_id(&account); - get_counterparties(&config, &account_id, &timeframe) + let teller = TellerClient::new(&config.auth_token); + teller.get_counterparties(&account_id, &timeframe) .map(|counterparties_with_currency| { represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index a47aa8b..0ea117f 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -1,5 +1,5 @@ use config::Config; -use client::{Incomings, get_incomings}; +use client::{Incomings, TellerClient}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; @@ -16,7 +16,8 @@ pub fn list_incomings_command(config: &Config, -> i32 { info!("Calling the list incomings command"); let account_id = config.get_account_id(&account); - get_incomings(&config, &account_id, &interval, &timeframe) + let teller = TellerClient::new(&config.auth_token); + teller.get_incomings(&account_id, &interval, &timeframe) .map(|incomings| { represent_list_incomings(&incomings, &output); 0 diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index 751d223..c9c40ae 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -1,5 +1,5 @@ use config::Config; -use client::{Outgoings, get_outgoings}; +use client::{Outgoings, TellerClient}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; @@ -16,7 +16,8 @@ pub fn list_outgoings_command(config: &Config, -> i32 { info!("Calling the list outgoings command"); let account_id = config.get_account_id(&account); - get_outgoings(&config, &account_id, &interval, &timeframe) + let teller = TellerClient::new(&config.auth_token); + teller.get_outgoings(&account_id, &interval, &timeframe) .map(|outgoings| { represent_list_outgoings(&outgoings, &output); 0 diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index a66d2c8..e5302e4 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -1,5 +1,5 @@ use config::Config; -use client::{Transaction, get_transactions_with_currency}; +use client::{Transaction, TransactionsWithCurrrency, TellerClient}; use cli::arg_types::{AccountType, Timeframe}; use std::io::Write; @@ -53,11 +53,11 @@ pub fn list_transactions_command(config: &Config, -> i32 { info!("Calling the list transactions command"); let account_id = config.get_account_id(&account); - get_transactions_with_currency(&config, &account_id, &timeframe) + let teller = TellerClient::new(&config.auth_token); + teller.get_transactions_with_currency(&account_id, &timeframe) .map(|transactions_with_currency| { - represent_list_transactions(&transactions_with_currency.transactions, - &transactions_with_currency.currency, - &show_description); + let TransactionsWithCurrrency { transactions, currency } = transactions_with_currency; + represent_list_transactions(&transactions, ¤cy, &show_description); 0 }) .unwrap_or_else(|err| { diff --git a/src/command/show_balance.rs b/src/command/show_balance.rs index 2e5ac49..8430fce 100644 --- a/src/command/show_balance.rs +++ b/src/command/show_balance.rs @@ -1,4 +1,4 @@ -use client::get_account_balance; +use client::TellerClient; use config::Config; use cli::arg_types::AccountType; use client::Money; @@ -11,13 +11,14 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { pub fn show_balance_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show balance command"); let account_id = config.get_account_id(&account); - get_account_balance(&config, &account_id) - .map(|balance| { - represent_money(&balance, &hide_currency); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to get account balance: {}", err); - 1 - }) + let teller = TellerClient::new(&config.auth_token); + teller.get_account_balance(&account_id) + .map(|balance| { + represent_money(&balance, &hide_currency); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to get account balance: {}", err); + 1 + }) } diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs index bb37da7..8aa910c 100644 --- a/src/command/show_incoming.rs +++ b/src/command/show_incoming.rs @@ -1,4 +1,4 @@ -use client::get_incoming; +use client::TellerClient; use config::Config; use cli::arg_types::AccountType; use client::Money; @@ -11,7 +11,8 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { pub fn show_incoming_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show incoming command"); let account_id = config.get_account_id(&account); - get_incoming(&config, &account_id) + let teller = TellerClient::new(&config.auth_token); + teller.get_incoming(&account_id) .map(|incoming| { represent_money(&incoming, &hide_currency); 0 diff --git a/src/command/show_outgoing.rs b/src/command/show_outgoing.rs index de25caa..d4d2666 100644 --- a/src/command/show_outgoing.rs +++ b/src/command/show_outgoing.rs @@ -1,4 +1,4 @@ -use client::get_outgoing; +use client::TellerClient; use config::Config; use cli::arg_types::AccountType; use client::Money; @@ -11,13 +11,14 @@ fn represent_money(money_with_currency: &Money, hide_currency: &bool) { pub fn show_outgoing_command(config: &Config, account: &AccountType, hide_currency: &bool) -> i32 { info!("Calling the show outgoing command"); let account_id = config.get_account_id(&account); - get_outgoing(&config, &account_id) - .map(|outgoing| { - represent_money(&outgoing, &hide_currency); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to get outgoing: {}", err); - 1 - }) + let teller = TellerClient::new(&config.auth_token); + teller.get_outgoing(&account_id) + .map(|outgoing| { + represent_money(&outgoing, &hide_currency); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to get outgoing: {}", err); + 1 + }) } From 18bdf3e37fb21545098ea2155564c31549431cdb Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Fri, 1 Jan 2016 17:16:15 +0000 Subject: [PATCH 10/16] refactor(tabwriter): less redundancy --- TODO.md | 5 +- src/command/list_balances.rs | 16 +++---- src/command/list_counterparties.rs | 29 +++++------- src/command/list_incomings.rs | 16 +++---- src/command/list_outgoings.rs | 16 +++---- src/command/list_transactions.rs | 9 +--- src/command/representations.rs | 22 +++++---- src/command/show_incoming.rs | 16 +++---- src/main.rs | 73 ++++++++++-------------------- 9 files changed, 85 insertions(+), 117 deletions(-) diff --git a/TODO.md b/TODO.md index 85e536c..4fe0e48 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). -- [ ] Carefully [remove many of the `unwrap` statements](https://github.com/Manishearth/rust-clippy/issues/24) and clean up many of the deeply-nested matches in the usual ways (separate functions, `unwrap_*`, early returns, `let expected_value = match thing { ... }`. - [ ] Data types from within TellerClient should be encapsulated depending on what they belong to. -- [ ] The remaining non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and receive the data from the API instead of creating it (the information that is applied, does not belong to the client). +- [ ] Non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and + receive the data from the API instead of creating it. Information applied + does not belong to the client. - [ ] Move client to a separate crate `teller_api`. diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index dc3f224..c2f8920 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -18,12 +18,12 @@ pub fn list_balances_command(config: &Config, let account_id = config.get_account_id(&account); let teller = TellerClient::new(&config.auth_token); teller.get_balances(&account_id, &interval, &timeframe) - .map(|balances| { - represent_list_balances(&balances, &output); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to list balances: {}", err); - 1 - }) + .map(|balances| { + represent_list_balances(&balances, &output); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list balances: {}", err); + 1 + }) } diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index 7728f67..ecb4dbf 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -2,8 +2,7 @@ use config::Config; use client::TellerClient; use cli::arg_types::{AccountType, Timeframe}; -use std::io::Write; -use tabwriter::TabWriter; +use super::representations::to_aligned_table; fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currency: &str, @@ -21,11 +20,7 @@ fn represent_list_counterparties(counterparties: &Vec<(String, String)>, counterparties_table = counterparties_table + &new_counterparty_row; } - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", counterparties_table).unwrap(); - tw.flush().unwrap(); - - let counterparties_str = String::from_utf8(tw.unwrap()).unwrap(); + let counterparties_str = to_aligned_table(&counterparties_table); println!("{}", counterparties_str) } @@ -39,14 +34,14 @@ pub fn list_counterparties_command(config: &Config, let account_id = config.get_account_id(&account); let teller = TellerClient::new(&config.auth_token); teller.get_counterparties(&account_id, &timeframe) - .map(|counterparties_with_currency| { - represent_list_counterparties(&counterparties_with_currency.counterparties, - &counterparties_with_currency.currency, - &count); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to list counterparties: {}", err); - 1 - }) + .map(|counterparties_with_currency| { + represent_list_counterparties(&counterparties_with_currency.counterparties, + &counterparties_with_currency.currency, + &count); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list counterparties: {}", err); + 1 + }) } diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index 0ea117f..1336180 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -18,12 +18,12 @@ pub fn list_incomings_command(config: &Config, let account_id = config.get_account_id(&account); let teller = TellerClient::new(&config.auth_token); teller.get_incomings(&account_id, &interval, &timeframe) - .map(|incomings| { - represent_list_incomings(&incomings, &output); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to list incomings: {}", err); - 1 - }) + .map(|incomings| { + represent_list_incomings(&incomings, &output); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list incomings: {}", err); + 1 + }) } diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index c9c40ae..19bd1d4 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -18,12 +18,12 @@ pub fn list_outgoings_command(config: &Config, let account_id = config.get_account_id(&account); let teller = TellerClient::new(&config.auth_token); teller.get_outgoings(&account_id, &interval, &timeframe) - .map(|outgoings| { - represent_list_outgoings(&outgoings, &output); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to list outgoings: {}", err); - 1 - }) + .map(|outgoings| { + represent_list_outgoings(&outgoings, &output); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to list outgoings: {}", err); + 1 + }) } diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index e5302e4..ea277ef 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -2,8 +2,7 @@ use config::Config; use client::{Transaction, TransactionsWithCurrrency, TellerClient}; use cli::arg_types::{AccountType, Timeframe}; -use std::io::Write; -use tabwriter::TabWriter; +use super::representations::to_aligned_table; fn represent_list_transactions(transactions: &Vec, currency: &str, @@ -37,11 +36,7 @@ fn represent_list_transactions(transactions: &Vec, } } - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", transactions_table).unwrap(); - tw.flush().unwrap(); - - let transactions_str = String::from_utf8(tw.unwrap()).unwrap(); + let transactions_str = to_aligned_table(&transactions_table); println!("{}", transactions_str) } diff --git a/src/command/representations.rs b/src/command/representations.rs index 888ca7c..5af6556 100644 --- a/src/command/representations.rs +++ b/src/command/representations.rs @@ -5,6 +5,16 @@ use config::Config; use client::{HistoricalAmountsWithCurrency, Account}; use cli::arg_types::OutputFormat; +pub fn to_aligned_table(table_str: &str) -> String { + let mut tw = TabWriter::new(Vec::new()); + write!(&mut tw, "{}", table_str).unwrap(); + tw.flush().unwrap(); + + let aligned_table_str = String::from_utf8(tw.unwrap()).unwrap(); + + aligned_table_str +} + pub fn represent_list_accounts(accounts: &Vec, config: &Config) { let mut accounts_table = String::new(); accounts_table.push_str("row\taccount no.\tbalance\n"); @@ -20,11 +30,7 @@ pub fn represent_list_accounts(accounts: &Vec, config: &Config) { accounts_table = accounts_table + &new_account_row; } - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", accounts_table).unwrap(); - tw.flush().unwrap(); - - let accounts_str = String::from_utf8(tw.unwrap()).unwrap(); + let accounts_str = to_aligned_table(&accounts_table); println!("{}", accounts_str) } @@ -55,11 +61,7 @@ pub fn represent_list_amounts(amount_type: &str, hac_table = hac_table + &new_amount; } - let mut tw = TabWriter::new(Vec::new()); - write!(&mut tw, "{}", hac_table).unwrap(); - tw.flush().unwrap(); - - let hac_str = String::from_utf8(tw.unwrap()).unwrap(); + let hac_str = to_aligned_table(&hac_table); println!("{}", hac_str) } diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs index 8aa910c..34d79c6 100644 --- a/src/command/show_incoming.rs +++ b/src/command/show_incoming.rs @@ -13,12 +13,12 @@ pub fn show_incoming_command(config: &Config, account: &AccountType, hide_curren let account_id = config.get_account_id(&account); let teller = TellerClient::new(&config.auth_token); teller.get_incoming(&account_id) - .map(|incoming| { - represent_money(&incoming, &hide_currency); - 0 - }) - .unwrap_or_else(|err| { - error!("Unable to get incoming: {}", err); - 1 - }) + .map(|incoming| { + represent_money(&incoming, &hide_currency); + 0 + }) + .unwrap_or_else(|err| { + error!("Unable to get incoming: {}", err); + 1 + }) } diff --git a/src/main.rs b/src/main.rs index de6f4fb..92ebc58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,63 +25,38 @@ const USAGE: &'static str = "Banking for the command line. Usage: teller init - teller \ - [list] accounts - teller [list] transactions [ \ - --timeframe= --show-description] - teller [list] \ - counterparties [ --timeframe= --count=] - teller \ - [list] (balances|outgoings|incomings) [ --interval= \ - --timeframe= --output=] - teller [show] balance [ \ - --hide-currency] - teller [show] outgoing [ \ - --hide-currency] - teller [show] incoming [ \ - --hide-currency] + teller [list] accounts + teller [list] transactions [ --timeframe= --show-description] + teller [list] counterparties [ --timeframe= --count=] + teller [list] (balances|outgoings|incomings) [ --interval= --timeframe= --output=] + teller [show] balance [ --hide-currency] + teller [show] outgoing [ --hide-currency] + teller [show] incoming [ --hide-currency] teller [--help | --version] Commands: - init \ - Configure. + init Configure. list accounts List accounts. - list \ - transactions List transactions. - list counterparties \ - List outgoing amounts grouped by counterparties. - list balances \ - List balances during a timeframe. - list outgoings List \ - outgoings during a timeframe. - list incomings List \ - incomings during a timeframe. - show balance Show the \ - current balance. - show outgoing Show the current \ - outgoing. + list transactions List transactions. + list counterparties List outgoing amounts grouped by counterparties. + list balances List balances during a timeframe. + list outgoings List outgoings during a timeframe. + list incomings List incomings during a timeframe. + show balance Show the current balance. + show outgoing Show the current outgoing. show incoming Show the current incoming. - \ - NOTE: By default commands are applied to the 'current' . + NOTE: By default commands are applied to the 'current' . -\ - Options: +Options: -h --help Show this screen. - -V \ - --version Show version. - -i --interval= Group \ - by an interval of time [default: monthly]. - -t --timeframe= \ - Operate upon a named period of time [default: 6-months]. - -c \ - --count= Only the top N elements [default: 10]. - -d \ - --show-description Show descriptions against transactions. - -c \ - --hide-currency Show money without currency codes. - -o \ - --output= Output in a particular format (e.g. spark). + -V --version Show version. + -i --interval= Group by an interval of time [default: monthly]. + -t --timeframe= Operate upon a named period of time [default: 6-months]. + -c --count= Only the top N elements [default: 10]. + -d --show-description Show descriptions against transactions. + -c --hide-currency Show money without currency codes. + -o --output= Output in a particular format (e.g. spark). "; fn main() { From fa736c657941d188728853a9271d91a8de009182 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Tue, 5 Jan 2016 10:51:17 +0000 Subject: [PATCH 11/16] docs(new-idea): show current balance in OSX menu bar --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d5b481..60a233b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This tool provides useful ways of interrogating your bank through your command line, and is not merely meant to be a one-to-one match with underlying APIs. -It uses [Teller](http://teller.io) behind-the-scenes to interact with your UK bank, so you will need to have an account there. +It uses [Teller](http://teller.io) behind-the-scenes to interact with your UK bank, so you will need to have an account there. Want an account? [@stevegraham can hook you up!](https://twitter.com/stevegraham) **:point_up_2: Heads up!** (1) This is my first [Rust](https://www.rust-lang.org/) project, (2) the interface is in flux while I try to make it human-like without ending up redundant, and (3) there are [no tests yet](https://github.com/sebinsua/teller-cli/issues/1)! @@ -82,6 +82,23 @@ then fi ``` +#### Show your current balance and last transaction in the OSX Menu Bar with [Bitbar](https://github.com/matryer/bitbar) + +`show-current-balance.1h.sh` +```sh +#!/bin/sh +export PATH='/usr/local/bin:/usr/bin/:$PATH'; + +CURRENT_BALANCE=`teller show balance current --hide-currency`; +LAST_TRANSACTION=`teller list transactions | tail -n 1 | pcregrep -o1 "[0-9]+[ ]+(.*)"`; + +echo "£$CURRENT_BALANCE"; +echo "---"; +echo "$LAST_TRANSACTION"; +``` + +![Current Balance in OSX Menu Bar](http://i.imgur.com/BzkazSB.png) + ## Installation ### From release From 0162bd50fcfebb5e739782eb264250dcd405906e Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Tue, 5 Jan 2016 11:59:30 +0000 Subject: [PATCH 12/16] refactor(api): client and inform split --- TODO.md | 7 - src/api/client.rs | 184 +++++++++++++ src/{client => api}/error.rs | 0 src/{client/mod.rs => api/inform.rs} | 395 ++++++++++----------------- src/api/mod.rs | 6 + src/command/initialise.rs | 2 +- src/command/list_accounts.rs | 2 +- src/command/list_balances.rs | 3 +- src/command/list_counterparties.rs | 3 +- src/command/list_incomings.rs | 3 +- src/command/list_outgoings.rs | 3 +- src/command/list_transactions.rs | 3 +- src/command/representations.rs | 2 +- src/command/show_balance.rs | 4 +- src/command/show_incoming.rs | 4 +- src/command/show_outgoing.rs | 4 +- src/main.rs | 2 +- 17 files changed, 348 insertions(+), 279 deletions(-) delete mode 100644 TODO.md create mode 100644 src/api/client.rs rename src/{client => api}/error.rs (100%) rename src/{client/mod.rs => api/inform.rs} (71%) create mode 100644 src/api/mod.rs diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 4fe0e48..0000000 --- a/TODO.md +++ /dev/null @@ -1,7 +0,0 @@ -Take a read of [this well-written rust code](https://www.reddit.com/r/rust/comments/2pmaqz/well_written_rust_code_to_read_and_learn_from/). - -- [ ] Data types from within TellerClient should be encapsulated depending on what they belong to. -- [ ] Non-HTTP parts of `'client'` should perhaps be renamed to `'inform'` and - receive the data from the API instead of creating it. Information applied - does not belong to the client. -- [ ] Move client to a separate crate `teller_api`. diff --git a/src/api/client.rs b/src/api/client.rs new file mode 100644 index 0000000..5732c3e --- /dev/null +++ b/src/api/client.rs @@ -0,0 +1,184 @@ +use cli::arg_types::Timeframe; + +use hyper::{Client, Url}; +use hyper::header::{Authorization, Bearer}; +use rustc_serialize::json; +use chrono::{Date, DateTime, UTC}; +use chrono::duration::Duration; + +use std::io::prelude::*; // Required for read_to_string use later. + +use super::error::TellerClientError; + +pub type ApiServiceResult = Result; + +#[derive(Debug, RustcDecodable)] +struct AccountResponse { + data: Account, +} + +#[derive(Debug, RustcDecodable)] +struct AccountsResponse { + data: Vec, +} + +#[derive(Debug, RustcDecodable)] +struct TransactionsResponse { + data: Vec, +} + +#[derive(Debug, RustcDecodable)] +pub struct Account { + pub updated_at: String, + pub institution: String, + pub id: String, + pub currency: String, + pub balance: String, + pub account_number_last_4: String, +} + +#[derive(Debug, RustcDecodable)] +pub struct Transaction { + pub description: String, + pub date: String, + pub counterparty: String, + pub amount: String, +} + +pub fn parse_utc_date_from_transaction(t: &Transaction) -> Date { + let full_date = &(t.date.to_owned() + "T00:00:00-00:00"); + let past_transaction_date_without_tz = DateTime::parse_from_rfc3339(full_date).unwrap().date(); + let past_transaction_date = past_transaction_date_without_tz.with_timezone(&UTC); + past_transaction_date +} + +const TELLER_API_SERVER_URL: &'static str = "https://api.teller.io"; + +pub struct TellerClient<'a> { + auth_token: &'a str, +} + +impl<'a> TellerClient<'a> { + pub fn new(auth_token: &'a str) -> TellerClient { + TellerClient { + auth_token: auth_token, + } + } + + fn get_body(&self, url: &str) -> ApiServiceResult { + let client = Client::new(); + let mut res = try!(client.get(url) + .header(Authorization( + Bearer { token: self.auth_token.to_string() } + )) + .send()); + if res.status.is_client_error() { + return Err(TellerClientError::AuthenticationError); + } + + let mut body = String::new(); + try!(res.read_to_string(&mut body)); + + debug!("GET {} response: {}", url, body); + + Ok(body) + } + + pub fn get_accounts(&self) -> ApiServiceResult> { + let body = try!(self.get_body(&format!("{}/accounts", TELLER_API_SERVER_URL))); + let accounts_response: AccountsResponse = try!(json::decode(&body)); + + Ok(accounts_response.data) + } + + pub fn get_account(&self, account_id: &str) -> ApiServiceResult { + let body = try!(self.get_body(&format!("{}/accounts/{}", TELLER_API_SERVER_URL, account_id))); + let account_response: AccountResponse = try!(json::decode(&body)); + + Ok(account_response.data) + } + + pub fn raw_transactions(&self, + account_id: &str, + count: u32, + page: u32) + -> ApiServiceResult> { + let mut url = Url::parse(&format!("{}/accounts/{}/transactions", + TELLER_API_SERVER_URL, + account_id)).unwrap(); + + const COUNT: &'static str = "count"; + const PAGE: &'static str = "page"; + let query = vec![(COUNT, count.to_string()), (PAGE, page.to_string())]; + url.set_query_from_pairs(query.into_iter()); + + let body = try!(self.get_body(&url.serialize())); + let transactions_response: TransactionsResponse = try!(json::decode(&body)); + + Ok(transactions_response.data) + } + + pub fn get_transactions(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult> { + let page_through_transactions = |from| -> ApiServiceResult> { + let mut all_transactions = vec![]; + + let mut fetching = true; + let mut page = 1; + let count = 250; + while fetching { + let mut transactions = try!(self.raw_transactions(&account_id, count, page)); + match transactions.last() { + None => { + // If there are no transactions left, do not fetch forever... + fetching = false + } + Some(past_transaction) => { + let past_transaction_date = parse_utc_date_from_transaction(&past_transaction); + if past_transaction_date < from { + fetching = false; + } + } + }; + + all_transactions.append(&mut transactions); + page = page + 1; + } + + all_transactions = all_transactions.into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + all_transactions.reverse(); + Ok(all_transactions) + }; + + match *timeframe { + Timeframe::ThreeMonths => { + let to = UTC::today(); + let from = to - Duration::days(91); // close enough... 😅 + + page_through_transactions(from) + } + Timeframe::SixMonths => { + let to = UTC::today(); + let from = to - Duration::days(183); + + page_through_transactions(from) + } + Timeframe::Year => { + let to = UTC::today(); + let from = to - Duration::days(365); + + page_through_transactions(from) + } + } + } + +} diff --git a/src/client/error.rs b/src/api/error.rs similarity index 100% rename from src/client/error.rs rename to src/api/error.rs diff --git a/src/client/mod.rs b/src/api/inform.rs similarity index 71% rename from src/client/mod.rs rename to src/api/inform.rs index cacc811..df8c062 100644 --- a/src/client/mod.rs +++ b/src/api/inform.rs @@ -1,22 +1,14 @@ -pub mod error; - use cli::arg_types::{Interval, Timeframe}; -use hyper::{Client, Url}; -use hyper::header::{Authorization, Bearer}; -use rustc_serialize::json; -use chrono::{Date, DateTime, UTC, Datelike}; -use chrono::duration::Duration; +use chrono::{UTC, Datelike}; use itertools::Itertools; use std::collections::HashMap; -use std::io::prelude::*; // Required for read_to_string use later. -use std::str::FromStr; - -use self::error::TellerClientError; +use std::str::FromStr; // Use of #from_str. -pub type ApiServiceResult = Result; +use super::client::{TellerClient, ApiServiceResult, Transaction, Account}; +use super::client::parse_utc_date_from_transaction; // TODO: Ew. bad bad bad. pub type IntervalAmount = (String, String); pub type Balances = HistoricalAmountsWithCurrency; @@ -24,39 +16,6 @@ pub type Outgoings = HistoricalAmountsWithCurrency; pub type Incomings = HistoricalAmountsWithCurrency; type DateStringToTransactions = (String, Vec); -#[derive(Debug, RustcDecodable)] -struct AccountResponse { - data: Account, -} - -#[derive(Debug, RustcDecodable)] -struct AccountsResponse { - data: Vec, -} - -#[derive(Debug, RustcDecodable)] -struct TransactionsResponse { - data: Vec, -} - -#[derive(Debug, RustcDecodable)] -pub struct Account { - pub updated_at: String, - pub institution: String, - pub id: String, - pub currency: String, - pub balance: String, - pub account_number_last_4: String, -} - -#[derive(Debug, RustcDecodable)] -pub struct Transaction { - pub description: String, - pub date: String, - pub counterparty: String, - pub amount: String, -} - #[derive(Debug)] pub struct HistoricalAmountsWithCurrency { pub historical_amounts: Vec, @@ -132,15 +91,110 @@ impl Money { } } -fn get_auth_header(auth_token: &str) -> Authorization { - Authorization(Bearer { token: auth_token.to_string() }) +pub trait GetAccountBalance { + fn get_account_balance(&self, account_id: &str) -> ApiServiceResult; +} + +impl<'a> GetAccountBalance for TellerClient<'a> { + fn get_account_balance(&self, account_id: &str) -> ApiServiceResult { + let to_money = |a: Account| Money::new(a.balance, a.currency); + self.get_account(&account_id).map(to_money) + } +} + +pub trait GetIncoming { + fn get_incoming(&self, account_id: &str) -> ApiServiceResult; +} + +impl<'a> GetIncoming for TellerClient<'a> { + fn get_incoming(&self, account_id: &str) -> ApiServiceResult { + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from = UTC::today().with_day(1).unwrap(); + let transactions: Vec = self.raw_transactions(&account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + let from_float_string_to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); + + let incoming = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci > 0) + .fold(0i64, |sum, v| sum + v); + + Ok(Money::new(from_cent_integer_to_float_string(incoming), currency)) + } +} + +pub trait GetOutgoing { + fn get_outgoing(&self, account_id: &str) -> ApiServiceResult; } -fn parse_utc_date_from_transaction(t: &Transaction) -> Date { - let full_date = &(t.date.to_owned() + "T00:00:00-00:00"); - let past_transaction_date_without_tz = DateTime::parse_from_rfc3339(full_date).unwrap().date(); - let past_transaction_date = past_transaction_date_without_tz.with_timezone(&UTC); - past_transaction_date +impl<'a> GetOutgoing for TellerClient<'a> { + fn get_outgoing(&self, account_id: &str) -> ApiServiceResult { + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from = UTC::today().with_day(1).unwrap(); + let transactions: Vec = self.raw_transactions(&account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + let from_float_string_to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); + + let outgoing = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci < 0) + .fold(0i64, |sum, v| sum + v); + + Ok(Money::new(from_cent_integer_to_float_string(outgoing.abs()), currency)) + } +} + +pub trait GetTransactionsWithCurrency { + fn get_transactions_with_currency(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult; +} + +impl<'a> GetTransactionsWithCurrency for TellerClient<'a> { + fn get_transactions_with_currency(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult { + let transactions = try!(self.get_transactions(&account_id, &timeframe)); + + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + Ok(TransactionsWithCurrrency::new(transactions, currency)) + } +} + +pub trait GetCounterparties { + fn get_counterparties(&self, + account_id: &str, + timeframe: &Timeframe) -> ApiServiceResult; } fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec) @@ -172,152 +226,8 @@ fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec { - auth_token: &'a str, -} - -impl<'a> TellerClient<'a> { - pub fn new(auth_token: &'a str) -> TellerClient { - TellerClient { - auth_token: auth_token, - } - } - - fn get_body(&self, url: &str) -> ApiServiceResult { - let client = Client::new(); - let mut res = try!(client.get(url) - .header(get_auth_header(&self.auth_token)) - .send()); - if res.status.is_client_error() { - return Err(TellerClientError::AuthenticationError); - } - - let mut body = String::new(); - try!(res.read_to_string(&mut body)); - - debug!("GET {} response: {}", url, body); - - Ok(body) - } - - pub fn get_accounts(&self) -> ApiServiceResult> { - let body = try!(self.get_body(&format!("{}/accounts", TELLER_API_SERVER_URL))); - let accounts_response: AccountsResponse = try!(json::decode(&body)); - - Ok(accounts_response.data) - } - - pub fn get_account(&self, account_id: &str) -> ApiServiceResult { - let body = try!(self.get_body(&format!("{}/accounts/{}", TELLER_API_SERVER_URL, account_id))); - let account_response: AccountResponse = try!(json::decode(&body)); - - Ok(account_response.data) - } - - // TODO: INFORM: Move elsewhere. - pub fn get_account_balance(&self, account_id: &str) -> ApiServiceResult { - let to_money = |a: Account| Money::new(a.balance, a.currency); - self.get_account(&account_id).map(to_money) - } - - pub fn raw_transactions(&self, - account_id: &str, - count: u32, - page: u32) - -> ApiServiceResult> { - let mut url = Url::parse(&format!("{}/accounts/{}/transactions", - TELLER_API_SERVER_URL, - account_id)).unwrap(); - - const COUNT: &'static str = "count"; - const PAGE: &'static str = "page"; - let query = vec![(COUNT, count.to_string()), (PAGE, page.to_string())]; - url.set_query_from_pairs(query.into_iter()); - - let body = try!(self.get_body(&url.serialize())); - let transactions_response: TransactionsResponse = try!(json::decode(&body)); - - Ok(transactions_response.data) - } - - pub fn get_transactions(&self, - account_id: &str, - timeframe: &Timeframe) - -> ApiServiceResult> { - let page_through_transactions = |from| -> ApiServiceResult> { - let mut all_transactions = vec![]; - - let mut fetching = true; - let mut page = 1; - let count = 250; - while fetching { - let mut transactions = try!(self.raw_transactions(&account_id, count, page)); - match transactions.last() { - None => { - // If there are no transactions left, do not fetch forever... - fetching = false - } - Some(past_transaction) => { - let past_transaction_date = parse_utc_date_from_transaction(&past_transaction); - if past_transaction_date < from { - fetching = false; - } - } - }; - - all_transactions.append(&mut transactions); - page = page + 1; - } - - all_transactions = all_transactions.into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - all_transactions.reverse(); - Ok(all_transactions) - }; - - match *timeframe { - Timeframe::ThreeMonths => { - let to = UTC::today(); - let from = to - Duration::days(91); // close enough... 😅 - - page_through_transactions(from) - } - Timeframe::SixMonths => { - let to = UTC::today(); - let from = to - Duration::days(183); - - page_through_transactions(from) - } - Timeframe::Year => { - let to = UTC::today(); - let from = to - Duration::days(365); - - page_through_transactions(from) - } - } - } - - pub fn get_transactions_with_currency(&self, - account_id: &str, - timeframe: &Timeframe) - -> ApiServiceResult { - let transactions = try!(self.get_transactions(&account_id, &timeframe)); - - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - Ok(TransactionsWithCurrrency::new(transactions, currency)) - } - - pub fn get_counterparties(&self, +impl<'a> GetCounterparties for TellerClient<'a> { + fn get_counterparties(&self, account_id: &str, timeframe: &Timeframe) -> ApiServiceResult { @@ -361,7 +271,34 @@ impl<'a> TellerClient<'a> { Ok(CounterpartiesWithCurrrency::new(counterparties, currency)) } +} +// TODO: Break this up into multiple modules. +pub trait GetAggregates { + fn get_grouped_transaction_aggregates(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe, + aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) + -> ApiServiceResult>; + + fn get_balances(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) -> ApiServiceResult; + + fn get_outgoings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) -> ApiServiceResult; + + fn get_incomings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) -> ApiServiceResult; +} + +impl<'a> GetAggregates for TellerClient<'a> { fn get_grouped_transaction_aggregates(&self, account_id: &str, interval: &Interval, @@ -389,7 +326,7 @@ impl<'a> TellerClient<'a> { Ok(month_year_grouped_transactions) } - pub fn get_balances(&self, + fn get_balances(&self, account_id: &str, interval: &Interval, timeframe: &Timeframe) @@ -428,7 +365,7 @@ impl<'a> TellerClient<'a> { Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } - pub fn get_outgoings(&self, + fn get_outgoings(&self, account_id: &str, interval: &Interval, timeframe: &Timeframe) @@ -467,7 +404,7 @@ impl<'a> TellerClient<'a> { Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } - pub fn get_incomings(&self, + fn get_incomings(&self, account_id: &str, interval: &Interval, timeframe: &Timeframe) @@ -506,60 +443,4 @@ impl<'a> TellerClient<'a> { Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } - pub fn get_outgoing(&self, account_id: &str) -> ApiServiceResult { - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - let from = UTC::today().with_day(1).unwrap(); - let transactions: Vec = self.raw_transactions(&account_id, 250, 1) - .unwrap_or(vec![]) - .into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - let from_float_string_to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let outgoing = transactions.iter() - .map(from_float_string_to_cent_integer) - .filter(|ci| *ci < 0) - .fold(0i64, |sum, v| sum + v); - - Ok(Money::new(from_cent_integer_to_float_string(outgoing.abs()), currency)) - } - - pub fn get_incoming(&self, account_id: &str) -> ApiServiceResult { - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - let from = UTC::today().with_day(1).unwrap(); - let transactions: Vec = self.raw_transactions(&account_id, 250, 1) - .unwrap_or(vec![]) - .into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - let from_float_string_to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let incoming = transactions.iter() - .map(from_float_string_to_cent_integer) - .filter(|ci| *ci > 0) - .fold(0i64, |sum, v| sum + v); - - Ok(Money::new(from_cent_integer_to_float_string(incoming), currency)) - } - } diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..9ffbc40 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,6 @@ +pub mod client; +pub mod error; +pub mod inform; + +pub use self::client::*; +pub use self::inform::*; diff --git a/src/command/initialise.rs b/src/command/initialise.rs index d646e5b..40f10ba 100644 --- a/src/command/initialise.rs +++ b/src/command/initialise.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use config::{Config, get_config_path, get_config_file_to_write, write_config}; use inquirer::{Question, Answer, ask_question, ask_questions}; -use client::TellerClient; +use api::TellerClient; use super::representations::represent_list_accounts; pub fn configure_cli(config_file_path: &PathBuf) -> Option { diff --git a/src/command/list_accounts.rs b/src/command/list_accounts.rs index 157e4e0..b2bb84a 100644 --- a/src/command/list_accounts.rs +++ b/src/command/list_accounts.rs @@ -1,5 +1,5 @@ use config::Config; -use client::TellerClient; +use api::TellerClient; use super::representations::represent_list_accounts; diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index c2f8920..9b93b32 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -1,5 +1,6 @@ use config::Config; -use client::{Balances, TellerClient}; +use api::TellerClient; +use api::inform::{Balances, GetAggregates}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index ecb4dbf..af82daf 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -1,5 +1,6 @@ use config::Config; -use client::TellerClient; +use api::TellerClient; +use api::inform::GetCounterparties; use cli::arg_types::{AccountType, Timeframe}; use super::representations::to_aligned_table; diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index 1336180..f23a038 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -1,5 +1,6 @@ use config::Config; -use client::{Incomings, TellerClient}; +use api::TellerClient; +use api::inform::{Incomings, GetAggregates}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index 19bd1d4..edc3a88 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -1,5 +1,6 @@ use config::Config; -use client::{Outgoings, TellerClient}; +use api::TellerClient; +use api::inform::{Outgoings, GetAggregates}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index ea277ef..0f27be4 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -1,5 +1,6 @@ use config::Config; -use client::{Transaction, TransactionsWithCurrrency, TellerClient}; +use api::{Transaction, TellerClient}; +use api::inform::{TransactionsWithCurrrency, GetTransactionsWithCurrency}; use cli::arg_types::{AccountType, Timeframe}; use super::representations::to_aligned_table; diff --git a/src/command/representations.rs b/src/command/representations.rs index 5af6556..f3a4b14 100644 --- a/src/command/representations.rs +++ b/src/command/representations.rs @@ -2,7 +2,7 @@ use std::io::Write; use tabwriter::TabWriter; use config::Config; -use client::{HistoricalAmountsWithCurrency, Account}; +use api::{HistoricalAmountsWithCurrency, Account}; use cli::arg_types::OutputFormat; pub fn to_aligned_table(table_str: &str) -> String { diff --git a/src/command/show_balance.rs b/src/command/show_balance.rs index 8430fce..7320b43 100644 --- a/src/command/show_balance.rs +++ b/src/command/show_balance.rs @@ -1,7 +1,7 @@ -use client::TellerClient; +use api::TellerClient; +use api::inform::{Money, GetAccountBalance}; use config::Config; use cli::arg_types::AccountType; -use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { println!("{}", diff --git a/src/command/show_incoming.rs b/src/command/show_incoming.rs index 34d79c6..e877bc1 100644 --- a/src/command/show_incoming.rs +++ b/src/command/show_incoming.rs @@ -1,7 +1,7 @@ -use client::TellerClient; +use api::TellerClient; +use api::inform::{Money, GetIncoming}; use config::Config; use cli::arg_types::AccountType; -use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { println!("{}", diff --git a/src/command/show_outgoing.rs b/src/command/show_outgoing.rs index d4d2666..f0e2559 100644 --- a/src/command/show_outgoing.rs +++ b/src/command/show_outgoing.rs @@ -1,7 +1,7 @@ -use client::TellerClient; +use api::TellerClient; +use api::inform::{Money, GetOutgoing}; use config::Config; use cli::arg_types::AccountType; -use client::Money; fn represent_money(money_with_currency: &Money, hide_currency: &bool) { println!("{}", diff --git a/src/main.rs b/src/main.rs index 92ebc58..37b4dfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ mod cli; mod command; mod config; mod inquirer; -mod client; +mod api; use docopt::Docopt; use cli::get_command_type; From f421268cf357de79ae3b673e94025e6a528cb662 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Tue, 5 Jan 2016 13:00:26 +0000 Subject: [PATCH 13/16] refactor(aggregates): separation of aggregate traits --- src/api/inform.rs | 114 +++++++++++++++------------------ src/api/mod.rs | 1 - src/command/list_balances.rs | 2 +- src/command/list_incomings.rs | 2 +- src/command/list_outgoings.rs | 2 +- src/command/representations.rs | 3 +- 6 files changed, 57 insertions(+), 67 deletions(-) diff --git a/src/api/inform.rs b/src/api/inform.rs index df8c062..a1babfe 100644 --- a/src/api/inform.rs +++ b/src/api/inform.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use std::str::FromStr; // Use of #from_str. use super::client::{TellerClient, ApiServiceResult, Transaction, Account}; -use super::client::parse_utc_date_from_transaction; // TODO: Ew. bad bad bad. +use super::client::parse_utc_date_from_transaction; pub type IntervalAmount = (String, String); pub type Balances = HistoricalAmountsWithCurrency; @@ -273,64 +273,51 @@ impl<'a> GetCounterparties for TellerClient<'a> { } } -// TODO: Break this up into multiple modules. -pub trait GetAggregates { - fn get_grouped_transaction_aggregates(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe, - aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) - -> ApiServiceResult>; - +pub trait GetBalances { fn get_balances(&self, account_id: &str, interval: &Interval, timeframe: &Timeframe) -> ApiServiceResult; +} +pub trait GetOutgoings { fn get_outgoings(&self, account_id: &str, interval: &Interval, timeframe: &Timeframe) -> ApiServiceResult; +} +pub trait GetIncomings { fn get_incomings(&self, account_id: &str, interval: &Interval, timeframe: &Timeframe) -> ApiServiceResult; } -impl<'a> GetAggregates for TellerClient<'a> { - fn get_grouped_transaction_aggregates(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe, - aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) - -> ApiServiceResult> { - let transactions: Vec = self.get_transactions(&account_id, &timeframe) - .unwrap_or(vec![]); - - let mut month_year_grouped_transactions: Vec<(String, i64)> = - transactions.into_iter() - .group_by(|t| { - let transaction_date = parse_utc_date_from_transaction(&t); - match *interval { - Interval::Monthly => { - let group_name = transaction_date.format("%m-%Y").to_string(); - group_name - } - } - }) - .map(aggregate_txs) - .collect(); - month_year_grouped_transactions.reverse(); - - Ok(month_year_grouped_transactions) - } +fn to_grouped_transaction_aggregates(transactions: Vec, + interval: &Interval, + aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) + -> Vec<(String, i64)> { + let mut month_year_grouped_transactions: Vec<(String, i64)> = transactions.into_iter().group_by(|t| { + let transaction_date = parse_utc_date_from_transaction(&t); + match *interval { + Interval::Monthly => { + let group_name = transaction_date.format("%m-%Y").to_string(); + group_name + } + } + }).map(aggregate_txs).collect(); + month_year_grouped_transactions.reverse(); + + month_year_grouped_transactions +} +impl<'a> GetBalances for TellerClient<'a> { fn get_balances(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { let sum_all = |myt: (String, Vec)| { let to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 @@ -341,10 +328,10 @@ impl<'a> GetAggregates for TellerClient<'a> { (group_name, amount) }; - let month_year_total_transactions = try!(self.get_grouped_transaction_aggregates(&account_id, - &interval, - &timeframe, - &sum_all)); + let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); + let month_year_total_transactions = to_grouped_transaction_aggregates(transactions, + &interval, + &sum_all); let account = try!(self.get_account(&account_id)); let current_balance = (f64::from_str(&account.balance).unwrap() * 100f64).round() as i64; @@ -364,12 +351,14 @@ impl<'a> GetAggregates for TellerClient<'a> { Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } +} +impl<'a> GetOutgoings for TellerClient<'a> { fn get_outgoings(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { let sum_outgoings = |myt: (String, Vec)| { let to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 @@ -384,10 +373,10 @@ impl<'a> GetAggregates for TellerClient<'a> { (group_name, amount) }; - let month_year_total_outgoing = try!(self.get_grouped_transaction_aggregates(&account_id, - &interval, - &timeframe, - &sum_outgoings)); + let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); + let month_year_total_outgoing = to_grouped_transaction_aggregates(transactions, + &interval, + &sum_outgoings); let account = try!(self.get_account(&account_id)); let currency = account.currency; @@ -403,12 +392,14 @@ impl<'a> GetAggregates for TellerClient<'a> { Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } +} +impl<'a> GetIncomings for TellerClient<'a> { fn get_incomings(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { let sum_incomings = |myt: (String, Vec)| { let to_cent_integer = |t: &Transaction| { (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 @@ -423,10 +414,10 @@ impl<'a> GetAggregates for TellerClient<'a> { (group_name, amount) }; - let month_year_total_incoming = try!(self.get_grouped_transaction_aggregates(&account_id, - &interval, - &timeframe, - &sum_incomings)); + let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); + let month_year_total_incoming = to_grouped_transaction_aggregates(transactions, + &interval, + &sum_incomings); let account = try!(self.get_account(&account_id)); let currency = account.currency; @@ -442,5 +433,4 @@ impl<'a> GetAggregates for TellerClient<'a> { Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) } - } diff --git a/src/api/mod.rs b/src/api/mod.rs index 9ffbc40..cc1c9bf 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,4 +3,3 @@ pub mod error; pub mod inform; pub use self::client::*; -pub use self::inform::*; diff --git a/src/command/list_balances.rs b/src/command/list_balances.rs index 9b93b32..46e55fe 100644 --- a/src/command/list_balances.rs +++ b/src/command/list_balances.rs @@ -1,6 +1,6 @@ use config::Config; use api::TellerClient; -use api::inform::{Balances, GetAggregates}; +use api::inform::{Balances, GetBalances}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; diff --git a/src/command/list_incomings.rs b/src/command/list_incomings.rs index f23a038..edf69d0 100644 --- a/src/command/list_incomings.rs +++ b/src/command/list_incomings.rs @@ -1,6 +1,6 @@ use config::Config; use api::TellerClient; -use api::inform::{Incomings, GetAggregates}; +use api::inform::{Incomings, GetIncomings}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; diff --git a/src/command/list_outgoings.rs b/src/command/list_outgoings.rs index edc3a88..d85488c 100644 --- a/src/command/list_outgoings.rs +++ b/src/command/list_outgoings.rs @@ -1,6 +1,6 @@ use config::Config; use api::TellerClient; -use api::inform::{Outgoings, GetAggregates}; +use api::inform::{Outgoings, GetOutgoings}; use cli::arg_types::{AccountType, OutputFormat, Interval, Timeframe}; use super::representations::represent_list_amounts; diff --git a/src/command/representations.rs b/src/command/representations.rs index f3a4b14..999c603 100644 --- a/src/command/representations.rs +++ b/src/command/representations.rs @@ -2,7 +2,8 @@ use std::io::Write; use tabwriter::TabWriter; use config::Config; -use api::{HistoricalAmountsWithCurrency, Account}; +use api::Account; +use api::inform::HistoricalAmountsWithCurrency; use cli::arg_types::OutputFormat; pub fn to_aligned_table(table_str: &str) -> String { From fa8f9e4e1d6f5f48bfd1116402397d2b26440448 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Tue, 5 Jan 2016 13:38:40 +0000 Subject: [PATCH 14/16] refactor(inform): modularised calculation code --- src/api/inform.rs | 436 ------------------ src/api/inform/get_account_balance.rs | 12 + src/api/inform/get_aggregates.rs | 195 ++++++++ src/api/inform/get_counterparties.rs | 102 ++++ src/api/inform/get_incoming.rs | 40 ++ src/api/inform/get_outgoing.rs | 40 ++ .../inform/get_transactions_with_currency.rs | 41 ++ src/api/inform/mod.rs | 40 ++ 8 files changed, 470 insertions(+), 436 deletions(-) delete mode 100644 src/api/inform.rs create mode 100644 src/api/inform/get_account_balance.rs create mode 100644 src/api/inform/get_aggregates.rs create mode 100644 src/api/inform/get_counterparties.rs create mode 100644 src/api/inform/get_incoming.rs create mode 100644 src/api/inform/get_outgoing.rs create mode 100644 src/api/inform/get_transactions_with_currency.rs create mode 100644 src/api/inform/mod.rs diff --git a/src/api/inform.rs b/src/api/inform.rs deleted file mode 100644 index a1babfe..0000000 --- a/src/api/inform.rs +++ /dev/null @@ -1,436 +0,0 @@ -use cli::arg_types::{Interval, Timeframe}; - -use chrono::{UTC, Datelike}; -use itertools::Itertools; - -use std::collections::HashMap; - -use std::str::FromStr; // Use of #from_str. - -use super::client::{TellerClient, ApiServiceResult, Transaction, Account}; -use super::client::parse_utc_date_from_transaction; - -pub type IntervalAmount = (String, String); -pub type Balances = HistoricalAmountsWithCurrency; -pub type Outgoings = HistoricalAmountsWithCurrency; -pub type Incomings = HistoricalAmountsWithCurrency; -type DateStringToTransactions = (String, Vec); - -#[derive(Debug)] -pub struct HistoricalAmountsWithCurrency { - pub historical_amounts: Vec, - pub currency: String, -} - -impl HistoricalAmountsWithCurrency { - pub fn new>(historical_amounts: Vec, - currency: S) - -> HistoricalAmountsWithCurrency { - HistoricalAmountsWithCurrency { - historical_amounts: historical_amounts, - currency: currency.into(), - } - } -} - -#[derive(Debug)] -pub struct TransactionsWithCurrrency { - pub transactions: Vec, - pub currency: String, -} - -impl TransactionsWithCurrrency { - pub fn new>(transactions: Vec, - currency: S) - -> TransactionsWithCurrrency { - TransactionsWithCurrrency { - transactions: transactions, - currency: currency.into(), - } - } -} - -#[derive(Debug)] -pub struct CounterpartiesWithCurrrency { - pub counterparties: Vec<(String, String)>, - pub currency: String, -} - -impl CounterpartiesWithCurrrency { - pub fn new>(counterparties: Vec<(String, String)>, - currency: S) - -> CounterpartiesWithCurrrency { - CounterpartiesWithCurrrency { - counterparties: counterparties, - currency: currency.into(), - } - } -} - -#[derive(Debug)] -pub struct Money { - amount: String, - currency: String, -} - -impl Money { - pub fn new>(amount: S, currency: S) -> Money { - Money { - amount: amount.into(), - currency: currency.into(), - } - } - - pub fn get_balance_for_display(&self, hide_currency: &bool) -> String { - if *hide_currency { - self.amount.to_owned() - } else { - let balance_with_currency = format!("{} {}", self.amount, self.currency); - balance_with_currency.to_owned() - } - } -} - -pub trait GetAccountBalance { - fn get_account_balance(&self, account_id: &str) -> ApiServiceResult; -} - -impl<'a> GetAccountBalance for TellerClient<'a> { - fn get_account_balance(&self, account_id: &str) -> ApiServiceResult { - let to_money = |a: Account| Money::new(a.balance, a.currency); - self.get_account(&account_id).map(to_money) - } -} - -pub trait GetIncoming { - fn get_incoming(&self, account_id: &str) -> ApiServiceResult; -} - -impl<'a> GetIncoming for TellerClient<'a> { - fn get_incoming(&self, account_id: &str) -> ApiServiceResult { - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - let from = UTC::today().with_day(1).unwrap(); - let transactions: Vec = self.raw_transactions(&account_id, 250, 1) - .unwrap_or(vec![]) - .into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - let from_float_string_to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let incoming = transactions.iter() - .map(from_float_string_to_cent_integer) - .filter(|ci| *ci > 0) - .fold(0i64, |sum, v| sum + v); - - Ok(Money::new(from_cent_integer_to_float_string(incoming), currency)) - } -} - -pub trait GetOutgoing { - fn get_outgoing(&self, account_id: &str) -> ApiServiceResult; -} - -impl<'a> GetOutgoing for TellerClient<'a> { - fn get_outgoing(&self, account_id: &str) -> ApiServiceResult { - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - let from = UTC::today().with_day(1).unwrap(); - let transactions: Vec = self.raw_transactions(&account_id, 250, 1) - .unwrap_or(vec![]) - .into_iter() - .filter(|t| { - let transaction_date = - parse_utc_date_from_transaction(&t); - transaction_date > from - }) - .collect(); - - let from_float_string_to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let outgoing = transactions.iter() - .map(from_float_string_to_cent_integer) - .filter(|ci| *ci < 0) - .fold(0i64, |sum, v| sum + v); - - Ok(Money::new(from_cent_integer_to_float_string(outgoing.abs()), currency)) - } -} - -pub trait GetTransactionsWithCurrency { - fn get_transactions_with_currency(&self, - account_id: &str, - timeframe: &Timeframe) - -> ApiServiceResult; -} - -impl<'a> GetTransactionsWithCurrency for TellerClient<'a> { - fn get_transactions_with_currency(&self, - account_id: &str, - timeframe: &Timeframe) - -> ApiServiceResult { - let transactions = try!(self.get_transactions(&account_id, &timeframe)); - - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - Ok(TransactionsWithCurrrency::new(transactions, currency)) - } -} - -pub trait GetCounterparties { - fn get_counterparties(&self, - account_id: &str, - timeframe: &Timeframe) -> ApiServiceResult; -} - -fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec) - -> HashMap> { - let grouped_counterparties = transactions.iter() - .fold(HashMap::new(), |mut acc: HashMap>, - t: &'a Transaction| { - let counterparty = t.counterparty.to_owned(); - if acc.contains_key(&counterparty) { - if let Some(txs) = acc.get_mut(&counterparty) { - txs.push(t); - } - } else { - let mut txs: Vec<&'a Transaction> = vec![]; - txs.push(t); - acc.insert(counterparty, txs); - } - - acc - }); - - grouped_counterparties.into_iter().fold(HashMap::new(), |mut acc, (counterparty, txs)| { - let date_amount_tuples = txs.into_iter() - .map(|tx| (tx.date.to_owned(), tx.amount.to_owned())) - .collect(); - acc.insert(counterparty.to_string(), date_amount_tuples); - acc - }) -} - -impl<'a> GetCounterparties for TellerClient<'a> { - fn get_counterparties(&self, - account_id: &str, - timeframe: &Timeframe) - -> ApiServiceResult { - let transactions_with_currency = try!(self.get_transactions_with_currency(&account_id, - &timeframe)); - - let to_cent_integer = |amount: &str| (f64::from_str(&amount).unwrap() * 100f64).round() as i64; - let from_cent_integer_to_float_string = |amount: &i64| { - format!("{:.2}", *amount as f64 / 100f64) - }; - - let transactions: Vec = transactions_with_currency.transactions - .into_iter() - .filter(|tx| { - to_cent_integer(&tx.amount) < - 0 - }) - .collect(); - let currency = transactions_with_currency.currency; - - let counterparty_to_date_amount_list = - convert_to_counterparty_to_date_amount_list(&transactions); - let sorted_counterparties = - counterparty_to_date_amount_list.into_iter() - .map(|(counterparty, date_amount_tuples)| { - let amount = - date_amount_tuples.iter().fold(0i64, |acc, dat| { - acc + to_cent_integer(&dat.1) - }); - (counterparty, amount.abs()) - }) - .sort_by(|&(_, amount_a), &(_, amount_b)| { - amount_a.cmp(&amount_b) - }); - let counterparties = sorted_counterparties.into_iter() - .map(|(counterparty, amount)| { - (counterparty, - from_cent_integer_to_float_string(&amount)) - }) - .collect(); - - Ok(CounterpartiesWithCurrrency::new(counterparties, currency)) - } -} - -pub trait GetBalances { - fn get_balances(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) -> ApiServiceResult; -} - -pub trait GetOutgoings { - fn get_outgoings(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) -> ApiServiceResult; -} - -pub trait GetIncomings { - fn get_incomings(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) -> ApiServiceResult; -} - -fn to_grouped_transaction_aggregates(transactions: Vec, - interval: &Interval, - aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) - -> Vec<(String, i64)> { - let mut month_year_grouped_transactions: Vec<(String, i64)> = transactions.into_iter().group_by(|t| { - let transaction_date = parse_utc_date_from_transaction(&t); - match *interval { - Interval::Monthly => { - let group_name = transaction_date.format("%m-%Y").to_string(); - group_name - } - } - }).map(aggregate_txs).collect(); - month_year_grouped_transactions.reverse(); - - month_year_grouped_transactions -} - -impl<'a> GetBalances for TellerClient<'a> { - fn get_balances(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { - let sum_all = |myt: (String, Vec)| { - let to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - - let group_name = myt.0; - let amount = myt.1.iter().map(to_cent_integer).fold(0i64, |sum, v| sum + v); - (group_name, amount) - }; - - let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); - let month_year_total_transactions = to_grouped_transaction_aggregates(transactions, - &interval, - &sum_all); - - let account = try!(self.get_account(&account_id)); - let current_balance = (f64::from_str(&account.balance).unwrap() * 100f64).round() as i64; - let currency = account.currency; - - let mut historical_amounts: Vec = vec![]; - historical_amounts.push(("current".to_string(), - format!("{:.2}", current_balance as f64 / 100f64))); - - let mut last_balance = current_balance; - for mytt in month_year_total_transactions { - last_balance = last_balance - mytt.1; - historical_amounts.push((mytt.0.to_string(), - format!("{:.2}", last_balance as f64 / 100f64))); - } - historical_amounts.reverse(); - - Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) - } -} - -impl<'a> GetOutgoings for TellerClient<'a> { - fn get_outgoings(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { - let sum_outgoings = |myt: (String, Vec)| { - let to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - - let group_name = myt.0; - let amount = myt.1 - .iter() - .map(to_cent_integer) - .filter(|ci| *ci < 0) - .fold(0i64, |sum, v| sum + v); - (group_name, amount) - }; - - let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); - let month_year_total_outgoing = to_grouped_transaction_aggregates(transactions, - &interval, - &sum_outgoings); - - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let mut historical_amounts: Vec = vec![]; - for mytt in month_year_total_outgoing { - historical_amounts.push((mytt.0.to_string(), - from_cent_integer_to_float_string(mytt.1.abs()))); - } - historical_amounts.reverse(); - - Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) - } -} - -impl<'a> GetIncomings for TellerClient<'a> { - fn get_incomings(&self, - account_id: &str, - interval: &Interval, - timeframe: &Timeframe) - -> ApiServiceResult { - let sum_incomings = |myt: (String, Vec)| { - let to_cent_integer = |t: &Transaction| { - (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 - }; - - let group_name = myt.0; - let amount = myt.1 - .iter() - .map(to_cent_integer) - .filter(|ci| *ci > 0) - .fold(0i64, |sum, v| sum + v); - (group_name, amount) - }; - - let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); - let month_year_total_incoming = to_grouped_transaction_aggregates(transactions, - &interval, - &sum_incomings); - - let account = try!(self.get_account(&account_id)); - let currency = account.currency; - - let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); - - let mut historical_amounts: Vec = vec![]; - for mytt in month_year_total_incoming { - historical_amounts.push((mytt.0.to_string(), - from_cent_integer_to_float_string(mytt.1))); - } - historical_amounts.reverse(); - - Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) - } -} diff --git a/src/api/inform/get_account_balance.rs b/src/api/inform/get_account_balance.rs new file mode 100644 index 0000000..9808bd5 --- /dev/null +++ b/src/api/inform/get_account_balance.rs @@ -0,0 +1,12 @@ +use super::{TellerClient, ApiServiceResult, Account, Money}; + +pub trait GetAccountBalance { + fn get_account_balance(&self, account_id: &str) -> ApiServiceResult; +} + +impl<'a> GetAccountBalance for TellerClient<'a> { + fn get_account_balance(&self, account_id: &str) -> ApiServiceResult { + let to_money = |a: Account| Money::new(a.balance, a.currency); + self.get_account(&account_id).map(to_money) + } +} diff --git a/src/api/inform/get_aggregates.rs b/src/api/inform/get_aggregates.rs new file mode 100644 index 0000000..38f18aa --- /dev/null +++ b/src/api/inform/get_aggregates.rs @@ -0,0 +1,195 @@ +use cli::arg_types::{Interval, Timeframe}; + +use std::str::FromStr; // Use of #from_str. + +use itertools::Itertools; + +use super::{TellerClient, ApiServiceResult, Transaction}; +use super::parse_utc_date_from_transaction; + +pub type Balances = HistoricalAmountsWithCurrency; +pub type Outgoings = HistoricalAmountsWithCurrency; +pub type Incomings = HistoricalAmountsWithCurrency; + +pub type IntervalAmount = (String, String); + +type DateStringToTransactions = (String, Vec); + +#[derive(Debug)] +pub struct HistoricalAmountsWithCurrency { + pub historical_amounts: Vec, + pub currency: String, +} + +impl HistoricalAmountsWithCurrency { + pub fn new>(historical_amounts: Vec, + currency: S) + -> HistoricalAmountsWithCurrency { + HistoricalAmountsWithCurrency { + historical_amounts: historical_amounts, + currency: currency.into(), + } + } +} + +pub trait GetBalances { + fn get_balances(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) -> ApiServiceResult; +} + +pub trait GetOutgoings { + fn get_outgoings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) -> ApiServiceResult; +} + +pub trait GetIncomings { + fn get_incomings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) -> ApiServiceResult; +} + +fn to_grouped_transaction_aggregates(transactions: Vec, + interval: &Interval, + aggregate_txs: &Fn(DateStringToTransactions) -> (String, i64)) + -> Vec<(String, i64)> { + let mut month_year_grouped_transactions: Vec<(String, i64)> = transactions.into_iter().group_by(|t| { + let transaction_date = parse_utc_date_from_transaction(&t); + match *interval { + Interval::Monthly => { + let group_name = transaction_date.format("%m-%Y").to_string(); + group_name + } + } + }).map(aggregate_txs).collect(); + month_year_grouped_transactions.reverse(); + + month_year_grouped_transactions +} + +impl<'a> GetBalances for TellerClient<'a> { + fn get_balances(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { + let sum_all = |myt: (String, Vec)| { + let to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + + let group_name = myt.0; + let amount = myt.1.iter().map(to_cent_integer).fold(0i64, |sum, v| sum + v); + (group_name, amount) + }; + + let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); + let month_year_total_transactions = to_grouped_transaction_aggregates(transactions, + &interval, + &sum_all); + + let account = try!(self.get_account(&account_id)); + let current_balance = (f64::from_str(&account.balance).unwrap() * 100f64).round() as i64; + let currency = account.currency; + + let mut historical_amounts: Vec = vec![]; + historical_amounts.push(("current".to_string(), + format!("{:.2}", current_balance as f64 / 100f64))); + + let mut last_balance = current_balance; + for mytt in month_year_total_transactions { + last_balance = last_balance - mytt.1; + historical_amounts.push((mytt.0.to_string(), + format!("{:.2}", last_balance as f64 / 100f64))); + } + historical_amounts.reverse(); + + Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) + } +} + +impl<'a> GetOutgoings for TellerClient<'a> { + fn get_outgoings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { + let sum_outgoings = |myt: (String, Vec)| { + let to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + + let group_name = myt.0; + let amount = myt.1 + .iter() + .map(to_cent_integer) + .filter(|ci| *ci < 0) + .fold(0i64, |sum, v| sum + v); + (group_name, amount) + }; + + let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); + let month_year_total_outgoing = to_grouped_transaction_aggregates(transactions, + &interval, + &sum_outgoings); + + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); + + let mut historical_amounts: Vec = vec![]; + for mytt in month_year_total_outgoing { + historical_amounts.push((mytt.0.to_string(), + from_cent_integer_to_float_string(mytt.1.abs()))); + } + historical_amounts.reverse(); + + Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) + } +} + +impl<'a> GetIncomings for TellerClient<'a> { + fn get_incomings(&self, + account_id: &str, + interval: &Interval, + timeframe: &Timeframe) + -> ApiServiceResult { + let sum_incomings = |myt: (String, Vec)| { + let to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + + let group_name = myt.0; + let amount = myt.1 + .iter() + .map(to_cent_integer) + .filter(|ci| *ci > 0) + .fold(0i64, |sum, v| sum + v); + (group_name, amount) + }; + + let transactions = self.get_transactions(&account_id, &timeframe).unwrap_or(vec![]); + let month_year_total_incoming = to_grouped_transaction_aggregates(transactions, + &interval, + &sum_incomings); + + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); + + let mut historical_amounts: Vec = vec![]; + for mytt in month_year_total_incoming { + historical_amounts.push((mytt.0.to_string(), + from_cent_integer_to_float_string(mytt.1))); + } + historical_amounts.reverse(); + + Ok(HistoricalAmountsWithCurrency::new(historical_amounts, currency)) + } +} diff --git a/src/api/inform/get_counterparties.rs b/src/api/inform/get_counterparties.rs new file mode 100644 index 0000000..cfbe174 --- /dev/null +++ b/src/api/inform/get_counterparties.rs @@ -0,0 +1,102 @@ +use cli::arg_types::Timeframe; + +use std::str::FromStr; // Use of #from_str. + +use std::collections::HashMap; +use itertools::Itertools; + +use super::{TellerClient, ApiServiceResult, Transaction}; + +#[derive(Debug)] +pub struct CounterpartiesWithCurrrency { + pub counterparties: Vec<(String, String)>, + pub currency: String, +} + +impl CounterpartiesWithCurrrency { + pub fn new>(counterparties: Vec<(String, String)>, + currency: S) + -> CounterpartiesWithCurrrency { + CounterpartiesWithCurrrency { + counterparties: counterparties, + currency: currency.into(), + } + } +} + +pub trait GetCounterparties { + fn get_counterparties(&self, + account_id: &str, + timeframe: &Timeframe) -> ApiServiceResult; +} + +fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec) + -> HashMap> { + let grouped_counterparties = transactions.iter() + .fold(HashMap::new(), |mut acc: HashMap>, + t: &'a Transaction| { + let counterparty = t.counterparty.to_owned(); + if acc.contains_key(&counterparty) { + if let Some(txs) = acc.get_mut(&counterparty) { + txs.push(t); + } + } else { + let mut txs: Vec<&'a Transaction> = vec![]; + txs.push(t); + acc.insert(counterparty, txs); + } + + acc + }); + + grouped_counterparties.into_iter().fold(HashMap::new(), |mut acc, (counterparty, txs)| { + let date_amount_tuples = txs.into_iter() + .map(|tx| (tx.date.to_owned(), tx.amount.to_owned())) + .collect(); + acc.insert(counterparty.to_string(), date_amount_tuples); + acc + }) +} + +impl<'a> GetCounterparties for TellerClient<'a> { + fn get_counterparties(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult { + let transactions = try!(self.get_transactions(&account_id, &timeframe)); + let account = try!(self.get_account(&account_id)); + + let to_cent_integer = |amount: &str| (f64::from_str(&amount).unwrap() * 100f64).round() as i64; + let from_cent_integer_to_float_string = |amount: &i64| { + format!("{:.2}", *amount as f64 / 100f64) + }; + + let outgoing_transactions: Vec = transactions.into_iter().filter(|tx| { + to_cent_integer(&tx.amount) < 0 + }).collect(); + + let counterparty_to_date_amount_list = + convert_to_counterparty_to_date_amount_list(&outgoing_transactions); + let sorted_counterparties = + counterparty_to_date_amount_list.into_iter() + .map(|(counterparty, date_amount_tuples)| { + let amount = + date_amount_tuples.iter().fold(0i64, |acc, dat| { + acc + to_cent_integer(&dat.1) + }); + (counterparty, amount.abs()) + }) + .sort_by(|&(_, amount_a), &(_, amount_b)| { + amount_a.cmp(&amount_b) + }); + let counterparties = sorted_counterparties.into_iter() + .map(|(counterparty, amount)| { + (counterparty, + from_cent_integer_to_float_string(&amount)) + }) + .collect(); + let currency = account.currency; + Ok(CounterpartiesWithCurrrency::new(counterparties, currency)) + } +} diff --git a/src/api/inform/get_incoming.rs b/src/api/inform/get_incoming.rs new file mode 100644 index 0000000..08e29ee --- /dev/null +++ b/src/api/inform/get_incoming.rs @@ -0,0 +1,40 @@ +use chrono::{UTC, Datelike}; + +use std::str::FromStr; // Use of #from_str. + +use super::{TellerClient, ApiServiceResult, Transaction, Money}; +use super::parse_utc_date_from_transaction; + +pub trait GetIncoming { + fn get_incoming(&self, account_id: &str) -> ApiServiceResult; +} + +impl<'a> GetIncoming for TellerClient<'a> { + fn get_incoming(&self, account_id: &str) -> ApiServiceResult { + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from = UTC::today().with_day(1).unwrap(); + let transactions: Vec = self.raw_transactions(&account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + let from_float_string_to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); + + let incoming = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci > 0) + .fold(0i64, |sum, v| sum + v); + + Ok(Money::new(from_cent_integer_to_float_string(incoming), currency)) + } +} diff --git a/src/api/inform/get_outgoing.rs b/src/api/inform/get_outgoing.rs new file mode 100644 index 0000000..ba04991 --- /dev/null +++ b/src/api/inform/get_outgoing.rs @@ -0,0 +1,40 @@ +use chrono::{UTC, Datelike}; + +use std::str::FromStr; // Use of #from_str. + +use super::{TellerClient, ApiServiceResult, Transaction, Money}; +use super::parse_utc_date_from_transaction; + +pub trait GetOutgoing { + fn get_outgoing(&self, account_id: &str) -> ApiServiceResult; +} + +impl<'a> GetOutgoing for TellerClient<'a> { + fn get_outgoing(&self, account_id: &str) -> ApiServiceResult { + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + let from = UTC::today().with_day(1).unwrap(); + let transactions: Vec = self.raw_transactions(&account_id, 250, 1) + .unwrap_or(vec![]) + .into_iter() + .filter(|t| { + let transaction_date = + parse_utc_date_from_transaction(&t); + transaction_date > from + }) + .collect(); + + let from_float_string_to_cent_integer = |t: &Transaction| { + (f64::from_str(&t.amount).unwrap() * 100f64).round() as i64 + }; + let from_cent_integer_to_float_string = |amount: i64| format!("{:.2}", amount as f64 / 100f64); + + let outgoing = transactions.iter() + .map(from_float_string_to_cent_integer) + .filter(|ci| *ci < 0) + .fold(0i64, |sum, v| sum + v); + + Ok(Money::new(from_cent_integer_to_float_string(outgoing.abs()), currency)) + } +} diff --git a/src/api/inform/get_transactions_with_currency.rs b/src/api/inform/get_transactions_with_currency.rs new file mode 100644 index 0000000..1bef5b9 --- /dev/null +++ b/src/api/inform/get_transactions_with_currency.rs @@ -0,0 +1,41 @@ +use cli::arg_types::Timeframe; + +use super::{TellerClient, ApiServiceResult, Transaction}; + +#[derive(Debug)] +pub struct TransactionsWithCurrrency { + pub transactions: Vec, + pub currency: String, +} + +impl TransactionsWithCurrrency { + pub fn new>(transactions: Vec, + currency: S) + -> TransactionsWithCurrrency { + TransactionsWithCurrrency { + transactions: transactions, + currency: currency.into(), + } + } +} + +pub trait GetTransactionsWithCurrency { + fn get_transactions_with_currency(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult; +} + +impl<'a> GetTransactionsWithCurrency for TellerClient<'a> { + fn get_transactions_with_currency(&self, + account_id: &str, + timeframe: &Timeframe) + -> ApiServiceResult { + let transactions = try!(self.get_transactions(&account_id, &timeframe)); + + let account = try!(self.get_account(&account_id)); + let currency = account.currency; + + Ok(TransactionsWithCurrrency::new(transactions, currency)) + } +} diff --git a/src/api/inform/mod.rs b/src/api/inform/mod.rs new file mode 100644 index 0000000..a0dc674 --- /dev/null +++ b/src/api/inform/mod.rs @@ -0,0 +1,40 @@ +pub mod get_account_balance; +pub mod get_incoming; +pub mod get_outgoing; +pub mod get_transactions_with_currency; +pub mod get_counterparties; +pub mod get_aggregates; + +pub use super::client::{TellerClient, ApiServiceResult, Transaction, Account}; +pub use super::client::parse_utc_date_from_transaction; + +pub use self::get_account_balance::*; +pub use self::get_incoming::*; +pub use self::get_outgoing::*; +pub use self::get_transactions_with_currency::*; +pub use self::get_counterparties::*; +pub use self::get_aggregates::*; + +#[derive(Debug)] +pub struct Money { + amount: String, + currency: String, +} + +impl Money { + pub fn new>(amount: S, currency: S) -> Money { + Money { + amount: amount.into(), + currency: currency.into(), + } + } + + pub fn get_balance_for_display(&self, hide_currency: &bool) -> String { + if *hide_currency { + self.amount.to_owned() + } else { + let balance_with_currency = format!("{} {}", self.amount, self.currency); + balance_with_currency.to_owned() + } + } +} From 50a8067f936d0bf60aae90d9591b10e436190ffa Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Tue, 5 Jan 2016 13:46:33 +0000 Subject: [PATCH 15/16] fix(representations): removed empty line at end of outputs of lists --- src/command/list_counterparties.rs | 2 +- src/command/list_transactions.rs | 2 +- src/command/representations.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/command/list_counterparties.rs b/src/command/list_counterparties.rs index af82daf..794ec34 100644 --- a/src/command/list_counterparties.rs +++ b/src/command/list_counterparties.rs @@ -23,7 +23,7 @@ fn represent_list_counterparties(counterparties: &Vec<(String, String)>, let counterparties_str = to_aligned_table(&counterparties_table); - println!("{}", counterparties_str) + print!("{}", counterparties_str) } pub fn list_counterparties_command(config: &Config, diff --git a/src/command/list_transactions.rs b/src/command/list_transactions.rs index 0f27be4..a4421c4 100644 --- a/src/command/list_transactions.rs +++ b/src/command/list_transactions.rs @@ -39,7 +39,7 @@ fn represent_list_transactions(transactions: &Vec, let transactions_str = to_aligned_table(&transactions_table); - println!("{}", transactions_str) + print!("{}", transactions_str) } pub fn list_transactions_command(config: &Config, diff --git a/src/command/representations.rs b/src/command/representations.rs index 999c603..987391f 100644 --- a/src/command/representations.rs +++ b/src/command/representations.rs @@ -33,7 +33,7 @@ pub fn represent_list_accounts(accounts: &Vec, config: &Config) { let accounts_str = to_aligned_table(&accounts_table); - println!("{}", accounts_str) + print!("{}", accounts_str) } pub fn represent_list_amounts(amount_type: &str, From 97f6013068329f806ae68c70d504809cececb673 Mon Sep 17 00:00:00 2001 From: Seb Insua Date: Tue, 5 Jan 2016 13:47:12 +0000 Subject: [PATCH 16/16] refactor(v1): finished refactor Fixes #18 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a92216..84acfed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ [root] name = "teller_cli" -version = "0.0.4" +version = "0.0.5" dependencies = [ "chrono 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", "docopt 0.6.78 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 1ea101a..857b14c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "teller_cli" -version = "0.0.4" +version = "0.0.5" authors = ["Seb Insua "] [[bin]] diff --git a/README.md b/README.md index 60a233b..7a078cf 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ echo "$LAST_TRANSACTION"; ### From release ``` -> curl -L https://github.com/sebinsua/teller-cli/releases/download/v0.0.4/teller > /usr/local/bin/teller && chmod +x /usr/local/bin/teller +> curl -L https://github.com/sebinsua/teller-cli/releases/download/v0.0.5/teller > /usr/local/bin/teller && chmod +x /usr/local/bin/teller ``` ### From source