Change object model and add crate Error type
This commit is contained in:
+120
-113
@@ -1,66 +1,23 @@
|
||||
use super::lookup::Lookup;
|
||||
use super::apitypes;
|
||||
use super::transform;
|
||||
use super::types;
|
||||
use crate::Error;
|
||||
use crate::Transaction;
|
||||
|
||||
pub struct Ynab {
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
token: String,
|
||||
webclient: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Ynab {
|
||||
pub fn new(token: &str) -> Ynab {
|
||||
Ynab {
|
||||
impl Client {
|
||||
pub fn new(token: &str) -> Client {
|
||||
Client {
|
||||
token: token.to_owned(),
|
||||
webclient: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_plan(&self, plan: Lookup) -> Result<String, String> {
|
||||
match plan {
|
||||
Lookup::Id(id) => Ok(id),
|
||||
Lookup::Name(name) => {
|
||||
let url = "plans";
|
||||
let res: types::YnabResponse<types::YnabPlanList> = serde_json::from_str(
|
||||
&self
|
||||
.get(url)
|
||||
.await
|
||||
.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_account(&self, plan_id: &str, account: Lookup) -> Result<String, String> {
|
||||
match account {
|
||||
Lookup::Id(id) => Ok(id),
|
||||
Lookup::Name(name) => {
|
||||
let url = format!("plans/{}/accounts", plan_id);
|
||||
let res: types::YnabResponse<types::YnabAccountList> = serde_json::from_str(
|
||||
&self
|
||||
.get(&url)
|
||||
.await
|
||||
.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) async fn get(&self, request: &str) -> Result<String, reqwest::Error> {
|
||||
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
|
||||
@@ -73,7 +30,7 @@ impl Ynab {
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
async fn post(&self, request: &str, body: &str) -> Result<(), String> {
|
||||
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
|
||||
@@ -83,92 +40,142 @@ impl Ynab {
|
||||
.bearer_auth(&self.token)
|
||||
.header("Content-Type", "application/json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("post error: {}", e))?;
|
||||
.await?;
|
||||
if res.status().is_success() {
|
||||
println!(
|
||||
"successful api response {}: {}",
|
||||
res.status(),
|
||||
res.text()
|
||||
.await
|
||||
.map_err(|e| format!("error retrieving api error: {}", e))?
|
||||
);
|
||||
// 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(format!(
|
||||
Err(Error::Text(format!(
|
||||
"api error: {}: {}",
|
||||
res.status(),
|
||||
res.text()
|
||||
.await
|
||||
.map_err(|e| format!("error retrieving api error: {}", e))?
|
||||
))
|
||||
res.text().await?
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_plans(&self) -> Result<Vec<String>, String> {
|
||||
pub async fn list_plans(&self) -> Result<Vec<String>, Error> {
|
||||
let url = String::from("plans");
|
||||
let res: types::YnabResponse<types::YnabPlanList> = serde_json::from_str(
|
||||
&self
|
||||
.get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("request error: {}", e))?,
|
||||
)
|
||||
.map_err(|e| format!("parse error: {}", e))?;
|
||||
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 async fn list_accounts(&self, plan: Lookup) -> Result<Vec<String>, String> {
|
||||
let plan_id = self.resolve_plan(plan).await?;
|
||||
let url = format!("plans/{}/accounts", plan_id);
|
||||
let res: types::YnabResponse<types::YnabAccountList> = serde_json::from_str(
|
||||
&self
|
||||
.get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("request error: {}", e))?,
|
||||
)
|
||||
.map_err(|e| format!("parse error: {}", e))?;
|
||||
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,
|
||||
plan: Lookup,
|
||||
account: Option<Lookup>,
|
||||
) -> Result<Vec<Transaction>, String> {
|
||||
let plan_id = self.resolve_plan(plan).await?;
|
||||
let account_id = match account {
|
||||
Some(account) => Some(self.resolve_account(&plan_id, account).await?),
|
||||
None => None,
|
||||
};
|
||||
account_id: Option<&str>,
|
||||
) -> Result<Vec<Transaction>, Error> {
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| format!("request error {}", e))?;
|
||||
let res: types::YnabResponse<types::YnabTransactionList> =
|
||||
serde_json::from_str(&raw).map_err(|e| format!("parse error: {}", e))?;
|
||||
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.as_deref(),
|
||||
account_id,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload(
|
||||
&self,
|
||||
transactions: &[Transaction],
|
||||
plan: Lookup,
|
||||
account: Lookup,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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),
|
||||
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,
|
||||
})
|
||||
.map_err(|e| format!("transaction format error: {}", e))?;
|
||||
self.post(&request, &body).await?;
|
||||
}
|
||||
|
||||
pub async fn list_transactions(&self) -> Result<Vec<Transaction>, Error> {
|
||||
self.plan.list_transactions(Some(&self.id)).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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user