diff --git a/Cargo.lock b/Cargo.lock index 6a7e62e..2b5164d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1446,9 +1446,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -1954,6 +1966,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9a35cfb..04456bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ directories = "6.0.0" reqwest = { version = "0.13.3", features = ["blocking"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" +tokio = { version = "1.52.1", features = ["macros", "rt-multi-thread"] } diff --git a/src/main.rs b/src/main.rs index 55ea2e7..9e67130 100644 --- a/src/main.rs +++ b/src/main.rs @@ -123,12 +123,13 @@ fn read_transactions_from( .collect()) } -fn main() -> Result<(), Box> { +#[tokio::main] +async fn main() -> Result<(), Box> { let cli = Cli::parse(); match cli.command { Command::Plans { token } => { - let plans = Ynab::new(&token).list_plans()?; + let plans = Ynab::new(&token).list_plans().await?; println!("Available plans:"); for plan in plans { println!(" - {}", plan); @@ -141,7 +142,7 @@ fn main() -> Result<(), Box> { plan_id, } => { let plan = Lookup::from_options(plan, plan_id)?; - let accounts = Ynab::list_accounts(&Ynab::new(&token), plan)?; + let accounts = Ynab::list_accounts(&Ynab::new(&token), plan).await?; println!("Available accounts in plan:"); for account in accounts { println!(" - {}", account); @@ -169,7 +170,7 @@ fn main() -> Result<(), Box> { Some(Lookup::from_options(name, id)?) } }; - let transactions = Ynab::new(&token).list_transactions(plan, account)?; + let transactions = Ynab::new(&token).list_transactions(plan, account).await?; println!("Transactions{} in the last 30 days:", accountstr); for t in transactions { println!("{},{},{}", t.date, t.payee, t.amount); @@ -191,7 +192,7 @@ fn main() -> Result<(), Box> { let transactions = read_transactions_from(&inputs, &format)?; - Ynab::new(&token).upload(&transactions, plan, account) + Ynab::new(&token).upload(&transactions, plan, account).await } Command::Convert { format, inputs } => { let transactions = read_transactions_from(&inputs, &format)?; diff --git a/src/ynab/client.rs b/src/ynab/client.rs index 491e2a0..94d3381 100644 --- a/src/ynab/client.rs +++ b/src/ynab/client.rs @@ -5,24 +5,27 @@ use crate::Transaction; pub struct Ynab { token: String, - webclient: reqwest::blocking::Client, + webclient: reqwest::Client, } impl Ynab { pub fn new(token: &str) -> Ynab { Ynab { token: token.to_owned(), - webclient: reqwest::blocking::Client::new(), + webclient: reqwest::Client::new(), } } - fn resolve_plan(&self, plan: Lookup) -> Result { + async fn resolve_plan(&self, plan: Lookup) -> Result { match plan { Lookup::Id(id) => Ok(id), Lookup::Name(name) => { let url = "plans"; let res: types::YnabResponse = serde_json::from_str( - &self.get(url).map_err(|e| format!("request error: {}", e))?, + &self + .get(url) + .await + .map_err(|e| format!("request error: {}", e))?, ) .map_err(|e| format!("parse error: {}", e))?; let plans = res.data.plans; @@ -35,7 +38,7 @@ impl Ynab { } } - fn resolve_account(&self, plan_id: &str, account: Lookup) -> Result { + async fn resolve_account(&self, plan_id: &str, account: Lookup) -> Result { match account { Lookup::Id(id) => Ok(id), Lookup::Name(name) => { @@ -43,6 +46,7 @@ impl Ynab { let res: types::YnabResponse = serde_json::from_str( &self .get(&url) + .await .map_err(|e| format!("request error: {}", e))?, ) .map_err(|e| format!("parse error: {}", e))?; @@ -56,19 +60,20 @@ impl Ynab { } } - pub(crate) fn get(&self, request: &str) -> Result { + pub(crate) async fn get(&self, request: &str) -> Result { const URL: &str = "https://api.ynab.com/v1/"; let request = format!("{}{}", URL, request); let res = self .webclient .get(request) .bearer_auth(&self.token) - .send()?; - let text = res.text()?; + .send() + .await?; + let text = res.text().await?; Ok(text) } - fn post(&self, request: &str, body: &str) -> Result<(), String> { + async fn post(&self, request: &str, body: &str) -> Result<(), String> { const URL: &str = "https://api.ynab.com/v1/"; let request = format!("{}{}", URL, request); let res = self @@ -78,12 +83,14 @@ impl Ynab { .bearer_auth(&self.token) .header("Content-Type", "application/json") .send() + .await .map_err(|e| format!("post error: {}", e))?; if res.status().is_success() { println!( "successful api response {}: {}", res.status(), res.text() + .await .map_err(|e| format!("error retrieving api error: {}", e))? ); Ok(()) @@ -92,48 +99,54 @@ impl Ynab { "api error: {}: {}", res.status(), res.text() + .await .map_err(|e| format!("error retrieving api error: {}", e))? )) } } - pub fn list_plans(&self) -> Result, String> { + pub async fn list_plans(&self) -> Result, String> { let url = String::from("plans"); let res: types::YnabResponse = serde_json::from_str( &self .get(&url) + .await .map_err(|e| format!("request error: {}", e))?, ) .map_err(|e| format!("parse error: {}", e))?; Ok(res.data.plans.into_iter().map(|p| p.name).collect()) } - pub fn list_accounts(&self, plan: Lookup) -> Result, String> { - let plan_id = self.resolve_plan(plan)?; + pub async fn list_accounts(&self, plan: Lookup) -> Result, String> { + let plan_id = self.resolve_plan(plan).await?; let url = format!("plans/{}/accounts", plan_id); let res: types::YnabResponse = serde_json::from_str( &self .get(&url) + .await .map_err(|e| format!("request error: {}", e))?, ) .map_err(|e| format!("parse error: {}", e))?; Ok(res.data.accounts.into_iter().map(|p| p.name).collect()) } - pub fn list_transactions( + pub async fn list_transactions( &self, plan: Lookup, account: Option, ) -> Result, String> { - let plan_id = self.resolve_plan(plan)?; + let plan_id = self.resolve_plan(plan).await?; let account_id = match account { - Some(account) => Some(self.resolve_account(&plan_id, account)?), + Some(account) => Some(self.resolve_account(&plan_id, account).await?), None => None, }; let since_date = chrono::Local::now().date_naive() - chrono::Duration::days(30); let since_date = since_date.format("%Y-%m-%d"); let url = format!("plans/{}/transactions?since_date={}", plan_id, since_date); - let raw = self.get(&url).map_err(|e| format!("request error {}", e))?; + let raw = self + .get(&url) + .await + .map_err(|e| format!("request error {}", e))?; let res: types::YnabResponse = serde_json::from_str(&raw).map_err(|e| format!("parse error: {}", e))?; Ok(transform::from_ynab_transactions( @@ -142,20 +155,20 @@ impl Ynab { )) } - pub fn upload( + pub async fn upload( &self, transactions: &[Transaction], plan: Lookup, account: Lookup, ) -> Result<(), Box> { - let plan_id = self.resolve_plan(plan)?; - let account_id = self.resolve_account(&plan_id, account)?; + let plan_id = self.resolve_plan(plan).await?; + let account_id = self.resolve_account(&plan_id, account).await?; let request = format!("plans/{}/transactions", plan_id); let body = serde_json::to_string(&types::YnabTransactionList { transactions: transform::to_ynab_transactions(transactions, &account_id), }) .map_err(|e| format!("transaction format error: {}", e))?; - self.post(&request, &body)?; + self.post(&request, &body).await?; Ok(()) } }