Skip to main content

tackler_core/config/
items.rs

1/*
2 * Tackler-NG 2024-2025
3 * SPDX-License-Identifier: Apache-2.0
4 */
5use 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
25/// UI/CFG key value for none
26pub 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    /// Storage type from string
42    ///
43    /// # Errors
44    /// Returns `Err` in case of invalid type
45    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    /// Report type from string
129    ///
130    /// # Errors
131    /// Returns `Err` in case of an invalid type
132    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    /// Export type from string
161    ///
162    /// # Errors
163    /// Returns `Err` in case of invalid type
164    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 of config file for this configuration
237    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    /// Create configuration
247    ///
248    /// # Errors
249    /// Returns `Err` in case there are syntactical or semantic errors with config
250    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        // todo: checks
369        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 &reg_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    // test: 195971d7-f16f-4c1c-a761-6764b28fd4db
828    fn test_invalid_storage_type() {
829        assert!(StorageType::try_from("invalid").is_err());
830    }
831
832    #[test]
833    // test: 2cc212bb-f167-4d42-a0e8-8124b3704e1c
834    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    // test: 698ef5a8-2d4c-4d5a-87b1-9df12051e2d7
842    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    // test: 999044e8-b3e6-447e-a15d-22e23cfdee1b
850    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    // test: 1076287b-22f2-4601-8e7e-f2899b71533d
858    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    // test: 02663b5d-1471-471a-befc-5f093e6993ee
866    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}