use std::path::{Path, PathBuf};
use clap::{Parser, Subcommand, ValueEnum};
use kontochronik::{Importer, gls::GlsImporter, import, read_archive, verify_archive};
#[derive(Debug, Parser)]
#[command(version, about)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Import {
#[arg(short, long)]
archive: PathBuf,
#[arg(short, long, value_enum, default_value_t = Format::Gls)]
format: Format,
#[arg(required = true)]
files: Vec<PathBuf>,
},
Info {
archive: PathBuf,
},
Verify {
archive: PathBuf,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Format {
Gls,
}
impl Format {
const fn importer(self) -> &'static dyn Importer {
match self {
Self::Gls => &GlsImporter,
}
}
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
logger::init(cli.verbose);
match cli.command {
Command::Import {
archive,
format,
files,
} => run_import(&archive, format, &files),
Command::Info { archive } => run_info(&archive),
Command::Verify { archive } => run_verify(&archive),
}
}
fn run_import(archive: &Path, format: Format, files: &[PathBuf]) -> anyhow::Result<()> {
let importer = format.importer();
for file in files {
let summary = import(importer, file, archive)?;
println!(
"{}: {} imported, {} duplicates skipped, archive now holds {} entries",
file.display(),
summary.imported,
summary.duplicates,
summary.total
);
}
Ok(())
}
fn run_info(archive: &Path) -> anyhow::Result<()> {
let records = read_archive(archive)?;
let Some(first) = records.first() else {
println!("{}: empty archive", archive.display());
return Ok(());
};
let oldest = records
.iter()
.map(|r| r.booking_date)
.min()
.expect("non-empty");
let newest = records
.iter()
.map(|r| r.booking_date)
.max()
.expect("non-empty");
let latest = records
.iter()
.find(|r| r.booking_date == newest)
.unwrap_or(first);
println!(
"Account: {} ({})",
first.account_iban, first.account_description
);
println!(
"Bank: {} ({})",
first.account_bank_name, first.account_bic
);
println!("Entries: {}", records.len());
println!("Period: {oldest} to {newest}");
println!(
"Balance: {} {} (after booking on {newest})",
latest.balance_after_booking, latest.currency
);
Ok(())
}
fn run_verify(archive: &Path) -> anyhow::Result<()> {
let summary = verify_archive(archive)?;
println!("{}: {} entries verified", archive.display(), summary.total);
if summary.missing_fingerprints > 0 {
println!(
"{} entries have no fingerprint yet; the next import will add it",
summary.missing_fingerprints
);
}
Ok(())
}
mod logger {
use log::{LevelFilter, Log, Metadata, Record};
struct StderrLogger;
static LOGGER: StderrLogger = StderrLogger;
impl Log for StderrLogger {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &Record<'_>) {
if self.enabled(record.metadata()) {
eprintln!("[{}] {}", record.level(), record.args());
}
}
fn flush(&self) {}
}
pub fn init(verbosity: u8) {
let level = match verbosity {
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
log::set_logger(&LOGGER).expect("no other logger has been initialized");
log::set_max_level(level);
}
}