1use crate::config::raw_items::{
6 AccountsPathRaw, AccountsRaw, AuditRaw, BalanceGroupRaw, BalanceRaw, CommoditiesPathRaw,
7 CommoditiesRaw, ConfigRaw, EquityRaw, ExportRaw, ExtIdRaw, FsRaw, GitRaw, InputRaw, KernelRaw,
8 PriceRaw, 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(crate) extid: ExtId,
276 pub input: Input,
277}
278impl Kernel {
279 fn try_from(k_raw: &KernelRaw) -> Result<Kernel, tackler::Error> {
280 let k = Kernel {
281 strict: k_raw.strict,
282 timestamp: Timestamp::from(&k_raw.timestamp)?,
283 audit: Audit::from(&k_raw.audit)?,
284 extid: ExtId::from(k_raw.extid.as_ref()),
285 input: Input::try_from(&k_raw.input)?,
286 };
287 Ok(k)
288 }
289}
290
291#[derive(Debug, Clone)]
292pub(crate) struct Timestamp {
293 pub(crate) default_time: jiff::civil::Time,
294 pub(crate) timezone: jiff::tz::TimeZone,
295}
296
297impl Default for Timestamp {
298 fn default() -> Self {
299 Timestamp {
300 default_time: jiff::civil::Time::midnight(),
301 timezone: jiff::tz::Offset::UTC.to_time_zone(),
302 }
303 }
304}
305
306impl Timestamp {
307 #[allow(clippy::cast_possible_wrap)]
308 fn from(ts_raw: &TimestampRaw) -> Result<Timestamp, tackler::Error> {
309 let ts = Timestamp {
310 default_time: {
311 let t = ts_raw.default_time;
312 jiff::civil::Time::new(
313 t.hour as i8,
314 t.minute as i8,
315 t.second.map_or(0, |s| s as i8),
316 t.nanosecond.map_or(0, |s| s as i32),
317 )?
318 },
319 timezone: { Timezone::from(&ts_raw.timezone)? },
320 };
321 Ok(ts)
322 }
323}
324
325impl Timezone {
326 fn from(tz_raw: &TimezoneRaw) -> Result<jiff::tz::TimeZone, tackler::Error> {
327 let tz = match (&tz_raw.name, &tz_raw.offset) {
328 (Some(_), Some(_)) => {
329 let msg = "timezone: 'name' and 'offset' are both defined".to_string();
330 return Err(msg.into());
331 }
332 (Some(tz_name), None) => jiff::tz::TimeZone::get(tz_name)?,
333 (None, Some(offset)) => {
334 if let Some(tm) = BrokenDownTime::parse("%:z", offset)?.offset() {
335 tm.to_time_zone()
336 } else {
337 let msg = format!("can't parse offset '{offset}' as valid offset");
338 return Err(msg.into());
339 }
340 }
341 (None, None) => jiff::tz::Offset::UTC.to_time_zone(),
342 };
343 Ok(tz)
344 }
345}
346
347#[derive(Debug, Clone, Default)]
348pub(crate) struct Audit {
349 pub(crate) hash: Hash,
350 pub(crate) mode: bool,
351}
352impl Audit {
353 fn from(a_raw: &AuditRaw) -> Result<Audit, tackler::Error> {
354 let a = Audit {
355 hash: Hash::from(&a_raw.hash)?,
356 mode: a_raw.mode,
357 };
358 Ok(a)
359 }
360}
361
362#[derive(Debug, Clone, Default)]
363pub(crate) struct ExtId {
364 pub(crate) unique: bool,
365}
366
367impl ExtId {
368 fn from(a_raw: Option<&ExtIdRaw>) -> ExtId {
369 ExtId {
370 unique: a_raw.is_some_and(|extid| extid.unique),
371 }
372 }
373}
374
375#[derive(Debug, Clone, Default)]
376pub struct Input {
377 pub storage: StorageType,
378 pub fs: Option<FS>,
379 pub git: Option<Git>,
380}
381impl Input {
382 fn try_from(input_raw: &InputRaw) -> Result<Input, tackler::Error> {
383 let i = Input {
385 storage: StorageType::try_from(input_raw.storage.as_str())?,
386 fs: match &input_raw.fs {
387 Some(fs) => Some(FS::try_from(fs)?),
388 None => None,
389 },
390 git: match &input_raw.git {
391 Some(git) => Some(Git::try_from(git)?),
392 None => None,
393 },
394 };
395 Ok(i)
396 }
397}
398
399#[derive(Debug, Clone, Default)]
400pub struct FS {
401 pub path: String,
402 pub dir: String,
403 pub ext: String,
404}
405impl FS {
406 fn try_from(fs_raw: &FsRaw) -> Result<FS, tackler::Error> {
407 let extension = match &fs_raw.ext {
408 Some(ext) => {
409 if fs_raw.suffix.is_some() {
410 let msg = "FS has both 'suffix' and 'ext' keys defined";
411 return Err(msg.into());
412 }
413 ext.clone()
414 }
415 None => {
416 if let Some(ext) = &fs_raw.suffix {
417 ext.clone()
418 } else {
419 let msg = "FS is missing 'ext' key";
420 return Err(msg.into());
421 }
422 }
423 };
424
425 let ext = extension
426 .strip_prefix('.')
427 .unwrap_or(extension.as_str())
428 .to_string();
429 Ok(FS {
430 path: fs_raw.path.clone().unwrap_or_default(),
431 dir: fs_raw.dir.clone(),
432 ext,
433 })
434 }
435}
436
437#[derive(Debug, Clone, Default)]
438pub struct Git {
439 pub repo: String,
440 pub reference: String,
441 pub dir: String,
442 pub ext: String,
443}
444impl Git {
445 fn try_from(git_raw: &GitRaw) -> Result<Git, tackler::Error> {
446 let repo = match &git_raw.repo {
447 Some(repo) => {
448 if git_raw.repository.is_some() {
449 let msg = "Git has both 'repo' and 'repository' keys defined";
450 return Err(msg.into());
451 }
452 repo.clone()
453 }
454 None => {
455 if let Some(repo) = &git_raw.repository {
456 repo.clone()
457 } else {
458 let msg = "Git is missing 'repo' key";
459 return Err(msg.into());
460 }
461 }
462 };
463 let extension = match &git_raw.ext {
464 Some(ext) => {
465 if git_raw.suffix.is_some() {
466 let msg = "Git has both 'suffix' and 'ext' keys defined";
467 return Err(msg.into());
468 }
469 ext.clone()
470 }
471 None => {
472 if let Some(ext) = &git_raw.suffix {
473 ext.clone()
474 } else {
475 let msg = "Git is missing 'ext' key";
476 return Err(msg.into());
477 }
478 }
479 };
480 let ext = extension
481 .strip_prefix('.')
482 .unwrap_or(extension.as_str())
483 .to_string();
484 Ok(Git {
485 repo,
486 reference: git_raw.git_ref.clone(),
487 dir: git_raw.dir.clone(),
488 ext,
489 })
490 }
491}
492
493#[derive(Debug, Clone, Default)]
494pub(crate) struct Price {
495 pub(crate) db_path: PathBuf,
496 pub(crate) lookup_type: PriceLookupType,
497}
498impl Price {
499 fn try_from<P: AsRef<Path>>(
500 base_path: P,
501 price_raw: &PriceRaw,
502 ) -> Result<Price, tackler::Error> {
503 let db_path_str = price_raw.db_path.as_str();
504 let lookup_type = PriceLookupType::try_from(price_raw.lookup_type.as_str())?;
505
506 match db_path_str {
507 NONE_VALUE => {
508 if lookup_type == PriceLookupType::None {
509 Ok(Price::default())
510 } else {
511 let msg = "Price database path is 'none' but lookup type is not 'none'";
512 Err(msg.into())
513 }
514 }
515 _ => Ok(Price {
516 db_path: get_abs_path(base_path, db_path_str)?,
517 lookup_type,
518 }),
519 }
520 }
521}
522
523#[derive(Debug, Clone, Default)]
524pub(crate) struct Transaction {
525 pub(crate) accounts: Accounts,
526 pub(crate) commodities: Commodities,
527 pub(crate) tags: Tags,
528}
529
530impl Transaction {
531 fn from<P: AsRef<Path>>(
532 path: P,
533 txn_raw: &TransactionRaw,
534 ) -> Result<Transaction, tackler::Error> {
535 Ok(Transaction {
536 accounts: Accounts::from(&path, &txn_raw.accounts)?,
537 commodities: Commodities::from(&path, &txn_raw.commodities)?,
538 tags: Tags::from(&path, &txn_raw.tags)?,
539 })
540 }
541}
542
543#[derive(Debug, Clone, Default)]
544pub(crate) struct Accounts {
545 pub names: Vec<String>,
546}
547impl Accounts {
548 fn from<P: AsRef<Path>>(
549 path: P,
550 accs_path_raw: &AccountsPathRaw,
551 ) -> Result<Accounts, tackler::Error> {
552 let accs_path_str = accs_path_raw.path.as_str();
553 if accs_path_str == NONE_VALUE {
554 Ok(Accounts::default())
555 } else {
556 let accs_path = get_abs_path(&path, accs_path_str)?;
557 let acc_raw: AccountsRaw = match fs::read_to_string(&accs_path) {
558 Ok(s) => toml::from_str(s.as_str())?,
559 Err(err) => {
560 let msg = format!(
561 "Accounts configuration error while reading file '{accs_path_str}': {err}"
562 );
563 return Err(msg.into());
564 }
565 };
566 Ok(Accounts {
567 names: acc_raw.names,
568 })
569 }
570 }
571}
572
573#[derive(Debug, Clone, Default)]
574pub(crate) struct Commodities {
575 pub(crate) permit_empty_commodity: Option<bool>,
576
577 pub(crate) names: Vec<String>,
578}
579impl Commodities {
580 fn from<P: AsRef<Path>>(
581 path: P,
582 comm_path_raw: &CommoditiesPathRaw,
583 ) -> Result<Commodities, tackler::Error> {
584 let comm_path_str = comm_path_raw.path.as_str();
585 if comm_path_str == NONE_VALUE {
586 Ok(Commodities {
587 permit_empty_commodity: Some(true),
588 names: Vec::new(),
589 })
590 } else {
591 let comm_path = get_abs_path(&path, comm_path_str)?;
592 let comm_raw: CommoditiesRaw = match fs::read_to_string(&comm_path) {
593 Ok(s) => toml::from_str(s.as_str())?,
594 Err(err) => {
595 let msg = format!(
596 "Commodities configuration error while reading file '{comm_path_str}': {err}"
597 );
598 return Err(msg.into());
599 }
600 };
601 Ok(Commodities {
602 permit_empty_commodity: comm_raw.permit_empty_commodity,
603 names: comm_raw.names,
604 })
605 }
606 }
607}
608
609#[derive(Debug, Clone, Default)]
610pub(crate) struct Tags {
611 pub(crate) names: Vec<String>,
612}
613
614impl Tags {
615 fn from<P: AsRef<Path>>(path: P, tags_path_raw: &TagsPathRaw) -> Result<Tags, tackler::Error> {
616 let tags_path_str = tags_path_raw.path.as_str();
617 if tags_path_str == NONE_VALUE {
618 Ok(Tags::default())
619 } else {
620 let tags_path = get_abs_path(&path, tags_path_str)?;
621 let tags_raw: TagsRaw = match fs::read_to_string(&tags_path) {
622 Ok(s) => toml::from_str(s.as_str())?,
623 Err(err) => {
624 let msg = format!(
625 "Tags configuration error while reading file '{tags_path_str}': {err}"
626 );
627 return Err(msg.into());
628 }
629 };
630 Ok(Tags {
631 names: tags_raw.names,
632 })
633 }
634 }
635}
636
637#[derive(Debug, Clone)]
638pub(crate) struct Report {
639 pub tz: TimeZone,
640 pub targets: Vec<ReportType>,
641 pub formats: Vec<FormatType>,
642 pub scale: Scale,
643 pub commodity: Option<Arc<Commodity>>,
644 pub register: Register,
645 pub balance_group: BalanceGroup,
646 pub balance: Balance,
647}
648
649impl Default for Report {
650 fn default() -> Self {
651 Report {
652 tz: TimeZone::UTC,
653 targets: Vec::new(),
654 formats: Vec::new(),
655 scale: Scale::default(),
656 commodity: None,
657 register: Register::default(),
658 balance_group: BalanceGroup::default(),
659 balance: Balance::default(),
660 }
661 }
662}
663
664impl Report {
665 fn from(report_raw: &ReportRaw) -> Result<Report, tackler::Error> {
666 let targets = to_report_targets(&report_raw.targets)?;
667 let formats = to_report_formats(report_raw.formats.as_deref())?;
668
669 Ok(Report {
670 tz: TimeZone::get(report_raw.report_tz.as_str())?,
671 targets,
672 formats,
673 scale: Scale::from(&report_raw.scale)?,
674 commodity: match &report_raw.commodity {
675 Some(c) => Some(Arc::new(Commodity::from(c)?)),
676 None => None,
677 },
678 register: Register::from(&report_raw.register, report_raw)?,
679 balance_group: BalanceGroup::from(&report_raw.balance_group, report_raw)?,
680 balance: Balance::from(&report_raw.balance, report_raw)?,
681 })
682 }
683}
684
685#[derive(Debug, Clone)]
686pub struct Scale {
687 min: u32,
688 max: u32,
689}
690impl Scale {
691 pub fn try_from(min: u32, max: u32) -> Result<Scale, tackler::Error> {
692 Self::check_range(min, max)?;
693 Ok(Scale { min, max })
694 }
695
696 fn check_range(min: u32, max: u32) -> Result<(), tackler::Error> {
697 let max_scale = 28;
698 if min > max_scale || max > max_scale {
699 let msg = format!(
700 "scale error: too large value - maximum scale value for min or max is {max_scale}"
701 );
702 return Err(msg.into());
703 }
704 if max < min {
705 let msg = "scale error: 'min' can't be greater than 'max'";
706 return Err(msg.into());
707 }
708 Ok(())
709 }
710 fn from(scale_raw: &ScaleRaw) -> Result<Scale, tackler::Error> {
711 Self::check_range(scale_raw.min, scale_raw.max)?;
712 Ok(Scale {
713 min: scale_raw.min,
714 max: scale_raw.max,
715 })
716 }
717 pub fn get_precision(&self, d: &Decimal) -> usize {
718 cmp::max(cmp::min(d.scale(), self.max), self.min) as usize
719 }
720}
721
722impl Default for Scale {
723 fn default() -> Self {
724 Scale { min: 2, max: 7 }
725 }
726}
727
728fn get_account_selector(
729 acc_sel: Option<&AccountSelectors>,
730 report: &ReportRaw,
731) -> AccountSelectors {
732 match acc_sel {
733 Some(av) => av.clone(),
734 None => match &report.accounts {
735 Some(av) => av.clone(),
736 None => vec![],
737 },
738 }
739}
740
741#[derive(Debug, Clone, Default)]
742pub(crate) struct Register {
743 pub title: String,
744 pub timestamp_style: TimestampStyle,
745 pub acc_sel: AccountSelectors,
746}
747
748impl Register {
749 fn from(reg_raw: &RegisterRaw, report: &ReportRaw) -> Result<Register, tackler::Error> {
750 Ok(Register {
751 title: reg_raw.title.clone(),
752 timestamp_style: match ®_raw.timestamp_style {
753 Some(style) => TimestampStyle::from(style.as_str())?,
754 None => TimestampStyle::Date,
755 },
756 acc_sel: get_account_selector(reg_raw.acc_sel.as_ref(), report),
757 })
758 }
759}
760
761#[derive(Debug, Clone, Default)]
762pub(crate) struct BalanceGroup {
763 pub title: String,
764 pub bal_type: BalanceType,
765 pub group_by: GroupBy,
766 pub acc_sel: AccountSelectors,
767}
768
769impl BalanceGroup {
770 fn from(
771 balgrp_raw: &BalanceGroupRaw,
772 report: &ReportRaw,
773 ) -> Result<BalanceGroup, tackler::Error> {
774 Ok(BalanceGroup {
775 title: balgrp_raw.title.clone(),
776 bal_type: match &balgrp_raw.bal_type {
777 Some(t) => BalanceType::try_from(t.as_str())?,
778 None => BalanceType::default(),
779 },
780 group_by: GroupBy::from(balgrp_raw.group_by.as_str())?,
781 acc_sel: get_account_selector(balgrp_raw.acc_sel.as_ref(), report),
782 })
783 }
784}
785
786#[derive(Debug, Clone, Default)]
787pub(crate) struct Balance {
788 pub title: String,
789 pub bal_type: BalanceType,
790 pub acc_sel: AccountSelectors,
791}
792
793impl Balance {
794 fn from(bal_raw: &BalanceRaw, report: &ReportRaw) -> Result<Balance, tackler::Error> {
795 Ok(Balance {
796 title: bal_raw.title.clone(),
797 bal_type: match &bal_raw.bal_type {
798 Some(t) => BalanceType::try_from(t.as_str())?,
799 None => BalanceType::default(),
800 },
801 acc_sel: get_account_selector(bal_raw.acc_sel.as_ref(), report),
802 })
803 }
804}
805
806#[derive(Debug, Clone, Default)]
807pub(crate) struct Export {
808 pub targets: Vec<ExportType>,
809 pub equity: Equity,
810}
811impl Export {
812 fn from(export_raw: &ExportRaw, report: &ReportRaw) -> Result<Export, tackler::Error> {
813 let trgs = to_export_targets(&export_raw.targets)?;
814 Ok(Export {
815 targets: trgs,
816 equity: Equity::from(&export_raw.equity, report),
817 })
818 }
819}
820
821#[derive(Debug, Clone, Default)]
822pub(crate) struct Equity {
823 pub(crate) equity_account: String,
824 pub(crate) acc_sel: AccountSelectors,
825}
826
827impl Equity {
828 fn from(eq_raw: &EquityRaw, report: &ReportRaw) -> Equity {
829 Equity {
830 equity_account: eq_raw.equity_account.clone(),
831 acc_sel: get_account_selector(eq_raw.acc_sel.as_ref(), report),
832 }
833 }
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839 use crate::config::raw_items::ScaleRaw;
840
841 #[test]
842 fn test_invalid_storage_type() {
844 assert!(StorageType::try_from("invalid").is_err());
845 }
846
847 #[test]
848 fn scale_big_min() {
850 let sr = ScaleRaw { min: 29, max: 30 };
851 let scale = Scale::from(&sr);
852 assert!(scale.is_err());
853 }
854
855 #[test]
856 fn scale_big_max() {
858 let sr = ScaleRaw { min: 1, max: 29 };
859 let scale = Scale::from(&sr);
860 assert!(scale.is_err());
861 }
862
863 #[test]
864 fn scale_max_min() {
866 let sr = ScaleRaw { min: 2, max: 1 };
867 let scale = Scale::from(&sr);
868 assert!(scale.is_err());
869 }
870
871 #[test]
872 fn scale_zeros() {
874 let sr = ScaleRaw { min: 0, max: 0 };
875 let scale = Scale::from(&sr);
876 assert!(scale.is_ok());
877 }
878
879 #[test]
880 fn scale_valid_values() {
882 let sr = ScaleRaw { min: 2, max: 4 };
883 let scale = Scale::from(&sr);
884 assert!(scale.is_ok());
885 }
886}