okane_core/report/
process.rs1use 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#[derive(Debug, Default)]
16pub struct ProcessOptions {
18 pub price_db_path: Option<PathBuf>,
20}
21
22pub 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}