diff --git a/src/main.rs b/src/main.rs index 0c6dd57..462026e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -115,7 +115,7 @@ fn main() -> Result<(), Box> { plan, plan_id, } => { - let plan = Lookup::try_from((plan, plan_id))?; + let plan = Lookup::from_options(plan, plan_id)?; let accounts = Ynab::new(&token).list_accounts(plan)?; println!("Available accounts in plan:"); for account in accounts { @@ -132,8 +132,8 @@ fn main() -> Result<(), Box> { format, inputs, } => { - let plan = Lookup::try_from((plan, plan_id))?; - let account = Lookup::try_from((account, account_id))?; + let plan = Lookup::from_options(plan, plan_id)?; + let account = Lookup::from_options(account, account_id)?; let transactions = read_transactions_from(&inputs, &format)?; diff --git a/src/ynab.rs b/src/ynab/client.rs similarity index 61% rename from src/ynab.rs rename to src/ynab/client.rs index b4fe59b..556fbf9 100644 --- a/src/ynab.rs +++ b/src/ynab/client.rs @@ -1,65 +1,7 @@ +use super::lookup::Lookup; +use super::transform; +use super::types; use crate::Transaction; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Debug, Serialize)] -struct YnabTransaction { - import_id: String, - date: String, - amount: i64, - payee_name: String, - account_id: String, -} - -#[derive(Serialize, Deserialize, Debug)] -struct YnabResponse { - data: T, -} - -#[derive(Deserialize, Debug)] -struct YnabPlanList { - plans: Vec, -} - -#[derive(Deserialize, Debug)] -struct YnabPlan { - id: String, - name: String, -} - -#[derive(Deserialize, Debug)] -struct YnabAccountList { - accounts: Vec, -} - -#[derive(Deserialize, Debug)] -struct YnabAccount { - id: String, - name: String, -} - -#[derive(Serialize, Debug)] -struct YnabTransactionList { - transactions: Vec, -} - -#[derive(Debug)] -pub enum Lookup { - Name(String), - Id(String), -} - -impl TryFrom<(Option, Option)> for Lookup { - type Error = String; - - fn try_from((name, id): (Option, Option)) -> Result { - match (name, id) { - (Some(name), _) => Ok(Lookup::Name(name)), - (_, Some(id)) => Ok(Lookup::Id(id)), - _ => Err("must provide name or id".to_owned()), - } - } -} pub struct Ynab { token: String, @@ -79,7 +21,7 @@ impl Ynab { Lookup::Id(id) => Ok(id), Lookup::Name(name) => { let url = "plans"; - let res: YnabResponse = serde_json::from_str( + 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))?; @@ -98,7 +40,7 @@ impl Ynab { Lookup::Id(id) => Ok(id), Lookup::Name(name) => { let url = format!("plans/{}/accounts", plan_id); - let res: YnabResponse = serde_json::from_str( + let res: types::YnabResponse = serde_json::from_str( &self .get(&url) .map_err(|e| format!("request error: {}", e))?, @@ -114,7 +56,7 @@ impl Ynab { } } - fn get(&self, request: &str) -> Result { + pub(crate) fn get(&self, request: &str) -> Result { const URL: &str = "https://api.ynab.com/v1/"; let request = format!("{}{}", URL, request); let res = self @@ -151,7 +93,7 @@ impl Ynab { pub fn list_plans(&self) -> Result, String> { let url = String::from("plans"); - let res: YnabResponse = serde_json::from_str( + let res: types::YnabResponse = serde_json::from_str( &self .get(&url) .map_err(|e| format!("request error: {}", e))?, @@ -163,7 +105,7 @@ impl Ynab { 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: YnabResponse = serde_json::from_str( + let res: types::YnabResponse = serde_json::from_str( &self .get(&url) .map_err(|e| format!("request error: {}", e))?, @@ -172,32 +114,6 @@ impl Ynab { Ok(res.data.accounts.into_iter().map(|p| p.name).collect()) } - fn ynab_transactions( - &self, - transactions: &[Transaction], - account_id: &str, - ) -> Vec { - let mut result = Vec::new(); - let mut idmap: HashMap<(String, i64), u32> = HashMap::new(); - for t in transactions { - let key = (t.date.clone(), t.amount); - let id = { - let n = idmap.entry(key).or_insert(0); - *n += 1; - *n - }; - let y = YnabTransaction { - import_id: format!("YNAB:{}:{}:{}", t.amount, t.date, id), - date: t.date.clone(), - amount: t.amount, - payee_name: t.payee.clone(), - account_id: account_id.to_string(), - }; - result.push(y); - } - result - } - pub fn upload( &self, transactions: &[Transaction], @@ -207,8 +123,8 @@ impl Ynab { 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(&YnabTransactionList { - transactions: self.ynab_transactions(transactions, &account_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); diff --git a/src/ynab/lookup.rs b/src/ynab/lookup.rs new file mode 100644 index 0000000..66e4597 --- /dev/null +++ b/src/ynab/lookup.rs @@ -0,0 +1,14 @@ +pub enum Lookup { + Name(String), + Id(String), +} + +impl Lookup { + pub fn from_options(name: Option, id: Option) -> Result { + match (name, id) { + (Some(name), _) => Ok(Self::Name(name)), + (_, Some(id)) => Ok(Self::Id(id)), + _ => Err("must provide name or id".to_owned()), + } + } +} diff --git a/src/ynab/mod.rs b/src/ynab/mod.rs new file mode 100644 index 0000000..1a97bbc --- /dev/null +++ b/src/ynab/mod.rs @@ -0,0 +1,9 @@ +mod lookup; +pub use lookup::Lookup; + +mod types; + +mod client; +pub use client::Ynab; + +mod transform; diff --git a/src/ynab/transform.rs b/src/ynab/transform.rs new file mode 100644 index 0000000..ae03b5f --- /dev/null +++ b/src/ynab/transform.rs @@ -0,0 +1,28 @@ +use super::types; +use crate::Transaction; +use std::collections::HashMap; + +pub(crate) fn ynab_transactions( + transactions: &[Transaction], + account_id: &str, +) -> Vec { + let mut result = Vec::new(); + let mut idmap: HashMap<(String, i64), u32> = HashMap::new(); + for t in transactions { + let key = (t.date.clone(), t.amount); + let id = { + let n = idmap.entry(key).or_insert(0); + *n += 1; + *n + }; + let y = types::YnabTransaction { + import_id: format!("YNAB:{}:{}:{}", t.amount, t.date, id), + date: t.date.clone(), + amount: t.amount, + payee_name: t.payee.clone(), + account_id: account_id.to_string(), + }; + result.push(y); + } + result +} diff --git a/src/ynab/types.rs b/src/ynab/types.rs new file mode 100644 index 0000000..36c92f5 --- /dev/null +++ b/src/ynab/types.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +pub(crate) struct YnabTransaction { + pub(crate) import_id: String, + pub(crate) date: String, + pub(crate) amount: i64, + pub(crate) payee_name: String, + pub(crate) account_id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct YnabResponse { + pub(crate) data: T, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct YnabPlanList { + pub(crate) plans: Vec, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct YnabPlan { + pub(crate) id: String, + pub(crate) name: String, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct YnabAccountList { + pub(crate) accounts: Vec, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct YnabAccount { + pub(crate) id: String, + pub(crate) name: String, +} + +#[derive(Serialize, Debug)] +pub(crate) struct YnabTransactionList { + pub(crate) transactions: Vec, +}