From 3539ceb20f5383a332a8ad1fcab816cf083f277e Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 18 Aug 2019 23:45:33 -0400 Subject: better error handling --- Cargo.lock | 29 ++++++++++++++++++++++ Cargo.toml | 1 + src/app.rs | 46 ++++++++++++++++++++++++++++++++++ src/display.rs | 14 ----------- src/main.rs | 58 ++++++++++++++++++++++--------------------- src/paths.rs | 58 ++++++++++++++++++++++++++++++++++++++++--- src/views/txn_tables.rs | 8 ++++-- src/ynab.rs | 1 + src/ynab/budget.rs | 66 ++++++++++++++++++++++++++++++++----------------- src/ynab/client.rs | 65 +++++++++++++++++++++++++++++++++++------------- 10 files changed, 259 insertions(+), 87 deletions(-) create mode 100644 src/app.rs delete mode 100644 src/display.rs diff --git a/Cargo.lock b/Cargo.lock index 6d03f5a..17dc685 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,6 +323,11 @@ dependencies = [ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "doc-comment" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "dtoa" version = "0.4.4" @@ -1309,6 +1314,26 @@ name = "smallvec" version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "snafu" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)", + "doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu-derive 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "snafu-derive" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "stable_deref_trait" version = "1.1.1" @@ -1699,6 +1724,7 @@ dependencies = [ "cursive 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "cursive_table_view 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "ynab-api 1.0.0", ] @@ -1740,6 +1766,7 @@ dependencies = [ "checksum darling_macro 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c6d8dac1c6f1d29a41c4712b4400f878cb4fcc4c7628f298dd75038e024998d1" "checksum directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" "checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" +"checksum doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" "checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b" "checksum encoding_rs 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)" = "4155785c79f2f6701f185eb2e6b4caf0555ec03477cb4c70db67b465311620ed" @@ -1852,6 +1879,8 @@ dependencies = [ "checksum signal-hook-registry 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1797d48f38f91643908bb14e35e79928f9f4b3cefb2420a564dde0991b4358dc" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" +"checksum snafu 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b028158eb06caa8345bee10cccfb25fa632beccf0ef5308832b4fd4b78a7db48" +"checksum snafu-derive 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf50aaef500c248a590e2696e8bf8c7620ca2235b9bb90a70363d82dd1abec6a" "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" diff --git a/Cargo.toml b/Cargo.toml index e112cfa..a153006 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ cursive = "0.12" # cursive = { version = "0.12", default-features = false, features = ["crossterm-backend"] } cursive_table_view = "0.10" directories = "2.0" +snafu = "0.4" ynab-api = { path = "ynab-api" } diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..22bf543 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,46 @@ +pub struct App { + cursive: cursive::Cursive, +} + +impl App { + pub fn new(budget: crate::ynab::Budget) -> Self { + let mut app = cursive::Cursive::default(); + let term_width = app.screen_size().x; + app.set_theme(Self::theme()); + app.add_global_callback('q', |s| s.quit()); + + let mut layout = cursive::views::LinearLayout::vertical(); + layout.add_child(cursive::views::TextView::new(format!( + "Budget: {} ({})\n{}", + budget.name(), + budget.id(), + "=".repeat(term_width), + ))); + + layout.add_child(crate::views::TxnTables::new("txn_tables", &budget)); + + app.set_user_data(budget); + app.add_fullscreen_layer(layout); + + Self { cursive: app } + } + + pub fn run(&mut self) { + self.cursive.run(); + } + + fn theme() -> cursive::theme::Theme { + let mut palette = cursive::theme::Palette::default(); + palette[cursive::theme::PaletteColor::Background] = + cursive::theme::Color::TerminalDefault; + palette[cursive::theme::PaletteColor::View] = + cursive::theme::Color::TerminalDefault; + palette[cursive::theme::PaletteColor::Primary] = + cursive::theme::Color::TerminalDefault; + cursive::theme::Theme { + shadow: false, + borders: cursive::theme::BorderStyle::Simple, + palette, + } + } +} diff --git a/src/display.rs b/src/display.rs deleted file mode 100644 index cbe0459..0000000 --- a/src/display.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn theme() -> cursive::theme::Theme { - let mut palette = cursive::theme::Palette::default(); - palette[cursive::theme::PaletteColor::Background] = - cursive::theme::Color::TerminalDefault; - palette[cursive::theme::PaletteColor::View] = - cursive::theme::Color::TerminalDefault; - palette[cursive::theme::PaletteColor::Primary] = - cursive::theme::Color::TerminalDefault; - cursive::theme::Theme { - shadow: false, - borders: cursive::theme::BorderStyle::Simple, - palette, - } -} diff --git a/src/main.rs b/src/main.rs index f7ce70f..d53ce1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,35 +1,37 @@ -mod display; +mod app; mod paths; mod views; mod ynab; -use std::io::Read; +use snafu::ResultExt; -fn main() { - let mut key = String::new(); - std::fs::File::open(paths::api_key()) - .unwrap() - .read_to_string(&mut key) - .unwrap(); - let key = key.trim(); - let budget = ynab::Budget::new(&key); - - let mut app = cursive::Cursive::default(); - let term_width = app.screen_size().x; - app.set_theme(display::theme()); - app.add_global_callback('q', |s| s.quit()); - - let mut layout = cursive::views::LinearLayout::vertical(); - layout.add_child(cursive::views::TextView::new(format!( - "Budget: {} ({})\n{}", - budget.name(), - budget.id(), - "=".repeat(term_width), - ))); - - layout.add_child(views::TxnTables::new("txn_tables", &budget)); - - app.set_user_data(budget); - app.add_fullscreen_layer(layout); +#[derive(Debug, snafu::Snafu)] +pub enum Error { + #[snafu(display("failed to get api key: {}", source))] + GetApiKey { source: crate::paths::Error }, + + #[snafu(display("failed to load budget: {}", source))] + LoadBudget { source: crate::ynab::BudgetError }, +} + +pub type Result = std::result::Result; + +fn run() -> Result<()> { + let key = paths::read_api_key().context(GetApiKey)?; + let budget = ynab::Budget::new(&key).context(LoadBudget)?; + + let mut app = app::App::new(budget); app.run(); + + Ok(()) +} + +fn main() { + match run() { + Ok(_) => {} + Err(e) => { + eprintln!("ynab-reimbursements: {}", e); + std::process::exit(1); + } + } } diff --git a/src/paths.rs b/src/paths.rs index 31c07d0..f53bef5 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -1,6 +1,56 @@ -pub fn api_key() -> std::path::PathBuf { - directories::ProjectDirs::from("", "", "ynab") - .unwrap() +use snafu::{OptionExt, ResultExt}; +use std::io::Read; + +#[derive(Debug, snafu::Snafu)] +pub enum Error { + #[snafu(display("couldn't find config path for project {}", name))] + FindConfigDir { name: String }, + + #[snafu(display( + "couldn't open file {}: {}", + file.to_string_lossy(), + source + ))] + OpenFile { + file: std::path::PathBuf, + source: std::io::Error, + }, + + #[snafu(display( + "couldn't read file {}: {}", + file.to_string_lossy(), + source + ))] + ReadFile { + file: std::path::PathBuf, + source: std::io::Error, + }, +} + +pub type Result = std::result::Result; + +const PROJECT_NAME: &str = "ynab"; + +pub fn api_key() -> Result { + Ok(directories::ProjectDirs::from("", "", PROJECT_NAME) + .with_context(|| FindConfigDir { + name: PROJECT_NAME.to_string(), + })? .config_dir() - .join("api-key") + .join("api-key")) +} + +pub fn read_api_key() -> Result { + let mut key = String::new(); + let key_file = api_key()?; + std::fs::File::open(key_file.clone()) + .with_context(|| OpenFile { + file: key_file.clone(), + })? + .read_to_string(&mut key) + .with_context(|| ReadFile { + file: key_file.clone(), + })?; + let key = key.trim(); + Ok(key.to_string()) } diff --git a/src/views/txn_tables.rs b/src/views/txn_tables.rs index f294039..62539ac 100644 --- a/src/views/txn_tables.rs +++ b/src/views/txn_tables.rs @@ -121,7 +121,7 @@ fn submit(s: &mut cursive::Cursive) { let budget: &mut crate::ynab::Budget = s.user_data().unwrap(); let txns: Vec<_> = inflows.iter().chain(outflows.iter()).collect(); let err = budget.reconcile_transactions(&txns); - if let Some(err) = err { + if let Err(err) = err { s.add_layer(super::util::dialog(&format!("Error: {}", err))) } else { s.add_layer(super::util::dialog(&format!( @@ -183,7 +183,11 @@ fn submit(s: &mut cursive::Cursive) { fn refresh(s: &mut cursive::Cursive) { let budget: &mut crate::ynab::Budget = s.user_data().unwrap(); - budget.refresh(); + let err = budget.refresh(); + if let Err(err) = err { + s.add_layer(super::util::dialog(&format!("Error: {}", err))); + return; + } let mut inflows: Vec<_> = budget .reimbursables() diff --git a/src/ynab.rs b/src/ynab.rs index 2f0db5a..f07d86e 100644 --- a/src/ynab.rs +++ b/src/ynab.rs @@ -4,6 +4,7 @@ mod transaction; mod util; pub use budget::Budget; +pub use budget::Error as BudgetError; pub use client::Client; pub use transaction::Transaction; pub use util::format_amount; diff --git a/src/ynab/budget.rs b/src/ynab/budget.rs index b11e72a..e3275bb 100644 --- a/src/ynab/budget.rs +++ b/src/ynab/budget.rs @@ -1,3 +1,19 @@ +use snafu::{OptionExt, ResultExt}; + +#[derive(Debug, snafu::Snafu)] +pub enum Error { + #[snafu(display("couldn't get default budget: {}", source))] + GetBudget { source: super::client::Error }, + + #[snafu(display("couldn't update transactions: {}", source))] + UpdateTransactions { source: super::client::Error }, + + #[snafu(display("couldn't find the reimbursables category"))] + FindReimbursablesCategory, +} + +pub type Result = std::result::Result; + pub struct Budget { client: super::client::Client, id: String, @@ -6,10 +22,10 @@ pub struct Budget { } impl Budget { - pub fn new(key: &str) -> Self { + pub fn new(key: &str) -> Result { let client = super::client::Client::new(key); - let budget = client.default_budget(); - let reimbursables = Self::get_reimbursables(&budget); + let budget = client.default_budget().context(GetBudget)?; + let reimbursables = Self::get_reimbursables(&budget)?; let budget = Self { client, id: budget.id.clone(), @@ -17,15 +33,17 @@ impl Budget { reimbursables, }; budget.check(); - budget + Ok(budget) } - pub fn refresh(&mut self) { - let budget = self.client.default_budget(); + #[must_use] + pub fn refresh(&mut self) -> Result<()> { + let budget = self.client.default_budget().context(GetBudget)?; self.id = budget.id.clone(); self.name = budget.name.clone(); - self.reimbursables = Self::get_reimbursables(&budget); + self.reimbursables = Self::get_reimbursables(&budget)?; self.check(); + Ok(()) } pub fn name(&self) -> String { @@ -40,10 +58,11 @@ impl Budget { &self.reimbursables } + #[must_use] pub fn reconcile_transactions( &self, txns: &[&super::transaction::Transaction], - ) -> Option { + ) -> Result<()> { let mut to_update = ynab_api::models::UpdateTransactionsWrapper::new(); to_update.transactions = Some( @@ -55,29 +74,31 @@ impl Budget { }) .collect(), ); - self.client.update_transactions(&self.id, to_update) + self.client + .update_transactions(&self.id, to_update) + .context(UpdateTransactions)?; + Ok(()) } fn get_reimbursables( budget: &ynab_api::models::BudgetDetail, - ) -> Vec { - let reimbursables_id = if let Some(categories) = &budget.categories { - categories - .iter() - .find(|c| c.name == "Reimbursables") - .map(|c| c.id.clone()) - .unwrap() - } else { - panic!("no categories found") - }; + ) -> Result> { + let reimbursables_id = budget + .categories + .as_ref() + .and_then(|categories| { + categories + .iter() + .find(|c| c.name == "Reimbursables") + .map(|c| c.id.clone()) + }) + .context(FindReimbursablesCategory)?; let mut payee_map = std::collections::HashMap::new(); if let Some(payees) = &budget.payees { for p in payees { payee_map.insert(p.id.clone(), p.name.clone()); } - } else { - panic!("no payees?"); } let payee_map = payee_map; @@ -87,6 +108,7 @@ impl Budget { account_map.insert(a.id.clone(), a.name.clone()); } } + let account_map = account_map; let mut reimbursables = vec![]; @@ -156,7 +178,7 @@ impl Budget { } reimbursables.sort_by_cached_key(|t| t.date.clone()); - reimbursables + Ok(reimbursables) } fn check(&self) { diff --git a/src/ynab/client.rs b/src/ynab/client.rs index 4b935c3..19b456b 100644 --- a/src/ynab/client.rs +++ b/src/ynab/client.rs @@ -1,3 +1,19 @@ +#[derive(Debug, snafu::Snafu)] +pub enum Error { + // ynab-api error types don't implement Error, so can't use the + // auto-source behavior + #[snafu(display("failed to update transactions: {}", source_msg))] + UpdateTransactions { source_msg: String }, + + #[snafu(display("failed to get budgets: {}", source_msg))] + GetBudgets { source_msg: String }, + + #[snafu(display("failed to get budget {}: {}", id, source_msg))] + GetBudgetById { id: String, source_msg: String }, +} + +pub type Result = std::result::Result; + pub struct Client { api: ynab_api::apis::client::APIClient, } @@ -15,31 +31,46 @@ impl Client { } } - pub fn default_budget(&self) -> ynab_api::models::BudgetDetail { - let budgets = - self.api.budgets_api().get_budgets().unwrap().data.budgets; - let budget = budgets.iter().next().unwrap(); - self.api + pub fn default_budget(&self) -> Result { + let budget_id = self + .api .budgets_api() - .get_budget_by_id(&budget.id, 0) - .unwrap() + .get_budgets() + .map_err(|e| Error::GetBudgets { + source_msg: format!("{:?}", e), + })? .data - .budget + .budgets + .iter() + .next() + .ok_or_else(|| Error::GetBudgets { + source_msg: "no budgets found".to_string(), + })? + .id + .clone(); + Ok(self + .api + .budgets_api() + .get_budget_by_id(&budget_id, 0) + .map_err(|e| Error::GetBudgetById { + id: budget_id.clone(), + source_msg: format!("{:?}", e), + })? + .data + .budget) } pub fn update_transactions( &self, budget_id: &str, transactions: ynab_api::models::UpdateTransactionsWrapper, - ) -> Option { - let res = self - .api + ) -> Result<()> { + self.api .transactions_api() - .update_transactions(budget_id, transactions); - if let Err(e) = res { - Some(format!("{:?}", e)) - } else { - None - } + .update_transactions(budget_id, transactions) + .map(|_| ()) + .map_err(|e| Error::UpdateTransactions { + source_msg: format!("{:?}", e), + }) } } -- cgit v1.2.3