Files
ynabmunger/src/ynab/client.rs
T

191 lines
5.7 KiB
Rust

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<String, Error> {
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<Vec<String>, Error> {
let url = String::from("plans");
let res: apitypes::Response<apitypes::PlanList> =
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, Error> {
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<Plan, Error> {
let raw = client.get("plans").await?;
let res: apitypes::Response<apitypes::PlanList> = 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, Error> {
Account::from_name(self, name).await
}
pub async fn list_accounts(&self) -> Result<Vec<String>, Error> {
let url = format!("plans/{}/accounts", self.id);
let raw = self.client.get(&url).await?;
let res: apitypes::Response<apitypes::AccountList> = 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<Vec<Transaction>, 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<apitypes::TransactionList> = 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<Account, Error> {
let raw = plan
.client
.get(&format!("plans/{}/accounts", &plan.id))
.await?;
let res: apitypes::Response<apitypes::AccountList> = 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<Vec<Transaction>, 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(())
}
}