use super::lookup::Lookup; use super::transform; use super::types; use crate::Transaction; pub struct Ynab { token: String, webclient: reqwest::blocking::Client, } impl Ynab { pub fn new(token: &str) -> Ynab { Ynab { token: token.to_owned(), webclient: reqwest::blocking::Client::new(), } } 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))?, ) .map_err(|e| format!("parse error: {}", e))?; let plans = res.data.plans; plans .into_iter() .find(|p| p.name == name) .map(|p| p.id) .ok_or_else(|| "no matching plan found".to_string()) } } } fn resolve_account(&self, plan_id: &str, account: Lookup) -> Result { match account { Lookup::Id(id) => Ok(id), Lookup::Name(name) => { let url = format!("plans/{}/accounts", plan_id); let res: types::YnabResponse = serde_json::from_str( &self .get(&url) .map_err(|e| format!("request error: {}", e))?, ) .map_err(|e| format!("parse error: {}", e))?; let accounts = res.data.accounts; accounts .into_iter() .find(|a| a.name == name) .map(|a| a.id) .ok_or_else(|| "no matching account found".to_string()) } } } pub(crate) 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()?; Ok(text) } 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 .webclient .post(request) .body(body.to_owned()) .bearer_auth(&self.token) .header("Content-Type", "application/json") .send() .map_err(|e| format!("post error: {}", e))?; if res.status().is_success() { Ok(()) } else { Err(format!( "api error: {}: {}", res.status(), res.text() .map_err(|e| format!("error retrieving api error: {}", e))? )) } } pub fn list_plans(&self) -> Result, String> { let url = String::from("plans"); let res: types::YnabResponse = serde_json::from_str( &self .get(&url) .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)?; let url = format!("plans/{}/accounts", plan_id); let res: types::YnabResponse = serde_json::from_str( &self .get(&url) .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 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 request = format!("plans/{}/transactions", plan_id); let body = serde_json::to_string(&types::YnabTransactionList { transactions: transform::ynab_transactions(transactions, &account_id), }) .map_err(|e| format!("transaction format error: {}", e))?; println!("{}", body); self.post(&request, &body)?; Ok(()) } }