use super::apitypes; use super::transform; use crate::Error; use crate::Transaction; #[derive(Clone)] pub struct Client { token: String, webclient: reqwest::Client, } impl Client { pub fn new(token: &str) -> Client { Client { token: token.to_owned(), webclient: reqwest::Client::new(), } } 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() .await?; if !res.status().is_success() { let code = res.status().as_u16(); let message = res.status().canonical_reason().unwrap_or("unknown"); return Err(Error::ApiError { status: code, message: message.to_string(), }); } let text = res.text().await?; Ok(text) } async fn post(&self, request: &str, body: &str) -> Result<(), Error> { 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() .await?; if res.status().is_success() { // Maybe do something with the status here // println!( // "successful api response {}: {}", // res.status(), // res.text() // .await // .map_err(|e| format!("error retrieving api error: {}", e))? // ); Ok(()) } else { Err(Error::Text(format!( "api error: {}: {}", res.status(), res.text().await? ))) } } pub async fn list_plans(&self) -> Result, Error> { let url = String::from("plans"); let res: apitypes::Response = serde_json::from_str(&self.get(&url).await?)?; Ok(res.data.plans.into_iter().map(|p| p.name).collect()) } pub fn plan_from_id(self, id: &str) -> Plan { Plan::from_id(self, id) } pub async fn plan_from_name(self, name: &str) -> Result { Plan::from_name(self, name).await } } #[derive(Clone)] pub struct Plan { client: Client, id: String, } impl Plan { fn from_id(client: Client, id: &str) -> Plan { Plan { client, id: id.to_owned(), } } async fn from_name(client: Client, name: &str) -> Result { let raw = client.get("plans").await?; let res: apitypes::Response = serde_json::from_str(&raw)?; let id = res .data .plans .into_iter() .find(|p| p.name == name) .map(|p| p.id) .ok_or_else(|| Error::PlanNotFound(name.to_string()))?; Ok(Plan { client, id }) } pub fn account_from_id(&self, id: &str) -> Account { Account::from_id(self, id) } pub async fn account_from_name(&self, name: &str) -> Result { Account::from_name(self, name).await } pub async fn list_accounts(&self) -> Result, Error> { let url = format!("plans/{}/accounts", self.id); let raw = self.client.get(&url).await?; let res: apitypes::Response = serde_json::from_str(&raw)?; Ok(res.data.accounts.into_iter().map(|p| p.name).collect()) } pub async fn list_transactions( &self, account_id: Option<&str>, days: i64, ) -> Result, Error> { let since_date = chrono::Local::now().date_naive() - chrono::Duration::days(days); let since_date = since_date.format("%Y-%m-%d"); let url = format!("plans/{}/transactions?since_date={}", self.id, since_date); let raw = self.client.get(&url).await?; let res: apitypes::Response = serde_json::from_str(&raw)?; Ok(transform::from_ynab_transactions( &res.data.transactions, account_id, )) } } pub struct Account { plan: Plan, id: String, } impl Account { fn from_id(plan: &Plan, id: &str) -> Account { Account { plan: plan.clone(), id: id.to_owned(), } } async fn from_name(plan: &Plan, name: &str) -> Result { let raw = plan .client .get(&format!("plans/{}/accounts", &plan.id)) .await?; let res: apitypes::Response = serde_json::from_str(&raw)?; let id = res .data .accounts .into_iter() .find(|p| p.name == name) .map(|p| p.id) .ok_or_else(|| Error::AccountNotFound(name.to_string()))?; Ok(Account { plan: plan.clone(), id, }) } pub async fn list_transactions(&self, days: i64) -> Result, Error> { self.plan.list_transactions(Some(&self.id), days).await } pub async fn upload(&self, transactions: &[Transaction]) -> Result<(), Error> { let request = format!("plans/{}/transactions", self.plan.id); let body = serde_json::to_string(&apitypes::TransactionList { transactions: transform::to_ynab_transactions(transactions, &self.id), })?; self.plan.client.post(&request, &body).await?; Ok(()) } }