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