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