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, 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
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(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        // todo: checks
384        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 &reg_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    // test: 195971d7-f16f-4c1c-a761-6764b28fd4db
843    fn test_invalid_storage_type() {
844        assert!(StorageType::try_from("invalid").is_err());
845    }
846
847    #[test]
848    // test: 2cc212bb-f167-4d42-a0e8-8124b3704e1c
849    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    // test: 698ef5a8-2d4c-4d5a-87b1-9df12051e2d7
857    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    // test: 999044e8-b3e6-447e-a15d-22e23cfdee1b
865    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    // test: 1076287b-22f2-4601-8e7e-f2899b71533d
873    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    // test: 02663b5d-1471-471a-befc-5f093e6993ee
881    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}