Skip to main content

okane_core/report/
process.rs

1use std::borrow::Borrow;
2use std::path::PathBuf;
3
4use crate::{load, syntax};
5
6use super::balance::Balance;
7use super::book_keeping::{self, BookKeepError};
8use super::context::ReportContext;
9use super::error::{self, ReportError};
10use super::price_db::PriceRepositoryBuilder;
11use super::query::Ledger;
12use super::transaction::Transaction;
13
14/// Options to control process behavior.
15#[derive(Debug, Default)]
16// TODO: non_exhaustive
17pub struct ProcessOptions {
18    /// Path to the price DB file.
19    pub price_db_path: Option<PathBuf>,
20}
21
22/// Takes the loader, and gives back the all read transactions.
23/// Also returns the computed balance, as a side-artifact.
24/// Usually this needs to be reordered, so just returning a `Vec`.
25pub fn process<'ctx, L, F>(
26    ctx: &mut ReportContext<'ctx>,
27    loader: L,
28    options: &ProcessOptions,
29) -> Result<Ledger<'ctx>, ReportError>
30where
31    L: Borrow<load::Loader<F>>,
32    F: load::FileSystem,
33{
34    let mut accum = ProcessAccumulator::new();
35    loader.borrow().load(|path, pctx, entry| {
36        accum.process(ctx, entry).map_err(|berr| {
37            ReportError::BookKeep(
38                berr,
39                error::ErrorContext::new(
40                    loader.borrow().error_style().clone(),
41                    path.to_owned(),
42                    pctx,
43                ),
44            )
45        })
46    })?;
47    ctx.account_tree.construct(&ctx.accounts);
48    if let Some(price_db_path) = options.price_db_path.as_deref() {
49        accum
50            .price_repos
51            .load_price_db(ctx, loader.borrow().filesystem(), price_db_path)?;
52    }
53    Ok(Ledger {
54        arena: ctx.arena,
55        transactions: accum.txns,
56        date_sorted_txns: None,
57        raw_balance: accum.balance,
58        price_repos: accum.price_repos.build(),
59    })
60}
61
62struct ProcessAccumulator<'ctx> {
63    balance: Balance<'ctx>,
64    txns: Vec<Transaction<'ctx>>,
65    price_repos: PriceRepositoryBuilder<'ctx>,
66}
67
68impl<'ctx> ProcessAccumulator<'ctx> {
69    fn new() -> Self {
70        Self {
71            balance: Balance::default(),
72            txns: Vec::new(),
73            price_repos: PriceRepositoryBuilder::default(),
74        }
75    }
76
77    fn process(
78        &mut self,
79        ctx: &mut ReportContext<'ctx>,
80        entry: &syntax::tracked::LedgerEntry,
81    ) -> Result<(), BookKeepError> {
82        match &entry.statement {
83            syntax::LedgerStatement::Txn(txn) => {
84                self.txns.push(book_keeping::add_transaction(
85                    ctx,
86                    &mut self.price_repos,
87                    &mut self.balance,
88                    txn,
89                )?);
90                Ok(())
91            }
92            syntax::LedgerStatement::Account(account) => process_account(ctx, account),
93            syntax::LedgerStatement::Commodity(commodity) => process_commodity(ctx, commodity),
94            _ => Ok(()),
95        }
96    }
97}
98
99pub fn process_account<'ctx>(
100    ctx: &mut ReportContext<'ctx>,
101    account: &syntax::AccountDeclaration<'_>,
102) -> Result<(), BookKeepError> {
103    let canonical = ctx.accounts.ensure(&account.name);
104    for ad in &account.details {
105        if let syntax::AccountDetail::Alias(alias) = ad {
106            ctx.accounts
107                .register_alias(alias, canonical)
108                .map_err(|_| BookKeepError::InvalidAccountAlias(alias.to_string()))?;
109        }
110    }
111    Ok(())
112}
113
114fn process_commodity<'ctx>(
115    ctx: &mut ReportContext<'ctx>,
116    commodity: &syntax::CommodityDeclaration<'_>,
117) -> Result<(), BookKeepError> {
118    let canonical = ctx.commodities.ensure(&commodity.name);
119    for cd in &commodity.details {
120        match cd {
121            syntax::CommodityDetail::Alias(alias) => {
122                ctx.commodities
123                    .register_alias(alias, canonical)
124                    .map_err(|_| BookKeepError::InvalidCommodityAlias(alias.to_string()))?;
125            }
126            syntax::CommodityDetail::Format(format_amount) => {
127                ctx.commodities.set_format(canonical, format_amount.value);
128            }
129            _ => {}
130        }
131    }
132    Ok(())
133}
134
135#[cfg(test)]
136mod tests {
137
138    use super::*;
139
140    use std::path::Path;
141
142    use assert_matches::assert_matches;
143    use bumpalo::Bump;
144
145    use super::super::account::{AccountAggregate, AccountTreeKey};
146
147    #[test]
148    fn process_constructs_account_tree() {
149        let arena = Bump::new();
150        let mut ctx = ReportContext::new(&arena);
151        let input =
152            Path::new(env!("CARGO_MANIFEST_DIR")).join("../testdata/report/multi_commodity.ledger");
153
154        let _ = process(
155            &mut ctx,
156            load::new_loader(input),
157            &ProcessOptions::default(),
158        )
159        .unwrap();
160
161        let parent = ctx.account_aggregate("Assets:Banks").unwrap();
162        let child1 = ctx.account_aggregate("Assets:Banks:Swiss Bank").unwrap();
163        let child2 = ctx.account_aggregate("Assets:Banks:あおによし").unwrap();
164        assert_matches!(parent, AccountAggregate::Ancestor(_));
165        assert_matches!(child1, AccountAggregate::Account(_));
166        assert_matches!(child2, AccountAggregate::Account(_));
167        let children: &[_] = &[child1, child2];
168
169        assert_eq!(Some(children), ctx.account_tree.children(parent));
170        assert_eq!(
171            Some(AccountTreeKey::Descendant(parent)),
172            ctx.account_tree.parent(child1)
173        );
174    }
175}