Refactor and add new commands and doc comments

This commit is contained in:
2026-05-01 17:24:29 +02:00
parent e9d6aa2e31
commit ec77c27700
4 changed files with 213 additions and 168 deletions
+98 -79
View File
@@ -1,5 +1,4 @@
use clap::{Parser, Subcommand};
//use directories::ProjectDirs;
use std::{
error::Error,
fs::File,
@@ -7,102 +6,83 @@ use std::{
path::PathBuf,
};
use ynabmunger::ynab;
use ynabmunger::{Transaction, read_transactions};
fn output_csv(transactions: &[Transaction]) -> Result<(), Box<dyn Error>> {
let mut writer = csv::Writer::from_writer(stdout());
writer
.write_record(["Date", "Payee", "Memo", "Amount"])
.unwrap();
for transaction in transactions {
let output = transaction.to_record();
writer.write_record(output).unwrap();
}
writer.flush().unwrap();
Ok(())
}
#[derive(Debug)]
enum Output {
Csv,
Ynab {
token: String,
plan: ynab::Lookup,
account: ynab::Lookup,
},
}
use ynabmunger::Transaction;
use ynabmunger::csv::{output_csv, read_transactions};
use ynabmunger::ynab::{Lookup, Ynab};
#[derive(Subcommand)]
enum Command {
Csv {
inputs: Vec<PathBuf>,
/// List available plans in YNAB
Plans {
/// Your YNAB token, available from developer settings in YNAB
#[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)]
token: String,
},
Ynab {
#[arg(short, long, env = "YNAB_API_TOKEN")]
/// List available accounts in a given plan in YNAB
Accounts {
/// Your YNAB token, available from developer settings in YNAB
#[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)]
token: String,
#[arg(short, long, help = "Plan name", group = "planref")]
/// The name of the YNAB plan to list
#[arg(short, long, group = "planref")]
plan: Option<String>,
#[arg(short = 'P', long, help = "Plan ID", group = "planref")]
/// Alternatively, give the YNAB plan ID directly
#[arg(short = 'P', long, group = "planref")]
plan_id: Option<String>,
},
/// Convert from a bank export to a CSV you can import manually to YNAB
Convert {
#[arg(short, long, help = "Bank export format", default_value_t = String::from("bulder"))]
format: String,
inputs: Vec<PathBuf>,
},
/// Read a bank export and import it directly to an account in ynab
Import {
/// Your YNAB token, available from developer settings in YNAB
#[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)]
token: String,
/// The name of the YNAB plan to import to
#[arg(short, long, group = "planref")]
plan: Option<String>,
/// Alternatively, give the YNAB plan ID directly
#[arg(short = 'P', long, group = "planref")]
plan_id: Option<String>,
#[arg(short, long, help = "Account name", group = "accountref")]
/// The name of the YNAB account to import to
#[arg(short, long, group = "accountref")]
account: Option<String>,
#[arg(short = 'A', long, help = "Account ID", group = "accountref")]
/// Alternatively, give the YNAB account ID directly
#[arg(short = 'A', long, group = "accountref")]
account_id: Option<String>,
/// The format of the bank export you are importing
///
/// This is different for every bank, and sometimes even the same
/// bank can have different formats for different account types.
#[arg(short, long, default_value_t = String::from("bulder"))]
format: String,
inputs: Vec<PathBuf>,
},
}
#[derive(Parser)]
struct Cli {
#[arg(short, long, help = "Bank export format", default_value_t = String::from("bulder"))]
format: String,
#[command(subcommand)]
command: Command,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
// Inject config file stuff here
let inputs: Vec<PathBuf>;
let output = match cli.command {
Command::Ynab {
plan,
plan_id,
account_id,
account,
inputs: inp,
token,
} => {
inputs = inp;
let output_plan = ynab::Lookup::try_from((plan, plan_id))?;
let output_account = ynab::Lookup::try_from((account, account_id))?;
Output::Ynab {
token,
plan: output_plan,
account: output_account,
}
}
Command::Csv { inputs: inp } => {
inputs = inp;
Output::Csv
}
};
// Config file reconciliation here?
let transactions: Vec<Transaction> = if inputs.is_empty() {
fn read_transactions_from(
inputs: &[PathBuf],
format: &str,
) -> Result<Vec<Transaction>, Box<dyn Error>> {
Ok(if inputs.is_empty() {
vec![Box::new(stdin()) as Box<dyn Read>]
} else {
inputs
@@ -111,18 +91,57 @@ fn main() -> Result<(), Box<dyn Error>> {
.collect::<Result<Vec<_>, std::io::Error>>()?
}
.iter_mut()
.map(|s| read_transactions(s, &cli.format))
.map(|s| read_transactions(s, format))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect();
.collect())
}
match output {
Output::Ynab {
fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
match cli.command {
Command::Plans { token } => {
let plans = Ynab::new(&token).list_plans()?;
println!("Available plans:");
for plan in plans {
println!(" - {}", plan);
}
Ok(())
}
Command::Accounts {
token,
plan,
plan_id,
} => {
let plan = Lookup::try_from((plan, plan_id))?;
let accounts = Ynab::new(&token).list_accounts(plan)?;
println!("Available accounts in plan:");
for account in accounts {
println!(" - {}", account);
}
Ok(())
}
Command::Import {
token,
plan,
plan_id,
account_id,
account,
} => ynab::Ynab::new(&token, plan, account)?.upload(&transactions),
Output::Csv => output_csv(&transactions),
format,
inputs,
} => {
let plan = Lookup::try_from((plan, plan_id))?;
let account = Lookup::try_from((account, account_id))?;
let transactions = read_transactions_from(&inputs, &format)?;
Ynab::new(&token).upload(&transactions, plan, account)
}
Command::Convert { format, inputs } => {
let transactions = read_transactions_from(&inputs, &format)?;
output_csv(stdout(), &transactions)
}
}
}