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