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