1use crate::config::overlaps::{InputOverlap, OverlapConfig, StorageOverlap};
6use crate::config::{
7 AccountSelectors, Config, Export, ExportType, Kernel, PriceLookupType, Report, ReportType,
8 StorageType,
9};
10use crate::kernel::hash::Hash;
11use crate::kernel::price_lookup::PriceLookup;
12use crate::model::TxnAccount;
13use crate::model::price_entry::PriceDb;
14use crate::model::{AccountTreeNode, Commodity};
15use crate::{config, parser, tackler};
16use jiff::Zoned;
17use std::collections::HashMap;
18use std::path::PathBuf;
19use std::sync::Arc;
20use tackler_api::txn_header::Tag;
21use tackler_api::txn_ts::GroupBy;
22use tackler_rs::normalize_extension;
23
24#[derive(Debug, Default, Clone)]
25pub struct FileInput {
26 pub path: PathBuf,
27}
28
29#[derive(Debug, Clone)]
30pub struct FsInput {
31 pub path: PathBuf,
32 pub dir: PathBuf,
33 pub ext: String,
34}
35
36#[derive(Debug, Clone)]
37pub enum GitInputSelector {
38 CommitId(String),
39 Reference(String),
40}
41
42#[derive(Debug, Clone)]
43pub struct GitInput {
44 pub repo: PathBuf,
45 pub dir: String,
46 pub git_ref: GitInputSelector,
47 pub ext: String,
48}
49
50#[derive(Debug, Clone)]
51pub enum InputSettings {
52 File(FileInput),
53 Fs(FsInput),
54 Git(GitInput),
55}
56impl Default for InputSettings {
57 fn default() -> Self {
58 Self::File(FileInput::default())
59 }
60}
61
62#[derive(Debug, Default)]
63struct Commodities {
64 names: HashMap<String, Arc<Commodity>>,
65 permit_empty_commodity: bool,
66}
67
68impl Commodities {
69 fn default_empty_ok() -> Self {
70 Commodities {
71 names: HashMap::new(),
72 permit_empty_commodity: true,
73 }
74 }
75
76 fn from(cfg: &Config) -> Result<Commodities, tackler::Error> {
77 let cfg_comm = &cfg.transaction.commodities;
78 let permit_empty_commodity = cfg_comm.permit_empty_commodity.unwrap_or(false);
79
80 let comms =
81 cfg_comm.names.iter().try_fold(
82 HashMap::new(),
83 |mut chm, comm| match Commodity::from(comm) {
84 Ok(c) => {
85 chm.insert(comm.into(), Arc::new(c));
86 Ok(chm)
87 }
88 Err(e) => {
89 let msg = format!("Invalid Chart of Commodities: {e}");
90 Err(msg)
91 }
92 },
93 )?;
94 Ok(Commodities {
95 names: comms,
96 permit_empty_commodity,
97 })
98 }
99}
100
101#[derive(Debug, Default)]
102struct AccountTrees {
103 defined_accounts: HashMap<String, Arc<AccountTreeNode>>,
104 synthetic_parents: HashMap<String, Arc<AccountTreeNode>>,
105}
106
107impl AccountTrees {
108 fn build_account_tree(
109 target_account_tree: &mut HashMap<String, Arc<AccountTreeNode>>,
110 atn: &Arc<AccountTreeNode>,
111 other_account_tree: Option<&HashMap<String, Arc<AccountTreeNode>>>,
112 ) -> Result<(), tackler::Error> {
113 let parent = atn.parent.as_str();
114 let has_parent = other_account_tree.is_some_and(|a| a.contains_key(parent))
115 || target_account_tree.contains_key(parent);
116
117 if has_parent || atn.is_root() {
118 Ok(())
120 } else {
121 let parent_atn = Arc::new(AccountTreeNode::make_parent(atn)?);
122 target_account_tree.insert(parent.to_string(), parent_atn.clone());
123
124 Self::build_account_tree(target_account_tree, &parent_atn, other_account_tree)
125 }
126 }
127
128 fn from(account_names: &[String], strict_mode: bool) -> Result<AccountTrees, tackler::Error> {
129 let mut defined_accounts =
130 account_names
131 .iter()
132 .try_fold(
133 HashMap::new(),
134 |mut accs, account| match AccountTreeNode::from(account) {
135 Ok(atn) => {
136 accs.insert(account.into(), Arc::new(atn));
137 Ok(accs)
138 }
139 Err(e) => {
140 let msg = format!("Invalid Chart of Accounts: {e}");
141 Err(msg)
142 }
143 },
144 )?;
145
146 let synthetic_parents = if strict_mode {
147 let mut sap = HashMap::new();
149 for atn_entry in &defined_accounts {
150 if !&defined_accounts.contains_key(atn_entry.1.parent.as_str()) {
151 let (_, atn) = atn_entry;
153 Self::build_account_tree(&mut sap, atn, Some(&defined_accounts))?;
154 }
155 }
156 sap
157 } else {
158 let a: Vec<Arc<AccountTreeNode>> = defined_accounts.values().cloned().collect();
159 for atn in a {
160 Self::build_account_tree(&mut defined_accounts, &atn, None)?;
161 }
162 HashMap::new()
163 };
164 Ok(AccountTrees {
165 defined_accounts,
166 synthetic_parents,
167 })
168 }
169}
170
171#[derive(Debug, Default)]
172pub struct Price {
173 pub price_db: PriceDb,
175 pub lookup_type: PriceLookupType,
176}
177
178#[derive(Debug)]
179pub struct Settings {
180 pub(crate) audit_mode: bool,
181 pub(crate) report: Report,
182 pub(crate) export: Export,
183 strict_mode: bool,
184 pub(crate) inverted: bool,
185 input_config: InputSettings,
186 kernel: Kernel,
187 pub price: Price,
188 price_lookup: PriceLookup,
189 global_acc_sel: Option<AccountSelectors>,
190 accounts: AccountTrees,
191 commodities: Commodities,
192 tags: HashMap<String, Arc<Tag>>,
193}
194
195impl Default for Settings {
196 fn default() -> Self {
197 Settings {
198 strict_mode: false,
199 audit_mode: false,
200 inverted: false,
201 input_config: InputSettings::default(),
202 report: Report::default(),
203 export: Export::default(),
204 kernel: Kernel::default(),
205 price: Price::default(),
206 price_lookup: PriceLookup::default(),
207 global_acc_sel: None,
208 accounts: AccountTrees::default(),
209 commodities: Commodities::default_empty_ok(),
210 tags: HashMap::new(),
211 }
212 }
213}
214
215impl Settings {
216 #[must_use]
217 pub fn default_audit() -> Self {
218 Settings {
219 audit_mode: true,
220 ..Self::default()
221 }
222 }
223 #[must_use]
224 pub fn default_extid() -> Self {
225 let mut s = Settings::default();
226 s.kernel.extid.unique = true;
227 s
228 }
229}
230
231impl Settings {
232 #[allow(clippy::too_many_lines)]
235 pub fn try_from(cfg: Config, overlaps: OverlapConfig) -> Result<Settings, tackler::Error> {
236 fn check_given_time_usage(
237 gt: Option<&String>,
238 plt: PriceLookupType,
239 ) -> Result<(), tackler::Error> {
240 if gt.is_some() {
241 let msg = format!(
242 "Price \"before timestamp\" is not allowed when price lookup type is \"{plt}\""
243 );
244 return Err(msg.into());
245 }
246 Ok(())
247 }
248
249 let strict_mode = overlaps.strict.mode.unwrap_or(cfg.kernel.strict);
250 let audit_mode = overlaps.audit.mode.unwrap_or(cfg.kernel.audit.mode);
251
252 let input_settings = Self::input_settings(&cfg, &overlaps.storage)?;
253
254 let reports = match overlaps.target.reports {
255 Some(reports) => config::to_report_targets(&reports)?,
256 None => cfg.report.targets.clone(),
257 };
258 let exports = match overlaps.target.exports {
259 Some(exports) => config::to_export_targets(&exports)?,
260 None => cfg.export.targets.clone(),
261 };
262
263 let formats = match overlaps.target.formats {
264 Some(formats) => config::to_report_formats(Some(&formats))?,
265 None => cfg.report.formats.clone(),
266 };
267
268 let lookup_type = overlaps.price.lookup_type.unwrap_or(cfg.price.lookup_type);
269
270 let db_path = overlaps.price.db_path.unwrap_or(cfg.price.db_path.clone());
271
272 let account_trees = AccountTrees::from(&cfg.transaction.accounts.names, strict_mode)?;
273
274 let mut commodities = Commodities::from(&cfg)?;
275
276 let tags = cfg
277 .transaction
278 .tags
279 .names
280 .iter()
281 .fold(HashMap::new(), |mut tags, tag| {
282 let t = Tag::from(tag.clone());
283 tags.insert(tag.into(), Arc::new(t));
284 tags
285 });
286
287 if strict_mode
288 && exports.contains(&ExportType::Equity)
289 && !account_trees
290 .defined_accounts
291 .contains_key(cfg.export.equity.equity_account.as_str())
292 {
293 let msg = "Unknown `equity.equity-account` and `strict` mode is on".to_string();
294 return Err(msg.into());
295 }
296
297 let cfg_rpt_commodity = cfg
298 .report
299 .commodity
300 .map(|c| {
301 Self::inner_get_or_create_commodity(
302 &mut commodities,
303 strict_mode,
304 Some(c.name.as_str()),
305 )
306 })
307 .transpose()?;
308
309 let report_commodity = match overlaps.report.commodity {
310 Some(c) => Some(Self::inner_get_or_create_commodity(
311 &mut commodities,
312 strict_mode,
313 Some(c.as_str()),
314 )?),
315 None => cfg_rpt_commodity,
316 };
317
318 if report_commodity.is_none() && lookup_type != PriceLookupType::None {
319 let msg =
320 "Price conversion is activated, but there is no `report.commodity`".to_string();
321 return Err(msg.into());
322 }
323
324 let group_by = overlaps
325 .report
326 .group_by
327 .map(|g| GroupBy::from(g.as_str()))
328 .unwrap_or(Ok(cfg.report.balance_group.group_by))?;
329
330 let mut tmp_settings = Settings {
331 strict_mode,
332 audit_mode,
333 inverted: overlaps.report.inverted,
334 kernel: cfg.kernel,
335 input_config: input_settings,
336 price: Price::default(), price_lookup: PriceLookup::default(), global_acc_sel: overlaps.report.account_overlap,
339 report: Report {
340 commodity: report_commodity,
341 targets: reports,
342 formats,
343 ..cfg.report
344 },
345 export: Export {
346 targets: exports,
347 ..cfg.export
348 },
349 accounts: account_trees,
350 commodities,
351 tags,
352 };
353 tmp_settings.report.balance_group.group_by = group_by;
354
355 let given_time = overlaps.price.before_time;
356
357 let price_lookup = match lookup_type {
358 plt @ PriceLookupType::LastPrice => {
359 check_given_time_usage(given_time.as_ref(), plt)?;
360 PriceLookup::LastPriceDbEntry
361 }
362 plt @ PriceLookupType::TxnTime => {
363 check_given_time_usage(given_time.as_ref(), plt)?;
364 PriceLookup::AtTheTimeOfTxn
365 }
366 plt @ PriceLookupType::GivenTime => {
367 if let Some(ts) = given_time {
368 tmp_settings
369 .parse_timestamp(ts.as_str())
370 .map(PriceLookup::GivenTime)?
371 } else {
372 let msg =
373 format!("Price lookup type is \"{plt}\" and there is no timestamp given");
374 return Err(msg.into());
375 }
376 }
377 plt @ PriceLookupType::None => {
378 check_given_time_usage(given_time.as_ref(), plt)?;
379 PriceLookup::None
380 }
381 };
382
383 let price = match &lookup_type {
384 PriceLookupType::None => Price::default(),
385 _ => Price {
386 price_db: parser::pricedb_from_file(&db_path, &mut tmp_settings)?,
388 lookup_type,
389 },
390 };
391
392 Ok(Settings {
393 price,
394 price_lookup,
395 ..tmp_settings
396 })
397 }
398}
399impl Settings {
400 pub(crate) fn get_hash(&self) -> Option<Hash> {
401 if self.audit_mode {
402 Some(self.kernel.audit.hash.clone())
403 } else {
404 None
405 }
406 }
407
408 pub(crate) fn is_extid_unique(&self) -> bool {
409 self.kernel.extid.unique
410 }
411
412 pub(crate) fn get_txn_account(
413 &self,
414 name: &str,
415 commodity: &Arc<Commodity>,
416 ) -> Result<TxnAccount, tackler::Error> {
417 let comm = self.get_commodity(commodity.name.as_str())?;
418
419 match self.accounts.defined_accounts.get(name) {
420 Some(account_tree) => Ok(TxnAccount {
421 atn: account_tree.clone(),
422 comm,
423 }),
424 None => {
425 if let Some(acc_parent) = self.accounts.synthetic_parents.get(name) {
426 Ok(TxnAccount {
427 atn: acc_parent.clone(),
428 comm,
429 })
430 } else {
431 let msg = format!("Unknown account: '{name}'");
432 Err(msg.into())
433 }
434 }
435 }
436 }
437
438 pub(crate) fn get_or_create_txn_account(
443 &mut self,
444 name: &str,
445 commodity: &Arc<Commodity>,
446 ) -> Result<TxnAccount, tackler::Error> {
447 let comm = self.get_or_create_commodity(Some(commodity.name.as_str()))?;
448
449 let atn_opt = self.accounts.defined_accounts.get(name).cloned();
450
451 let atn = if let Some(account_tree) = atn_opt {
452 TxnAccount {
453 atn: account_tree.clone(),
454 comm,
455 }
456 } else {
457 if self.strict_mode {
458 let msg = format!("Unknown account: '{name}'");
459 return Err(msg.into());
460 }
461 let atn = Arc::new(AccountTreeNode::unchecked_from(name));
462 self.accounts
463 .defined_accounts
464 .insert(name.into(), atn.clone());
465 AccountTrees::build_account_tree(&mut self.accounts.defined_accounts, &atn, None)?;
466
467 TxnAccount { atn, comm }
468 };
469 Ok(atn)
470 }
471
472 pub fn get_commodity(&self, name: &str) -> Result<Arc<Commodity>, tackler::Error> {
475 if let Some(comm) = self.commodities.names.get(name) {
476 Ok(comm.clone())
477 } else {
478 let msg = format!("Unknown commodity: '{name}'");
479 Err(msg.into())
480 }
481 }
482
483 pub(crate) fn get_or_create_commodity(
488 &mut self,
489 name: Option<&str>,
490 ) -> Result<Arc<Commodity>, tackler::Error> {
491 Self::inner_get_or_create_commodity(&mut self.commodities, self.strict_mode, name)
492 }
493
494 fn inner_get_or_create_commodity(
495 commodities: &mut Commodities,
496 strict_mode: bool,
497 name: Option<&str>,
498 ) -> Result<Arc<Commodity>, tackler::Error> {
499 if let Some(n) = name {
500 if n.is_empty() {
501 let res = if commodities.permit_empty_commodity {
502 if let Some(c) = commodities.names.get(n) {
503 Ok(c.clone())
504 } else {
505 let comm = Arc::new(Commodity::default());
506 commodities.names.insert(n.into(), comm.clone());
507
508 Ok(comm)
509 }
510 } else {
511 let msg = "Empty commodity and 'permit-empty-commodity' is not set".to_string();
512 Err(msg.into())
513 };
514 return res;
515 }
516 match commodities.names.get(n) {
517 Some(comm) => Ok(comm.clone()),
518 None => {
519 if strict_mode {
520 let msg = format!("Unknown commodity: '{n}'");
521 Err(msg.into())
522 } else {
523 let comm = Arc::new(Commodity::unchecked_from(n));
524 commodities.names.insert(n.into(), comm.clone());
525 Ok(comm)
526 }
527 }
528 }
529 } else {
530 let comm = Arc::new(Commodity::default());
531 Ok(comm)
532 }
533 }
534
535 pub(crate) fn get_or_create_tag(&mut self, name: &str) -> Result<Arc<Tag>, tackler::Error> {
536 if name.is_empty() {
537 let msg = "Tag name is empty string".to_string();
538 return Err(msg.into());
539 }
540 match self.tags.get(name) {
541 Some(tag) => Ok(tag.clone()),
542 None => {
543 if self.strict_mode {
544 let msg = format!("Unknown tag: '{name}'");
545 Err(msg.into())
546 } else {
547 let tag = Arc::new(Tag::from(name));
548 self.tags.insert(name.into(), tag.clone());
549 Ok(tag)
550 }
551 }
552 }
553 }
554
555 #[must_use]
556 pub fn get_price_lookup(&self) -> PriceLookup {
557 self.price_lookup.clone()
558 }
559
560 #[must_use]
561 pub fn input(&self) -> InputSettings {
562 self.input_config.clone()
563 }
564
565 #[allow(clippy::too_many_lines)]
566 fn input_settings(
567 cfg: &Config,
568 storage_overlap: &StorageOverlap,
569 ) -> Result<InputSettings, tackler::Error> {
570 let cfg_input = &cfg.kernel.input;
571
572 let storage_target = match storage_overlap.storage_type {
576 Some(storage) => storage,
577 None => cfg_input.storage,
578 };
579
580 let storage_type = if let Some(io) = &storage_overlap.input {
581 match io {
582 InputOverlap::File(f) => {
583 return Ok(InputSettings::File(FileInput {
585 path: f.path.clone(),
586 }));
587 }
588 InputOverlap::Fs(fs) => {
589 if fs.path.is_none() && storage_target != StorageType::Fs {
590 let msg = "Conflicting input and storage system arguments. Targeting 'fs', but it's not activated by configuration or by cli options";
591 return Err(msg.into());
592 }
593 StorageType::Fs
594 }
595 InputOverlap::Git(git) => {
596 if git.repo.is_none() && storage_target != StorageType::Git {
597 let msg = "Conflicting input and storage system arguments. Targeting 'git', but it's not activated by configuration or by cli options";
598 return Err(msg.into());
599 }
600 StorageType::Git
601 }
602 }
603 } else {
604 storage_target
605 };
606
607 match storage_type {
608 StorageType::Fs => match &cfg_input.fs {
609 Some(fs_cfg) => {
610 let i = if let Some(InputOverlap::Fs(fs_soi)) = &storage_overlap.input {
611 let path = fs_soi.path.as_ref().unwrap_or(&fs_cfg.path);
613 let dir = fs_soi.dir.as_ref().unwrap_or(&fs_cfg.dir);
614 let ext = fs_soi.ext.as_ref().unwrap_or(&fs_cfg.ext);
615 FsInput {
616 path: tackler_rs::get_abs_path(cfg.path(), path)?,
617 dir: PathBuf::from(dir),
618 ext: normalize_extension(ext).to_string(),
619 }
620 } else {
621 let ext = &fs_cfg.ext;
623 FsInput {
624 path: tackler_rs::get_abs_path(cfg.path(), fs_cfg.path.as_str())?,
625 dir: PathBuf::from(&fs_cfg.dir),
626 ext: normalize_extension(ext).to_string(),
627 }
628 };
629 Ok(InputSettings::Fs(i))
630 }
631 None => {
632 if let Some(InputOverlap::Fs(fs_soi)) = &storage_overlap.input {
634 if let (Some(path), Some(dir), Some(ext)) =
635 (&fs_soi.path, &fs_soi.dir, &fs_soi.ext)
636 {
637 Ok(InputSettings::Fs(FsInput {
638 path: tackler_rs::get_abs_path(cfg.path(), path)?,
639 dir: PathBuf::from(dir),
640 ext: normalize_extension(ext).to_string(),
641 }))
642 } else {
643 let msg = format!(
644 "Not enough information to configure 'fs' storage: path = '{:?}', dir = '{:?}', ext = '{:?}'",
645 fs_soi.path, fs_soi.dir, fs_soi.ext
646 );
647 Err(msg.into())
648 }
649 } else {
650 Err("Storage type 'fs' is not configured".into())
651 }
652 }
653 },
654 StorageType::Git => match &cfg_input.git {
655 Some(git_cfg) => {
656 let i = if let Some(InputOverlap::Git(git_soi)) = &storage_overlap.input {
657 let repo = git_soi.repo.as_ref().unwrap_or(&git_cfg.repo);
659 let dir = git_soi.dir.as_ref().unwrap_or(&git_cfg.dir);
660 let ext = git_soi.ext.as_ref().unwrap_or(&git_cfg.ext);
661 let cfg_ref = GitInputSelector::Reference(git_cfg.reference.clone());
663 let git_ref = git_soi.git_ref.as_ref().unwrap_or(&cfg_ref);
664
665 GitInput {
666 repo: tackler_rs::get_abs_path(cfg.path(), repo)?,
667 git_ref: git_ref.clone(),
668 dir: dir.clone(),
669 ext: normalize_extension(ext).to_string(),
670 }
671 } else {
672 let repo = git_cfg.repo.as_str();
674 let ext = &git_cfg.ext;
675 GitInput {
676 repo: tackler_rs::get_abs_path(cfg.path(), repo)?,
677 git_ref: GitInputSelector::Reference(git_cfg.reference.clone()),
678 dir: git_cfg.dir.clone(),
679 ext: normalize_extension(ext).to_string(),
680 }
681 };
682 Ok(InputSettings::Git(i))
683 }
684 None => {
685 if let Some(InputOverlap::Git(git_soi)) = &storage_overlap.input {
687 if let (Some(repo), Some(dir), Some(ext), Some(git_ref)) =
688 (&git_soi.repo, &git_soi.dir, &git_soi.ext, &git_soi.git_ref)
689 {
690 Ok(InputSettings::Git(GitInput {
691 repo: tackler_rs::get_abs_path(cfg.path(), repo)?,
692 git_ref: git_ref.clone(),
693 dir: dir.clone(),
694 ext: normalize_extension(ext).to_string(),
695 }))
696 } else {
697 let msg = format!(
698 "Not enough information to configure 'git' storage: repo = '{:?}', dir = '{:?}', ext = '{:?}', ref = '{:?}'",
699 git_soi.repo, git_soi.dir, git_soi.ext, git_soi.git_ref
700 );
701 Err(msg.into())
702 }
703 } else {
704 Err("Storage type 'git' is not configured".into())
705 }
706 }
707 },
708 }
709 }
710}
711
712impl Settings {
713 pub fn parse_timestamp(&mut self, ts: &str) -> Result<Zoned, tackler::Error> {
725 Ok(winnow::Parser::parse(
726 &mut crate::parser::parts::timestamp::parse_timestamp,
727 winnow::Stateful {
728 input: ts,
729 state: self,
730 },
731 )
732 .map_err(|e| e.to_string())?)
733 }
734
735 pub fn get_offset_datetime(&self, dt: jiff::civil::DateTime) -> Result<Zoned, tackler::Error> {
738 match dt.to_zoned(self.kernel.timestamp.timezone.clone()) {
739 Ok(ts) => Ok(ts),
740 Err(err) => {
741 let msg = format!("time is invalid '{err:?}'");
742 Err(msg.into())
743 }
744 }
745 }
746 pub fn get_offset_date(&self, date: jiff::civil::Date) -> Result<Zoned, tackler::Error> {
749 let ts = date.to_datetime(self.kernel.timestamp.default_time);
750 match ts.to_zoned(self.kernel.timestamp.timezone.clone()) {
751 Ok(ts) => Ok(ts),
752 Err(err) => {
753 let msg = format!("time is invalid '{err:?}'");
754 Err(msg.into())
755 }
756 }
757 }
758
759 #[must_use]
760 pub fn get_report_commodity(&self) -> Option<Arc<Commodity>> {
761 self.report.commodity.clone()
762 }
763
764 #[must_use]
765 pub fn get_report_targets(&self) -> Vec<ReportType> {
766 self.report.targets.clone()
767 }
768
769 #[must_use]
770 pub fn get_export_targets(&self) -> Vec<ExportType> {
771 self.export.targets.clone()
772 }
773
774 #[must_use]
775 fn get_account_selector(&self, acc_sel: &AccountSelectors) -> AccountSelectors {
776 let v = match &self.global_acc_sel {
777 Some(global_acc_sel) => global_acc_sel.clone(),
778 None => acc_sel.clone(),
779 };
780
781 if v.len() == 1 && v[0].is_empty() {
783 Vec::new()
784 } else {
785 v
786 }
787 }
788
789 #[must_use]
790 pub fn get_balance_ras(&self) -> AccountSelectors {
791 self.get_account_selector(&self.report.balance.acc_sel)
792 }
793
794 #[must_use]
795 pub fn get_balance_group_ras(&self) -> AccountSelectors {
796 self.get_account_selector(&self.report.balance_group.acc_sel)
797 }
798
799 #[must_use]
800 pub fn get_register_ras(&self) -> AccountSelectors {
801 self.get_account_selector(&self.report.register.acc_sel)
802 }
803
804 #[must_use]
805 pub fn get_equity_ras(&self) -> AccountSelectors {
806 self.get_account_selector(&self.export.equity.acc_sel)
807 }
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn accounts_strict_false() {
816 let comm = Arc::new(Commodity::default());
817 let mut settings = Settings::default();
818
819 let txntn_1 = settings.get_or_create_txn_account("a:b:c", &comm).unwrap();
820 assert_eq!(settings.accounts.defined_accounts.len(), 3);
821
822 assert_eq!(txntn_1.atn.depth, 3);
823 assert_eq!(txntn_1.atn.get_root(), "a");
824 assert_eq!(txntn_1.atn.parent, "a:b");
825 assert_eq!(txntn_1.atn.account, "a:b:c");
826 assert_eq!(txntn_1.atn.get_name(), "c");
827
828 let txntn_2 = settings.get_txn_account("a:b:c", &comm).unwrap();
829 assert_eq!(settings.accounts.defined_accounts.len(), 3);
830
831 assert_eq!(txntn_2.atn.depth, 3);
832 assert_eq!(txntn_2.atn.get_root(), "a");
833 assert_eq!(txntn_2.atn.parent, "a:b");
834 assert_eq!(txntn_2.atn.account, "a:b:c");
835 assert_eq!(txntn_2.atn.get_name(), "c");
836
837 let txntn_3 = settings.get_or_create_txn_account("a:b:b-leaf", &comm).unwrap();
838 assert_eq!(settings.accounts.defined_accounts.len(), 4);
839
840 assert_eq!(txntn_3.atn.depth, 3);
841 assert_eq!(txntn_3.atn.get_root(), "a");
842 assert_eq!(txntn_3.atn.parent, "a:b");
843 assert_eq!(txntn_3.atn.account, "a:b:b-leaf");
844 assert_eq!(txntn_3.atn.get_name(), "b-leaf");
845 }
846
847 #[test]
848 fn accounts_strict_true() {
849 let comm = Arc::new(Commodity::default());
850 let mut settings = Settings::default();
851 let accounts = vec!["a:b:c".to_string()];
852
853 let acc_trees = AccountTrees::from(&accounts, true).unwrap();
854 settings.accounts = acc_trees;
855 settings.strict_mode = true;
856
857 assert_eq!(settings.accounts.defined_accounts.len(), 1);
858 assert_eq!(settings.accounts.synthetic_parents.len(), 2);
859
860 let txntn_1 = settings.get_or_create_txn_account("a:b:c", &comm).unwrap();
861 assert_eq!(settings.accounts.defined_accounts.len(), 1);
862 assert_eq!(settings.accounts.synthetic_parents.len(), 2);
863
864 assert_eq!(txntn_1.atn.depth, 3);
865 assert_eq!(txntn_1.atn.get_root(), "a");
866 assert_eq!(txntn_1.atn.parent, "a:b");
867 assert_eq!(txntn_1.atn.account, "a:b:c");
868 assert_eq!(txntn_1.atn.get_name(), "c");
869
870 let txntn_2 = settings.get_txn_account("a:b:c", &comm).unwrap();
871 assert_eq!(settings.accounts.defined_accounts.len(), 1);
872 assert_eq!(settings.accounts.synthetic_parents.len(), 2);
873
874 assert_eq!(txntn_2.atn.depth, 3);
875 assert_eq!(txntn_2.atn.get_root(), "a");
876 assert_eq!(txntn_2.atn.parent, "a:b");
877 assert_eq!(txntn_2.atn.account, "a:b:c");
878 assert_eq!(txntn_2.atn.get_name(), "c");
879
880 assert!(settings.get_or_create_txn_account("a:b", &comm).is_err());
882 assert_eq!(settings.accounts.defined_accounts.len(), 1);
883 assert_eq!(settings.accounts.synthetic_parents.len(), 2);
884
885 let txntn_3 = settings.get_txn_account("a:b", &comm).unwrap();
887 assert_eq!(settings.accounts.defined_accounts.len(), 1);
888 assert_eq!(settings.accounts.synthetic_parents.len(), 2);
889
890 assert_eq!(txntn_3.atn.depth, 2);
891 assert_eq!(txntn_3.atn.get_root(), "a");
892 assert_eq!(txntn_3.atn.parent, "a");
893 assert_eq!(txntn_3.atn.account, "a:b");
894 assert_eq!(txntn_3.atn.get_name(), "b");
895
896 let txntn_4 = settings.get_txn_account("a", &comm).unwrap();
898 assert_eq!(settings.accounts.defined_accounts.len(), 1);
899 assert_eq!(settings.accounts.synthetic_parents.len(), 2);
900
901 assert_eq!(txntn_4.atn.depth, 1);
902 assert_eq!(txntn_4.atn.get_root(), "a");
903 assert_eq!(txntn_4.atn.parent, "");
904 assert_eq!(txntn_4.atn.account, "a");
905 assert_eq!(txntn_4.atn.get_name(), "a");
906 }
907
908 #[test]
909 fn accounts_strict_true_child_first() {
910 let comm = Arc::new(Commodity::default());
911 let mut settings = Settings::default();
912 let accounts = vec!["a:b:c".to_string(), "a:b".to_string(), "a".to_string()];
913
914 let acc_trees = AccountTrees::from(&accounts, true).unwrap();
915 settings.accounts = acc_trees;
916 settings.strict_mode = true;
917
918 assert_eq!(settings.accounts.defined_accounts.len(), 3);
919 assert_eq!(settings.accounts.synthetic_parents.len(), 0);
920
921 let txntn_1 = settings.get_or_create_txn_account("a:b:c", &comm).unwrap();
922 assert_eq!(settings.accounts.defined_accounts.len(), 3);
923 assert_eq!(settings.accounts.synthetic_parents.len(), 0);
924 assert_eq!(txntn_1.atn.account, "a:b:c");
925
926 let txntn_2 = settings.get_or_create_txn_account("a:b", &comm).unwrap();
927 assert_eq!(settings.accounts.defined_accounts.len(), 3);
928 assert_eq!(settings.accounts.synthetic_parents.len(), 0);
929 assert_eq!(txntn_2.atn.account, "a:b");
930
931 let txntn_2 = settings.get_or_create_txn_account("a", &comm).unwrap();
932 assert_eq!(settings.accounts.defined_accounts.len(), 3);
933 assert_eq!(settings.accounts.synthetic_parents.len(), 0);
934 assert_eq!(txntn_2.atn.account, "a");
935 }
936
937 #[test]
938 fn accounts_strict_true_gap() {
939 let comm = Arc::new(Commodity::default());
940 let mut settings = Settings::default();
941 let accounts = vec!["a:b:c:d".to_string(), "a:b".to_string(), "a".to_string()];
942
943 let acc_trees = AccountTrees::from(&accounts, true).unwrap();
944 settings.accounts = acc_trees;
945 settings.strict_mode = true;
946
947 assert_eq!(settings.accounts.defined_accounts.len(), 3);
948 assert_eq!(settings.accounts.synthetic_parents.len(), 1);
949
950 assert!(settings.get_or_create_txn_account("a:b:c", &comm).is_err());
952
953 let txntn_synth = settings.get_txn_account("a:b:c", &comm).unwrap();
954 assert_eq!(settings.accounts.defined_accounts.len(), 3);
955 assert_eq!(settings.accounts.synthetic_parents.len(), 1);
956 assert_eq!(txntn_synth.atn.account, "a:b:c");
957
958 let txntn_2 = settings.get_or_create_txn_account("a:b", &comm).unwrap();
959 assert_eq!(settings.accounts.defined_accounts.len(), 3);
960 assert_eq!(settings.accounts.synthetic_parents.len(), 1);
961 assert_eq!(txntn_2.atn.account, "a:b");
962
963 let txntn_2 = settings.get_or_create_txn_account("a", &comm).unwrap();
964 assert_eq!(settings.accounts.defined_accounts.len(), 3);
965 assert_eq!(settings.accounts.synthetic_parents.len(), 1);
966 assert_eq!(txntn_2.atn.account, "a");
967 }
968}