use std::borrow::Borrow;
use std::path::PathBuf;
use crate::{load, syntax};
use super::balance::Balance;
use super::book_keeping::{self, BookKeepError};
use super::context::ReportContext;
use super::error::{self, ReportError};
use super::price_db::PriceRepositoryBuilder;
use super::query::Ledger;
use super::transaction::Transaction;
#[derive(Debug, Default)]
pub struct ProcessOptions {
pub price_db_path: Option<PathBuf>,
}
pub fn process<'ctx, L, F>(
ctx: &mut ReportContext<'ctx>,
loader: L,
options: &ProcessOptions,
) -> Result<Ledger<'ctx>, ReportError>
where
L: Borrow<load::Loader<F>>,
F: load::FileSystem,
{
let mut accum = ProcessAccumulator::new();
loader.borrow().load(|path, pctx, entry| {
accum.process(ctx, entry).map_err(|berr| {
ReportError::BookKeep(
berr,
error::ErrorContext::new(
loader.borrow().error_style().clone(),
path.to_owned(),
pctx,
),
)
})
})?;
ctx.account_tree.construct(&ctx.accounts);
if let Some(price_db_path) = options.price_db_path.as_deref() {
accum
.price_repos
.load_price_db(ctx, loader.borrow().filesystem(), price_db_path)?;
}
Ok(Ledger {
arena: ctx.arena,
transactions: accum.txns,
date_sorted_txns: None,
raw_balance: accum.balance,
price_repos: accum.price_repos.build(),
})
}
struct ProcessAccumulator<'ctx> {
balance: Balance<'ctx>,
txns: Vec<Transaction<'ctx>>,
price_repos: PriceRepositoryBuilder<'ctx>,
}
impl<'ctx> ProcessAccumulator<'ctx> {
fn new() -> Self {
Self {
balance: Balance::default(),
txns: Vec::new(),
price_repos: PriceRepositoryBuilder::default(),
}
}
fn process(
&mut self,
ctx: &mut ReportContext<'ctx>,
entry: &syntax::tracked::LedgerEntry,
) -> Result<(), BookKeepError> {
match &entry.statement {
syntax::LedgerStatement::Txn(txn) => {
self.txns.push(book_keeping::add_transaction(
ctx,
&mut self.price_repos,
&mut self.balance,
txn,
)?);
Ok(())
}
syntax::LedgerStatement::Account(account) => process_account(ctx, account),
syntax::LedgerStatement::Commodity(commodity) => process_commodity(ctx, commodity),
_ => Ok(()),
}
}
}
pub fn process_account<'ctx>(
ctx: &mut ReportContext<'ctx>,
account: &syntax::AccountDeclaration<'_>,
) -> Result<(), BookKeepError> {
let canonical = ctx.accounts.ensure(&account.name);
for ad in &account.details {
if let syntax::AccountDetail::Alias(alias) = ad {
ctx.accounts
.register_alias(alias, canonical)
.map_err(|_| BookKeepError::InvalidAccountAlias(alias.to_string()))?;
}
}
Ok(())
}
fn process_commodity<'ctx>(
ctx: &mut ReportContext<'ctx>,
commodity: &syntax::CommodityDeclaration<'_>,
) -> Result<(), BookKeepError> {
let canonical = ctx.commodities.ensure(&commodity.name);
for cd in &commodity.details {
match cd {
syntax::CommodityDetail::Alias(alias) => {
ctx.commodities
.register_alias(alias, canonical)
.map_err(|_| BookKeepError::InvalidCommodityAlias(alias.to_string()))?;
}
syntax::CommodityDetail::Format(format_amount) => {
ctx.commodities.set_format(canonical, format_amount.value);
}
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use assert_matches::assert_matches;
use bumpalo::Bump;
use super::super::account::{AccountAggregate, AccountTreeKey};
#[test]
fn process_constructs_account_tree() {
let arena = Bump::new();
let mut ctx = ReportContext::new(&arena);
let input =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../testdata/report/multi_commodity.ledger");
let _ = process(
&mut ctx,
load::new_loader(input),
&ProcessOptions::default(),
)
.unwrap();
let parent = ctx.account_aggregate("Assets:Banks").unwrap();
let child1 = ctx.account_aggregate("Assets:Banks:Swiss Bank").unwrap();
let child2 = ctx.account_aggregate("Assets:Banks:あおによし").unwrap();
assert_matches!(parent, AccountAggregate::Ancestor(_));
assert_matches!(child1, AccountAggregate::Account(_));
assert_matches!(child2, AccountAggregate::Account(_));
let children: &[_] = &[child1, child2];
assert_eq!(Some(children), ctx.account_tree.children(parent));
assert_eq!(
Some(AccountTreeKey::Descendant(parent)),
ctx.account_tree.parent(child1)
);
}
}