aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2019-08-18 23:45:33 -0400
committerJesse Luehrs <doy@tozt.net>2019-08-18 23:45:33 -0400
commit3539ceb20f5383a332a8ad1fcab816cf083f277e (patch)
tree5cea964a052c7c0bd967ede1d4283b75eba58ea5
parentca8b49db6c369bc9b5c71a43ca7eb31140223cf1 (diff)
downloadynab-reimbursements-3539ceb20f5383a332a8ad1fcab816cf083f277e.tar.gz
ynab-reimbursements-3539ceb20f5383a332a8ad1fcab816cf083f277e.zip
better error handling
-rw-r--r--Cargo.lock29
-rw-r--r--Cargo.toml1
-rw-r--r--src/app.rs46
-rw-r--r--src/display.rs14
-rw-r--r--src/main.rs58
-rw-r--r--src/paths.rs58
-rw-r--r--src/views/txn_tables.rs8
-rw-r--r--src/ynab.rs1
-rw-r--r--src/ynab/budget.rs66
-rw-r--r--src/ynab/client.rs65
10 files changed, 259 insertions, 87 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6d03f5a..17dc685 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -324,6 +324,11 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1310,6 +1315,26 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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<T> = std::result::Result<T, Error>;
+
+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<T> = std::result::Result<T, Error>;
+
+const PROJECT_NAME: &str = "ynab";
+
+pub fn api_key() -> Result<std::path::PathBuf> {
+ 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<String> {
+ 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<T> = std::result::Result<T, Error>;
+
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<Self> {
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<String> {
+ ) -> 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<super::transaction::Transaction> {
- 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<Vec<super::transaction::Transaction>> {
+ 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<T> = std::result::Result<T, Error>;
+
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<ynab_api::models::BudgetDetail> {
+ 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<String> {
- 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),
+ })
}
}