191 lines
5.7 KiB
Rust
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(())
|
|
}
|
|
}
|