Handle errors more gracefully than panicking

master
Nick Krichevsky 2022-10-09 15:58:48 -04:00
parent f35cb57a79
commit 999c9922b5
4 changed files with 67 additions and 34 deletions

7
Cargo.lock generated
View File

@ -32,6 +32,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]] [[package]]
name = "async-channel" name = "async-channel"
version = "1.6.1" version = "1.6.1"
@ -2303,6 +2309,7 @@ dependencies = [
name = "ynabifier" name = "ynabifier"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"async-imap", "async-imap",
"async-native-tls", "async-native-tls",
"async-std", "async-std",

View File

@ -24,6 +24,7 @@ chrono = "0.4.22"
regex = "1.6" regex = "1.6"
reqwest = { version = "0.11.12", features = ["json"] } reqwest = { version = "0.11.12", features = ["json"] }
url = "2.2" url = "2.2"
anyhow = "1.0"
# For annoying reasons, we must pin exactly the same versions as async-imap if we want to use # For annoying reasons, we must pin exactly the same versions as async-imap if we want to use
# their types. # their types.

View File

@ -190,9 +190,15 @@ where
debug!("idling"); debug!("idling");
let sequence_number = Self::idle_for_email(idle_handle).await?; let sequence_number = Self::idle_for_email(idle_handle).await?;
debug!("got email"); debug!("got email");
// TODO: handle these failures let send_res = sender.send(sequence_number).await;
sender.send(sequence_number).await.expect("failed to send"); if let Err(err) = send_res {
debug!("sent"); error!(
"Successfully fetched, but failed to dispatch email with sequence number {}: {}",
sequence_number, err
);
} else {
debug!("sent");
}
} }
debug!("done"); debug!("done");

View File

@ -3,7 +3,7 @@ extern crate log;
use futures::{stream::StreamExt, Future}; use futures::{stream::StreamExt, Future};
use log::LevelFilter; use log::LevelFilter;
use std::{fs::File, sync::Arc}; use std::{fs::File, process, sync::Arc};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use ynabifier::parse::Transaction; use ynabifier::parse::Transaction;
use ynabifier::{ use ynabifier::{
@ -14,39 +14,28 @@ use ynabifier::{
}; };
fn main() { fn main() {
let config_file = File::open("config.yml").expect("failed to open config file"); let config_res = load_config("config.yml");
let config = if let Err(err) = config_res {
serde_yaml::from_reader::<_, Config>(config_file).expect("failed to parse config file"); eprintln!("Failed to load configuration: {}", err);
process::exit(1);
}
setup_logger(config.log_level()).expect("failed to seutp logger"); let config = config_res.unwrap();
if let Err(err) = setup_logger(config.log_level()) {
eprintln!("Failed to setup logger: {}", err);
process::exit(1);
}
let runtime = Runtime::new().expect("failed to create runtime"); if let Err(err) = listen_for_transactions(&config) {
runtime.block_on(async move { error!("Failed to listen for transaction emails: {}", err);
let ynab_client = YNABClient::new(config.ynab().personal_access_token().to_string()); process::exit(2);
let mut stream = }
ynabifier::stream_new_messages(Arc::new(TokioSpawner), config.imap().clone()) }
.await
.expect("failed to setup stream");
let accounts = config.ynab().accounts(); fn load_config(filename: &str) -> Result<Config, anyhow::Error> {
while let Some(msg) = stream.next().await { let config_file = File::open(filename)?;
if let Some((account, transaction)) = try_parse_email(accounts.iter(), &msg) { let config = serde_yaml::from_reader(config_file)?;
info!( Ok(config)
"Parsed transaction for {} to {} with parser {}",
transaction.amount(),
transaction.payee(),
account.parser_name(),
);
submit_transaction(
&ynab_client,
&transaction,
config.ynab().budeget_id(),
account.id(),
)
.await;
}
}
});
} }
fn setup_logger(log_level: LevelFilter) -> Result<(), fern::InitError> { fn setup_logger(log_level: LevelFilter) -> Result<(), fern::InitError> {
@ -68,6 +57,36 @@ fn setup_logger(log_level: LevelFilter) -> Result<(), fern::InitError> {
Ok(()) Ok(())
} }
fn listen_for_transactions(config: &Config) -> Result<(), anyhow::Error> {
let runtime = Runtime::new()?;
runtime.block_on(async move {
let ynab_client = YNABClient::new(config.ynab().personal_access_token().to_string());
let mut stream =
ynabifier::stream_new_messages(Arc::new(TokioSpawner), config.imap().clone()).await?;
let accounts = config.ynab().accounts();
while let Some(msg) = stream.next().await {
if let Some((account, transaction)) = try_parse_email(accounts.iter(), &msg) {
info!(
"Parsed transaction for {} to {} with parser {}",
transaction.amount(),
transaction.payee(),
account.parser_name(),
);
submit_transaction(
&ynab_client,
&transaction,
config.ynab().budeget_id(),
account.id(),
)
.await;
}
}
Ok(())
})
}
fn try_parse_email<'a, I>(ynab_accounts: I, msg: &Message) -> Option<(&'a YNABAccount, Transaction)> fn try_parse_email<'a, I>(ynab_accounts: I, msg: &Message) -> Option<(&'a YNABAccount, Transaction)>
where where
I: Iterator<Item = &'a YNABAccount>, I: Iterator<Item = &'a YNABAccount>,