1use crate::config::raw_items::{
6 AccountsPathRaw, AccountsRaw, AuditRaw, BalanceGroupRaw, BalanceRaw, CommoditiesPathRaw,
7 CommoditiesRaw, ConfigRaw, EquityRaw, ExportRaw, FsRaw, GitRaw, InputRaw, KernelRaw, PriceRaw,
8 RegisterRaw, ReportRaw, ScaleRaw, TagsPathRaw, TagsRaw, TimestampRaw, TimezoneRaw,
9 TransactionRaw,
10};
11use crate::config::{to_export_targets, to_report_formats, to_report_targets};
12use crate::kernel::hash::Hash;
13use crate::model::Commodity;
14use crate::tackler;
15use jiff::fmt::strtime::BrokenDownTime;
16use jiff::tz::TimeZone;
17use rust_decimal::Decimal;
18use std::fmt::{Debug, Display, Formatter};
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21use std::{cmp, fs};
22use tackler_api::txn_ts::{GroupBy, TimestampStyle};
23use tackler_rs::get_abs_path;
24
25pub const NONE_VALUE: &str = "none";
27
28#[derive(Debug, Copy, Clone, Default, PartialEq)]
29pub enum StorageType {
30 #[default]
31 Fs,
32 Git,
33}
34#[rustfmt::skip]
35impl StorageType {
36 pub const FS: &'static str = "fs";
37 pub const GIT: &'static str = "git";
38}
39
40impl StorageType {
41 pub fn try_from(storage: &str) -> Result<StorageType, tackler::Error> {
46 match storage {
47 StorageType::FS => Ok(StorageType::Fs),
48 StorageType::GIT => Ok(StorageType::Git),
49 _ => Err(format!(
50 "Unknown storage type: '{storage}'. Valid options are: {}, {}",
51 Self::FS,
52 Self::GIT,
53 )
54 .into()),
55 }
56 }
57}
58
59impl Display for StorageType {
60 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
61 let s = match &self {
62 StorageType::Fs => StorageType::FS,
63 StorageType::Git => StorageType::GIT,
64 };
65 write!(f, "{s}")
66 }
67}
68
69#[derive(Debug, Clone, Copy, Default, PartialEq)]
70pub enum PriceLookupType {
71 #[default]
72 None,
73 LastPrice,
74 TxnTime,
75 GivenTime,
76}
77
78impl PriceLookupType {
79 pub const NONE: &'static str = NONE_VALUE;
80 pub const LAST_PRICE: &'static str = "last-price";
81 pub const TXN_TIME: &'static str = "txn-time";
82 pub const GIVEN_TIME: &'static str = "given-time";
83}
84
85impl Display for PriceLookupType {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 match self {
88 Self::None => f.write_str(PriceLookupType::NONE),
89 Self::LastPrice => f.write_str(PriceLookupType::LAST_PRICE),
90 Self::TxnTime => f.write_str(PriceLookupType::TXN_TIME),
91 Self::GivenTime => f.write_str(PriceLookupType::GIVEN_TIME),
92 }
93 }
94}
95
96impl TryFrom<&str> for PriceLookupType {
97 type Error = tackler::Error;
98
99 fn try_from(lookup: &str) -> Result<PriceLookupType, tackler::Error> {
100 match lookup {
101 PriceLookupType::NONE => Ok(PriceLookupType::None),
102 PriceLookupType::LAST_PRICE => Ok(PriceLookupType::LastPrice),
103 PriceLookupType::TXN_TIME => Ok(PriceLookupType::TxnTime),
104 PriceLookupType::GIVEN_TIME => Ok(PriceLookupType::GivenTime),
105 _ => Err(format!(
106 "Unknown price lookup type: '{lookup}'. Valid options are: {}, {}, {}, {}",
107 Self::NONE,
108 Self::LAST_PRICE,
109 Self::TXN_TIME,
110 Self::GIVEN_TIME,
111 )
112 .into()),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Default)]
118pub enum ReportType {
119 #[default]
120 Balance,
121 BalanceGroup,
122 Register,
123}
124impl ReportType {
125 const BALANCE: &'static str = "balance";
126 const BALANCE_GROUP: &'static str = "balance-group";
127 const REGISTER: &'static str = "register";
128 pub fn try_from(r: &str) -> Result<Self, tackler::Error> {
133 match r {
134 Self::BALANCE => Ok(ReportType::Balance),
135 Self::BALANCE_GROUP => Ok(ReportType::BalanceGroup),
136 Self::REGISTER => Ok(ReportType::Register),
137 _ => Err(format!(
138 "Unknown report type: '{r}'. Valid options are: {}, {}, {}",
139 Self::BALANCE,
140 Self::BALANCE_GROUP,
141 Self::REGISTER,
142 )
143 .into()),
144 }
145 }
146}
147
148#[derive(Debug, Clone, Default, PartialEq)]
149pub enum ExportType {
150 #[default]
151 Equity,
152 Identity,
153 Accounts,
154}
155impl ExportType {
156 const EQUITY: &'static str = "equity";
157 const IDENTITY: &'static str = "identity";
158 const ACCOUNTS: &'static str = "accounts";
159
160 pub fn try_from(e: &str) -> Result<Self, tackler::Error> {
165 match e {
166 Self::EQUITY => Ok(ExportType::Equity),
167 Self::IDENTITY => Ok(ExportType::Identity),
168 Self::ACCOUNTS => Ok(ExportType::Accounts),
169 _ => Err(format!(
170 "Unknown export type: '{e}'. Valid options are: {}, {}",
171 Self::EQUITY,
172 Self::IDENTITY,
173 )
174 .into()),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Default)]
180pub enum BalanceType {
181 #[default]
182 Tree,
183 Flat,
184}
185impl BalanceType {
186 const TREE: &'static str = "tree";
187 const FLAT: &'static str = "flat";
188
189 fn try_from(b: &str) -> Result<Self, tackler::Error> {
190 match b {
191 Self::TREE => Ok(BalanceType::Tree),
192 Self::FLAT => Ok(BalanceType::Flat),
193 _ => Err(format!(
194 "Unknown balance type: '{b}'. Valid options are: {}, {}",
195 Self::TREE,
196 Self::FLAT,
197 )
198 .into()),
199 }
200 }
201}
202
203#[derive(Debug, Clone, Default)]
204pub enum FormatType {
205 #[default]
206 Txt,
207 Json,
208}
209impl FormatType {
210 pub const TXT: &'static str = "txt";
211 pub const JSON: &'static str = "json";
212}
213impl TryFrom<&str> for FormatType {
214 type Error = tackler::Error;
215
216 fn try_from(t: &str) -> Result<Self, tackler::Error> {
217 match t {
218 Self::TXT => Ok(FormatType::Txt),
219 Self::JSON => Ok(FormatType::Json),
220 _ => Err(format!(
221 "Unknown report output format type: '{t}'. Valid options are: {}, {}",
222 Self::TXT,
223 Self::JSON
224 )
225 .into()),
226 }
227 }
228}
229
230enum Timezone {}
231
232pub(crate) type AccountSelectors = Vec<String>;
233
234#[derive(Debug)]
235pub struct Config {
236 path: PathBuf,
238 pub(crate) kernel: Kernel,
239 pub(crate) price: Price,
240 pub(crate) transaction: Transaction,
241 pub(crate) report: Report,
242 pub(crate) export: Export,
243}
244
245impl Config {
246 pub fn try_from<P: AsRef<Path>>(cfg_path: P) -> Result<Config, tackler::Error> {
251 let cfg_raw: ConfigRaw = toml::from_str(fs::read_to_string(&cfg_path)?.as_str())?;
252
253 Ok(Config {
254 path: cfg_path.as_ref().to_path_buf(),
255 kernel: Kernel::try_from(&cfg_raw.kernel)?,
256 price: cfg_raw.price.map_or(Ok(Price::default()), |raw_price| {
257 Price::try_from(&cfg_path, &raw_price)
258 })?,
259 transaction: Transaction::from(&cfg_path, &cfg_raw.transaction)?,
260 report: Report::from(&cfg_raw.report)?,
261 export: { Export::from(&cfg_raw.export, &cfg_raw.report)? },
262 })
263 }
264 #[must_use]
265 pub fn path(&self) -> PathBuf {
266 self.path.clone()
267 }
268}
269
270#[derive(Debug, Clone, Default)]
271pub(crate) struct Kernel {
272 pub(crate) strict: bool,
273 pub(crate) timestamp: Timestamp,
274 pub(crate) audit: Audit,
275 pub input: Input,
276}
277impl Kernel {
278 fn try_from(k_raw: &KernelRaw) -> Result<Kernel, tackler::Error> {
279 let k = Kernel {
280 strict: k_raw.strict,
281 timestamp: Timestamp::from(&k_raw.timestamp)?,
282 audit: Audit::from(&k_raw.audit)?,
283 input: Input::try_from(&k_raw.input)?,
284 };
285 Ok(k)
286 }
287}
288
289#[derive(Debug, Clone)]
290pub(crate) struct Timestamp {
291 pub(crate) default_time: jiff::civil::Time,
292 pub(crate) timezone: jiff::tz::TimeZone,
293}
294
295impl Default for Timestamp {
296 fn default() -> Self {
297 Timestamp {
298 default_time: jiff::civil::Time::midnight(),
299 timezone: jiff::tz::Offset::UTC.to_time_zone(),
300 }
301 }
302}
303
304impl Timestamp {
305 #[allow(clippy::cast_possible_wrap)]
306 fn from(ts_raw: &TimestampRaw) -> Result<Timestamp, tackler::Error> {
307 let ts = Timestamp {
308 default_time: {
309 let t = ts_raw.default_time;
310 jiff::civil::Time::new(
311 t.hour as i8,
312 t.minute as i8,
313 t.second.map_or(0, |s| s as i8),
314 t.nanosecond.map_or(0, |s| s as i32),
315 )?
316 },
317 timezone: { Timezone::from(&ts_raw.timezone)? },
318 };
319 Ok(ts)
320 }
321}
322
323impl Timezone {
324 fn from(tz_raw: &TimezoneRaw) -> Result<jiff::tz::TimeZone, tackler::Error> {
325 let tz = match (&tz_raw.name, &tz_raw.offset) {
326 (Some(_), Some(_)) => {
327 let msg = "timezone: 'name' and 'offset' are both defined".to_string();
328 return Err(msg.into());
329 }
330 (Some(tz_name), None) => jiff::tz::TimeZone::get(tz_name)?,
331 (None, Some(offset)) => {
332 if let Some(tm) = BrokenDownTime::parse("%:z", offset)?.offset() {
333 tm.to_time_zone()
334 } else {
335 let msg = format!("can't parse offset '{offset}' as valid offset");
336 return Err(msg.into());
337 }
338 }
339 (None, None) => jiff::tz::Offset::UTC.to_time_zone(),
340 };
341 Ok(tz)
342 }
343}
344
345#[derive(Debug, Clone, Default)]
346pub(crate) struct Audit {
347 pub(crate) hash: Hash,
348 pub(crate) mode: bool,
349}
350impl Audit {
351 fn from(a_raw: &AuditRaw) -> Result<Audit, tackler::Error> {
352 let a = Audit {
353 hash: Hash::from(&a_raw.hash)?,
354 mode: a_raw.mode,
355 };
356 Ok(a)
357 }
358}
359
360#[derive(Debug, Clone, Default)]
361pub struct Input {
362 pub storage: StorageType,
363 pub fs: Option<FS>,
364 pub git: Option<Git>,
365}
366impl Input {
367 fn try_from(input_raw: &InputRaw) -> Result<Input, tackler::Error> {
368 let i = Input {
370 storage: StorageType::try_from(input_raw.storage.as_str())?,
371 fs: match &input_raw.fs {
372 Some(fs) => Some(FS::try_from(fs)?),
373 None => None,
374 },
375 git: match &input_raw.git {
376 Some(git) => Some(Git::try_from(git)?),
377 None => None,
378 },
379 };
380 Ok(i)
381 }
382}
383
384#[derive(Debug, Clone, Default)]
385pub struct FS {
386 pub path: String,
387 pub dir: String,
388 pub ext: String,
389}
390impl FS {
391 fn try_from(fs_raw: &FsRaw) -> Result<FS, tackler::Error> {
392 let extension = match &fs_raw.ext {
393 Some(ext) => {
394 if fs_raw.suffix.is_some() {
395 let msg = "FS has both 'suffix' and 'ext' keys defined";
396 return Err(msg.into());
397 }
398 ext.clone()
399 }
400 None => {
401 if let Some(ext) = &fs_raw.suffix {
402 ext.clone()
403 } else {
404 let msg = "FS is missing 'ext' key";
405 return Err(msg.into());
406 }
407 }
408 };
409
410 let ext = extension
411 .strip_prefix('.')
412 .unwrap_or(extension.as_str())
413 .to_string();
414 Ok(FS {
415 path: fs_raw.path.clone().unwrap_or_default(),
416 dir: fs_raw.dir.clone(),
417 ext,
418 })
419 }
420}
421
422#[derive(Debug, Clone, Default)]
423pub struct Git {
424 pub repo: String,
425 pub reference: String,
426 pub dir: String,
427 pub ext: String,
428}
429impl Git {
430 fn try_from(git_raw: &GitRaw) -> Result<Git, tackler::Error> {
431 let repo = match &git_raw.repo {
432 Some(repo) => {
433 if git_raw.repository.is_some() {
434 let msg = "Git has both 'repo' and 'repository' keys defined";
435 return Err(msg.into());
436 }
437 repo.clone()
438 }
439 None => {
440 if let Some(repo) = &git_raw.repository {
441 repo.clone()
442 } else {
443 let msg = "Git is missing 'repo' key";
444 return Err(msg.into());
445 }
446 }
447 };
448 let extension = match &git_raw.ext {
449 Some(ext) => {
450 if git_raw.suffix.is_some() {
451 let msg = "Git has both 'suffix' and 'ext' keys defined";
452 return Err(msg.into());
453 }
454 ext.clone()
455 }
456 None => {
457 if let Some(ext) = &git_raw.suffix {
458 ext.clone()
459 } else {
460 let msg = "Git is missing 'ext' key";
461 return Err(msg.into());
462 }
463 }
464 };
465 let ext = extension
466 .strip_prefix('.')
467 .unwrap_or(extension.as_str())
468 .to_string();
469 Ok(Git {
470 repo,
471 reference: git_raw.git_ref.clone(),
472 dir: git_raw.dir.clone(),
473 ext,
474 })
475 }
476}
477
478#[derive(Debug, Clone, Default)]
479pub(crate) struct Price {
480 pub(crate) db_path: PathBuf,
481 pub(crate) lookup_type: PriceLookupType,
482}
483impl Price {
484 fn try_from<P: AsRef<Path>>(
485 base_path: P,
486 price_raw: &PriceRaw,
487 ) -> Result<Price, tackler::Error> {
488 let db_path_str = price_raw.db_path.as_str();
489 let lookup_type = PriceLookupType::try_from(price_raw.lookup_type.as_str())?;
490
491 match db_path_str {
492 NONE_VALUE => {
493 if lookup_type == PriceLookupType::None {
494 Ok(Price::default())
495 } else {
496 let msg = "Price database path is 'none' but lookup type is not 'none'";
497 Err(msg.into())
498 }
499 }
500 _ => Ok(Price {
501 db_path: get_abs_path(base_path, db_path_str)?,
502 lookup_type,
503 }),
504 }
505 }
506}
507
508#[derive(Debug, Clone, Default)]
509pub(crate) struct Transaction {
510 pub(crate) accounts: Accounts,
511 pub(crate) commodities: Commodities,
512 pub(crate) tags: Tags,
513}
514
515impl Transaction {
516 fn from<P: AsRef<Path>>(
517 path: P,
518 txn_raw: &TransactionRaw,
519 ) -> Result<Transaction, tackler::Error> {
520 Ok(Transaction {
521 accounts: Accounts::from(&path, &txn_raw.accounts)?,
522 commodities: Commodities::from(&path, &txn_raw.commodities)?,
523 tags: Tags::from(&path, &txn_raw.tags)?,
524 })
525 }
526}
527
528#[derive(Debug, Clone, Default)]
529pub(crate) struct Accounts {
530 pub names: Vec<String>,
531}
532impl Accounts {
533 fn from<P: AsRef<Path>>(
534 path: P,
535 accs_path_raw: &AccountsPathRaw,
536 ) -> Result<Accounts, tackler::Error> {
537 let accs_path_str = accs_path_raw.path.as_str();
538 if accs_path_str == NONE_VALUE {
539 Ok(Accounts::default())
540 } else {
541 let accs_path = get_abs_path(&path, accs_path_str)?;
542 let acc_raw: AccountsRaw = match fs::read_to_string(&accs_path) {
543 Ok(s) => toml::from_str(s.as_str())?,
544 Err(err) => {
545 let msg = format!(
546 "Accounts configuration error while reading file '{accs_path_str}': {err}"
547 );
548 return Err(msg.into());
549 }
550 };
551 Ok(Accounts {
552 names: acc_raw.names,
553 })
554 }
555 }
556}
557
558#[derive(Debug, Clone, Default)]
559pub(crate) struct Commodities {
560 pub(crate) permit_empty_commodity: Option<bool>,
561
562 pub(crate) names: Vec<String>,
563}
564impl Commodities {
565 fn from<P: AsRef<Path>>(
566 path: P,
567 comm_path_raw: &CommoditiesPathRaw,
568 ) -> Result<Commodities, tackler::Error> {
569 let comm_path_str = comm_path_raw.path.as_str();
570 if comm_path_str == NONE_VALUE {
571 Ok(Commodities {
572 permit_empty_commodity: Some(true),
573 names: Vec::new(),
574 })
575 } else {
576 let comm_path = get_abs_path(&path, comm_path_str)?;
577 let comm_raw: CommoditiesRaw = match fs::read_to_string(&comm_path) {
578 Ok(s) => toml::from_str(s.as_str())?,
579 Err(err) => {
580 let msg = format!(
581 "Commodities configuration error while reading file '{comm_path_str}': {err}"
582 );
583 return Err(msg.into());
584 }
585 };
586 Ok(Commodities {
587 permit_empty_commodity: comm_raw.permit_empty_commodity,
588 names: comm_raw.names,
589 })
590 }
591 }
592}
593
594#[derive(Debug, Clone, Default)]
595pub(crate) struct Tags {
596 pub(crate) names: Vec<String>,
597}
598
599impl Tags {
600 fn from<P: AsRef<Path>>(path: P, tags_path_raw: &TagsPathRaw) -> Result<Tags, tackler::Error> {
601 let tags_path_str = tags_path_raw.path.as_str();
602 if tags_path_str == NONE_VALUE {
603 Ok(Tags::default())
604 } else {
605 let tags_path = get_abs_path(&path, tags_path_str)?;
606 let tags_raw: TagsRaw = match fs::read_to_string(&tags_path) {
607 Ok(s) => toml::from_str(s.as_str())?,
608 Err(err) => {
609 let msg = format!(
610 "Tags configuration error while reading file '{tags_path_str}': {err}"
611 );
612 return Err(msg.into());
613 }
614 };
615 Ok(Tags {
616 names: tags_raw.names,
617 })
618 }
619 }
620}
621
622#[derive(Debug, Clone)]
623pub(crate) struct Report {
624 pub tz: TimeZone,
625 pub targets: Vec<ReportType>,
626 pub formats: Vec<FormatType>,
627 pub scale: Scale,
628 pub commodity: Option<Arc<Commodity>>,
629 pub register: Register,
630 pub balance_group: BalanceGroup,
631 pub balance: Balance,
632}
633
634impl Default for Report {
635 fn default() -> Self {
636 Report {
637 tz: TimeZone::UTC,
638 targets: Vec::new(),
639 formats: Vec::new(),
640 scale: Scale::default(),
641 commodity: None,
642 register: Register::default(),
643 balance_group: BalanceGroup::default(),
644 balance: Balance::default(),
645 }
646 }
647}
648
649impl Report {
650 fn from(report_raw: &ReportRaw) -> Result<Report, tackler::Error> {
651 let targets = to_report_targets(&report_raw.targets)?;
652 let formats = to_report_formats(report_raw.formats.as_deref())?;
653
654 Ok(Report {
655 tz: TimeZone::get(report_raw.report_tz.as_str())?,
656 targets,
657 formats,
658 scale: Scale::from(&report_raw.scale)?,
659 commodity: match &report_raw.commodity {
660 Some(c) => Some(Arc::new(Commodity::from(c.clone())?)),
661 None => None,
662 },
663 register: Register::from(&report_raw.register, report_raw)?,
664 balance_group: BalanceGroup::from(&report_raw.balance_group, report_raw)?,
665 balance: Balance::from(&report_raw.balance, report_raw)?,
666 })
667 }
668}
669
670#[derive(Debug, Clone)]
671pub struct Scale {
672 min: u32,
673 max: u32,
674}
675impl Scale {
676 pub fn try_from(min: u32, max: u32) -> Result<Scale, tackler::Error> {
677 Self::check_range(min, max)?;
678 Ok(Scale { min, max })
679 }
680
681 fn check_range(min: u32, max: u32) -> Result<(), tackler::Error> {
682 let max_scale = 28;
683 if min > max_scale || max > max_scale {
684 let msg = format!(
685 "scale error: too large value - maximum scale value for min or max is {max_scale}"
686 );
687 return Err(msg.into());
688 }
689 if max < min {
690 let msg = "scale error: 'min' can't be greater than 'max'";
691 return Err(msg.into());
692 }
693 Ok(())
694 }
695 fn from(scale_raw: &ScaleRaw) -> Result<Scale, tackler::Error> {
696 Self::check_range(scale_raw.min, scale_raw.max)?;
697 Ok(Scale {
698 min: scale_raw.min,
699 max: scale_raw.max,
700 })
701 }
702 pub fn get_precision(&self, d: &Decimal) -> usize {
703 cmp::max(cmp::min(d.scale(), self.max), self.min) as usize
704 }
705}
706
707impl Default for Scale {
708 fn default() -> Self {
709 Scale { min: 2, max: 7 }
710 }
711}
712
713fn get_account_selector(
714 acc_sel: Option<&AccountSelectors>,
715 report: &ReportRaw,
716) -> AccountSelectors {
717 match acc_sel {
718 Some(av) => av.clone(),
719 None => match &report.accounts {
720 Some(av) => av.clone(),
721 None => vec![],
722 },
723 }
724}
725
726#[derive(Debug, Clone, Default)]
727pub(crate) struct Register {
728 pub title: String,
729 pub timestamp_style: TimestampStyle,
730 pub acc_sel: AccountSelectors,
731}
732
733impl Register {
734 fn from(reg_raw: &RegisterRaw, report: &ReportRaw) -> Result<Register, tackler::Error> {
735 Ok(Register {
736 title: reg_raw.title.clone(),
737 timestamp_style: match ®_raw.timestamp_style {
738 Some(style) => TimestampStyle::from(style.as_str())?,
739 None => TimestampStyle::Date,
740 },
741 acc_sel: get_account_selector(reg_raw.acc_sel.as_ref(), report),
742 })
743 }
744}
745
746#[derive(Debug, Clone, Default)]
747pub(crate) struct BalanceGroup {
748 pub title: String,
749 pub bal_type: BalanceType,
750 pub group_by: GroupBy,
751 pub acc_sel: AccountSelectors,
752}
753
754impl BalanceGroup {
755 fn from(
756 balgrp_raw: &BalanceGroupRaw,
757 report: &ReportRaw,
758 ) -> Result<BalanceGroup, tackler::Error> {
759 Ok(BalanceGroup {
760 title: balgrp_raw.title.clone(),
761 bal_type: match &balgrp_raw.bal_type {
762 Some(t) => BalanceType::try_from(t.as_str())?,
763 None => BalanceType::default(),
764 },
765 group_by: GroupBy::from(balgrp_raw.group_by.as_str())?,
766 acc_sel: get_account_selector(balgrp_raw.acc_sel.as_ref(), report),
767 })
768 }
769}
770
771#[derive(Debug, Clone, Default)]
772pub(crate) struct Balance {
773 pub title: String,
774 pub bal_type: BalanceType,
775 pub acc_sel: AccountSelectors,
776}
777
778impl Balance {
779 fn from(bal_raw: &BalanceRaw, report: &ReportRaw) -> Result<Balance, tackler::Error> {
780 Ok(Balance {
781 title: bal_raw.title.clone(),
782 bal_type: match &bal_raw.bal_type {
783 Some(t) => BalanceType::try_from(t.as_str())?,
784 None => BalanceType::default(),
785 },
786 acc_sel: get_account_selector(bal_raw.acc_sel.as_ref(), report),
787 })
788 }
789}
790
791#[derive(Debug, Clone, Default)]
792pub(crate) struct Export {
793 pub targets: Vec<ExportType>,
794 pub equity: Equity,
795}
796impl Export {
797 fn from(export_raw: &ExportRaw, report: &ReportRaw) -> Result<Export, tackler::Error> {
798 let trgs = to_export_targets(&export_raw.targets)?;
799 Ok(Export {
800 targets: trgs,
801 equity: Equity::from(&export_raw.equity, report),
802 })
803 }
804}
805
806#[derive(Debug, Clone, Default)]
807pub(crate) struct Equity {
808 pub(crate) equity_account: String,
809 pub(crate) acc_sel: AccountSelectors,
810}
811
812impl Equity {
813 fn from(eq_raw: &EquityRaw, report: &ReportRaw) -> Equity {
814 Equity {
815 equity_account: eq_raw.equity_account.clone(),
816 acc_sel: get_account_selector(eq_raw.acc_sel.as_ref(), report),
817 }
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824 use crate::config::raw_items::ScaleRaw;
825
826 #[test]
827 fn test_invalid_storage_type() {
829 assert!(StorageType::try_from("invalid").is_err());
830 }
831
832 #[test]
833 fn scale_big_min() {
835 let sr = ScaleRaw { min: 29, max: 30 };
836 let scale = Scale::from(&sr);
837 assert!(scale.is_err());
838 }
839
840 #[test]
841 fn scale_big_max() {
843 let sr = ScaleRaw { min: 1, max: 29 };
844 let scale = Scale::from(&sr);
845 assert!(scale.is_err());
846 }
847
848 #[test]
849 fn scale_max_min() {
851 let sr = ScaleRaw { min: 2, max: 1 };
852 let scale = Scale::from(&sr);
853 assert!(scale.is_err());
854 }
855
856 #[test]
857 fn scale_zeros() {
859 let sr = ScaleRaw { min: 0, max: 0 };
860 let scale = Scale::from(&sr);
861 assert!(scale.is_ok());
862 }
863
864 #[test]
865 fn scale_valid_values() {
867 let sr = ScaleRaw { min: 2, max: 4 };
868 let scale = Scale::from(&sr);
869 assert!(scale.is_ok());
870 }
871}