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_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};
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)]
29pub enum StorageType {
30    #[default]
31    FS,
32    Git,
33}
34
35#[derive(Debug, Clone, Copy, Default, PartialEq)]
36pub enum PriceLookupType {
37    #[default]
38    None,
39    LastPrice,
40    TxnTime,
41    GivenTime,
42}
43
44impl PriceLookupType {
45    pub const NONE: &'static str = NONE_VALUE;
46    pub const LAST_PRICE: &'static str = "last-price";
47    pub const TXN_TIME: &'static str = "txn-time";
48    pub const GIVEN_TIME: &'static str = "given-time";
49}
50
51impl Display for PriceLookupType {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::None => f.write_str(PriceLookupType::NONE),
55            Self::LastPrice => f.write_str(PriceLookupType::LAST_PRICE),
56            Self::TxnTime => f.write_str(PriceLookupType::TXN_TIME),
57            Self::GivenTime => f.write_str(PriceLookupType::GIVEN_TIME),
58        }
59    }
60}
61
62impl TryFrom<&str> for PriceLookupType {
63    type Error = tackler::Error;
64
65    fn try_from(lookup: &str) -> Result<PriceLookupType, tackler::Error> {
66        match lookup {
67            PriceLookupType::NONE => Ok(PriceLookupType::None),
68            PriceLookupType::LAST_PRICE => Ok(PriceLookupType::LastPrice),
69            PriceLookupType::TXN_TIME => Ok(PriceLookupType::TxnTime),
70            PriceLookupType::GIVEN_TIME => Ok(PriceLookupType::GivenTime),
71            _ => Err(format!("Unknown price lookup type: {}", lookup).into()),
72        }
73    }
74}
75
76#[rustfmt::skip]
77impl StorageType {
78    pub const STORAGE_FS:   &'static str = "fs";
79    pub const STORAGE_GIT:  &'static str = "git";
80
81    pub(crate) fn from(storage: &str) -> Result<StorageType, tackler::Error> {
82        match storage {
83            StorageType::STORAGE_FS => Ok(StorageType::FS),
84            StorageType::STORAGE_GIT => Ok(StorageType::Git),
85            _ => Err(format!("Unknown storage type: {}", storage).into()),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Default)]
91pub enum ReportType {
92    #[default]
93    Balance,
94    BalanceGroup,
95    Register,
96}
97impl ReportType {
98    pub fn from(r: &str) -> Result<Self, tackler::Error> {
99        match r {
100            "balance" => Ok(ReportType::Balance),
101            "balance-group" => Ok(ReportType::BalanceGroup),
102            "register" => Ok(ReportType::Register),
103            _ => Err(format!("Unknown report type {r}").into()),
104        }
105    }
106}
107
108#[derive(Debug, Clone, Default, PartialEq)]
109pub enum ExportType {
110    #[default]
111    Equity,
112    Identity,
113}
114impl ExportType {
115    pub fn from(r: &str) -> Result<Self, tackler::Error> {
116        match r {
117            "equity" => Ok(ExportType::Equity),
118            "identity" => Ok(ExportType::Identity),
119            _ => Err(format!("Unknown export type {r}").into()),
120        }
121    }
122}
123
124enum Timezone {}
125
126pub(crate) type AccountSelectors = Vec<String>;
127
128#[derive(Debug)]
129#[allow(dead_code)]
130pub struct Config {
131    pub(crate) kernel: Kernel,
132    pub(crate) price: Price,
133    pub(crate) transaction: Transaction,
134    pub(crate) report: Report,
135    pub(crate) export: Export,
136}
137
138impl Config {
139    pub fn from<P: AsRef<Path>>(cfg_path: P) -> Result<Config, tackler::Error> {
140        let cfg_raw: ConfigRaw = toml::from_str(fs::read_to_string(&cfg_path)?.as_str())?;
141
142        Ok(Config {
143            kernel: Kernel::from(&cfg_raw.kernel)?,
144            price: cfg_raw.price.map_or(Ok(Price::default()), |raw_price| {
145                Price::try_from(&cfg_path, &raw_price)
146            })?,
147            transaction: Transaction::from(cfg_path, &cfg_raw.transaction)?,
148            report: Report::from(&cfg_raw.report)?,
149            export: { Export::from(&cfg_raw.export, &cfg_raw.report)? },
150        })
151    }
152}
153
154#[allow(dead_code)]
155#[derive(Debug, Clone, Default)]
156pub(crate) struct Kernel {
157    pub(crate) strict: bool,
158    pub(crate) timestamp: Timestamp,
159    pub(crate) audit: Audit,
160    pub input: Input,
161}
162impl Kernel {
163    fn from(k_raw: &KernelRaw) -> Result<Kernel, tackler::Error> {
164        let k = Kernel {
165            strict: k_raw.strict,
166            timestamp: Timestamp::from(&k_raw.timestamp)?,
167            audit: Audit::from(&k_raw.audit)?,
168            input: Input::from(&k_raw.input)?,
169        };
170        Ok(k)
171    }
172}
173
174#[derive(Debug, Clone)]
175pub(crate) struct Timestamp {
176    pub(crate) default_time: jiff::civil::Time,
177    pub(crate) timezone: jiff::tz::TimeZone,
178}
179
180impl Default for Timestamp {
181    fn default() -> Self {
182        Timestamp {
183            default_time: jiff::civil::Time::midnight(),
184            timezone: jiff::tz::Offset::UTC.to_time_zone(),
185        }
186    }
187}
188
189impl Timestamp {
190    fn from(ts_raw: &TimestampRaw) -> Result<Timestamp, tackler::Error> {
191        let ts = Timestamp {
192            default_time: {
193                let t = ts_raw.default_time;
194                jiff::civil::Time::new(
195                    t.hour as i8,
196                    t.minute as i8,
197                    t.second as i8,
198                    t.nanosecond as i32,
199                )?
200            },
201            timezone: { Timezone::from(&ts_raw.timezone)? },
202        };
203        Ok(ts)
204    }
205}
206
207impl Timezone {
208    fn from(tz_raw: &TimezoneRaw) -> Result<jiff::tz::TimeZone, tackler::Error> {
209        let tz = match (&tz_raw.name, &tz_raw.offset) {
210            (Some(_), Some(_)) => {
211                let msg = "timezone: 'name' and 'offset' are both defined".to_string();
212                return Err(msg.into());
213            }
214            (Some(tz_name), None) => jiff::tz::TimeZone::get(tz_name)?,
215            (None, Some(offset)) => match BrokenDownTime::parse("%:z", offset)?.offset() {
216                Some(tm) => tm.to_time_zone(),
217                None => {
218                    let msg = format!("can't parse offset '{offset}' as valid offset");
219                    return Err(msg.into());
220                }
221            },
222            (None, None) => jiff::tz::Offset::UTC.to_time_zone(),
223        };
224        Ok(tz)
225    }
226}
227
228#[allow(dead_code)]
229#[derive(Debug, Clone, Default)]
230pub(crate) struct Audit {
231    pub(crate) hash: Hash,
232    pub(crate) mode: bool,
233}
234impl Audit {
235    fn from(a_raw: &AuditRaw) -> Result<Audit, tackler::Error> {
236        let a = Audit {
237            hash: Hash::from(&a_raw.hash)?,
238            mode: a_raw.mode,
239        };
240        Ok(a)
241    }
242}
243
244#[allow(dead_code)]
245#[derive(Debug, Clone, Default)]
246pub struct Input {
247    pub storage: StorageType,
248    pub fs: Option<FS>,
249    pub git: Option<Git>,
250}
251impl Input {
252    fn from(input_raw: &InputRaw) -> Result<Input, tackler::Error> {
253        // todo: checks
254        let i = Input {
255            storage: StorageType::from(input_raw.storage.as_str())?,
256            fs: match &input_raw.fs {
257                Some(fs) => Some(FS::from(fs)?),
258                None => None,
259            },
260            git: match &input_raw.git {
261                Some(git) => Some(Git::from(git)?),
262                None => None,
263            },
264        };
265        Ok(i)
266    }
267}
268
269#[derive(Debug, Clone, Default)]
270pub struct FS {
271    pub dir: String,
272    pub suffix: String,
273}
274impl FS {
275    fn from(fs_raw: &FsRaw) -> Result<FS, tackler::Error> {
276        let dir = match &fs_raw.path {
277            Some(path) => format!("{}/{}", path, fs_raw.dir),
278            None => fs_raw.dir.clone(),
279        };
280        Ok(FS {
281            dir,
282            suffix: fs_raw.suffix.clone(),
283        })
284    }
285}
286
287#[derive(Debug, Clone, Default)]
288pub struct Git {
289    pub repo: String,
290    pub git_ref: String,
291    pub dir: String,
292    pub suffix: String,
293}
294impl Git {
295    fn from(git_raw: &GitRaw) -> Result<Git, tackler::Error> {
296        let repo = match &git_raw.repo {
297            Some(repo) => repo.clone(),
298            None => match &git_raw.repository {
299                Some(repo) => repo.clone(),
300                None => {
301                    let msg = "Git is missing 'repo' key";
302                    return Err(msg.into());
303                }
304            },
305        };
306        Ok(Git {
307            repo,
308            git_ref: git_raw.git_ref.clone(),
309            dir: git_raw.dir.clone(),
310            suffix: git_raw.suffix.clone(),
311        })
312    }
313}
314
315#[derive(Debug, Clone, Default)]
316pub(crate) struct Price {
317    pub(crate) db_path: PathBuf,
318    pub(crate) lookup_type: PriceLookupType,
319}
320impl Price {
321    fn try_from<P: AsRef<Path>>(
322        base_path: P,
323        price_raw: &PriceRaw,
324    ) -> Result<Price, tackler::Error> {
325        let db_path_str = price_raw.db_path.as_str();
326        let lookup_type = PriceLookupType::try_from(price_raw.lookup_type.as_str())?;
327
328        match db_path_str {
329            NONE_VALUE => match lookup_type {
330                PriceLookupType::None => Ok(Price::default()),
331                _ => {
332                    let msg = "Price database path is 'none' but lookup type is not 'none'";
333                    Err(msg.into())
334                }
335            },
336            _ => Ok(Price {
337                db_path: get_abs_path(base_path, db_path_str)?,
338                lookup_type,
339            }),
340        }
341    }
342}
343
344#[derive(Debug, Clone, Default)]
345pub(crate) struct Transaction {
346    pub(crate) accounts: Accounts,
347    pub(crate) commodities: Commodities,
348    pub(crate) tags: Tags,
349}
350
351impl Transaction {
352    fn from<P: AsRef<Path>>(
353        path: P,
354        txn_raw: &TransactionRaw,
355    ) -> Result<Transaction, tackler::Error> {
356        Ok(Transaction {
357            accounts: Accounts::from(&path, &txn_raw.accounts)?,
358            commodities: Commodities::from(&path, &txn_raw.commodities)?,
359            tags: Tags::from(&path, &txn_raw.tags)?,
360        })
361    }
362}
363
364#[derive(Debug, Clone, Default)]
365pub(crate) struct Accounts {
366    pub names: Vec<String>,
367}
368impl Accounts {
369    fn from<P: AsRef<Path>>(
370        path: P,
371        accs_path_raw: &AccountsPathRaw,
372    ) -> Result<Accounts, tackler::Error> {
373        let accs_path_str = accs_path_raw.path.as_str();
374        match accs_path_str {
375            NONE_VALUE => Ok(Accounts::default()),
376            _ => {
377                let accs_path = get_abs_path(&path, accs_path_str)?;
378                let acc_raw: AccountsRaw = match fs::read_to_string(&accs_path) {
379                    Ok(s) => toml::from_str(s.as_str())?,
380                    Err(err) => {
381                        let msg = format!(
382                            "Accounts configuration error while reading file '{accs_path_str}': {err}"
383                        );
384                        return Err(msg.into());
385                    }
386                };
387                Ok(Accounts {
388                    names: acc_raw.names,
389                })
390            }
391        }
392    }
393}
394
395#[derive(Debug, Clone, Default)]
396pub(crate) struct Commodities {
397    pub(crate) permit_empty_commodity: Option<bool>,
398
399    pub(crate) names: Vec<String>,
400}
401impl Commodities {
402    fn from<P: AsRef<Path>>(
403        path: P,
404        comm_path_raw: &CommoditiesPathRaw,
405    ) -> Result<Commodities, tackler::Error> {
406        let comm_path_str = comm_path_raw.path.as_str();
407        match comm_path_str {
408            NONE_VALUE => Ok(Commodities {
409                permit_empty_commodity: Some(true),
410                names: Vec::new(),
411            }),
412            _ => {
413                let comm_path = get_abs_path(&path, comm_path_str)?;
414                let comm_raw: CommoditiesRaw = match fs::read_to_string(&comm_path) {
415                    Ok(s) => toml::from_str(s.as_str())?,
416                    Err(err) => {
417                        let msg = format!(
418                            "Commodities configuration error while reading file '{comm_path_str}': {err}"
419                        );
420                        return Err(msg.into());
421                    }
422                };
423                Ok(Commodities {
424                    permit_empty_commodity: comm_raw.permit_empty_commodity,
425                    names: comm_raw.names,
426                })
427            }
428        }
429    }
430}
431
432#[allow(dead_code)]
433#[derive(Debug, Clone, Default)]
434pub(crate) struct Tags {
435    pub(crate) names: Vec<String>,
436}
437
438impl Tags {
439    fn from<P: AsRef<Path>>(path: P, tags_path_raw: &TagsPathRaw) -> Result<Tags, tackler::Error> {
440        let tags_path_str = tags_path_raw.path.as_str();
441        match tags_path_str {
442            NONE_VALUE => Ok(Tags::default()),
443            _ => {
444                let tags_path = get_abs_path(&path, tags_path_str)?;
445                let tags_raw: TagsRaw = match fs::read_to_string(&tags_path) {
446                    Ok(s) => toml::from_str(s.as_str())?,
447                    Err(err) => {
448                        let msg = format!(
449                            "Tags configuration error while reading file '{tags_path_str}': {err}"
450                        );
451                        return Err(msg.into());
452                    }
453                };
454                Ok(Tags {
455                    names: tags_raw.names,
456                })
457            }
458        }
459    }
460}
461
462#[derive(Debug, Clone)]
463pub(crate) struct Report {
464    pub report_tz: TimeZone,
465    pub targets: Vec<ReportType>,
466    pub scale: Scale,
467    pub commodity: Option<Arc<Commodity>>,
468    pub register: Register,
469    pub balance_group: BalanceGroup,
470    pub balance: Balance,
471}
472
473impl Default for Report {
474    fn default() -> Self {
475        Report {
476            report_tz: jiff::tz::TimeZone::UTC,
477            targets: Vec::new(),
478            scale: Scale::default(),
479            commodity: None,
480            register: Register::default(),
481            balance_group: BalanceGroup::default(),
482            balance: Balance::default(),
483        }
484    }
485}
486
487impl Report {
488    fn from(report_raw: &ReportRaw) -> Result<Report, tackler::Error> {
489        let trgs = to_report_targets(&report_raw.targets)?;
490        Ok(Report {
491            report_tz: TimeZone::get(report_raw.report_tz.as_str())?,
492            targets: trgs,
493            scale: Scale::from(&report_raw.scale)?,
494            commodity: match &report_raw.commodity {
495                Some(c) => Some(Arc::new(Commodity::from(c.clone())?)),
496                None => None,
497            },
498            register: Register::from(&report_raw.register, report_raw)?,
499            balance_group: BalanceGroup::from(&report_raw.balance_group, report_raw)?,
500            balance: Balance::from(&report_raw.balance, report_raw)?,
501        })
502    }
503}
504
505#[derive(Debug, Clone)]
506pub struct Scale {
507    min: u32,
508    max: u32,
509}
510impl Scale {
511    fn from(scale_raw: &ScaleRaw) -> Result<Scale, tackler::Error> {
512        let max_scale = 28;
513        if scale_raw.min > max_scale || scale_raw.max > max_scale {
514            let msg = format!(
515                "scale error: too large value - maximum scale value for min or max is {max_scale}"
516            );
517            return Err(msg.into());
518        }
519        if scale_raw.max < scale_raw.min {
520            let msg = "scale error: 'min' can't be greater than 'max'";
521            return Err(msg.into());
522        }
523        Ok(Scale {
524            min: scale_raw.min,
525            max: scale_raw.max,
526        })
527    }
528    pub fn get_precision(&self, d: &Decimal) -> usize {
529        cmp::max(cmp::min(d.scale(), self.max), self.min) as usize
530    }
531}
532
533impl Default for Scale {
534    fn default() -> Self {
535        Scale { min: 2, max: 7 }
536    }
537}
538
539fn get_account_selector(
540    acc_sel: &Option<AccountSelectors>,
541    report: &ReportRaw,
542) -> AccountSelectors {
543    match acc_sel {
544        Some(av) => av.clone(),
545        None => match &report.accounts {
546            Some(av) => av.clone(),
547            None => vec![],
548        },
549    }
550}
551
552#[derive(Debug, Clone, Default)]
553pub(crate) struct Register {
554    pub title: String,
555    pub timestamp_style: TimestampStyle,
556    pub acc_sel: AccountSelectors,
557}
558
559impl Register {
560    fn from(reg_raw: &RegisterRaw, report: &ReportRaw) -> Result<Register, tackler::Error> {
561        Ok(Register {
562            title: reg_raw.title.clone(),
563            timestamp_style: match &reg_raw.timestamp_style {
564                Some(style) => TimestampStyle::from(style.as_str())?,
565                None => TimestampStyle::Date,
566            },
567            acc_sel: get_account_selector(&reg_raw.acc_sel, report),
568        })
569    }
570}
571
572#[derive(Debug, Clone, Default)]
573pub(crate) struct BalanceGroup {
574    pub title: String,
575    pub group_by: GroupBy,
576    pub acc_sel: AccountSelectors,
577}
578
579impl BalanceGroup {
580    fn from(
581        balgrp_raw: &BalanceGroupRaw,
582        report: &ReportRaw,
583    ) -> Result<BalanceGroup, tackler::Error> {
584        Ok(BalanceGroup {
585            title: balgrp_raw.title.clone(),
586            group_by: GroupBy::from(balgrp_raw.group_by.as_str())?,
587            acc_sel: get_account_selector(&balgrp_raw.acc_sel, report),
588        })
589    }
590}
591
592#[derive(Debug, Clone, Default)]
593pub(crate) struct Balance {
594    pub title: String,
595    pub acc_sel: AccountSelectors,
596}
597
598impl Balance {
599    fn from(bal_raw: &BalanceRaw, report: &ReportRaw) -> Result<Balance, tackler::Error> {
600        Ok(Balance {
601            title: bal_raw.title.clone(),
602            acc_sel: get_account_selector(&bal_raw.acc_sel, report),
603        })
604    }
605}
606
607#[derive(Debug, Clone, Default)]
608pub(crate) struct Export {
609    pub targets: Vec<ExportType>,
610    pub equity: Equity,
611}
612impl Export {
613    fn from(export_raw: &ExportRaw, report: &ReportRaw) -> Result<Export, tackler::Error> {
614        let trgs = to_export_targets(&export_raw.targets)?;
615        Ok(Export {
616            targets: trgs,
617            equity: Equity::from(&export_raw.equity, report)?,
618        })
619    }
620}
621
622#[derive(Debug, Clone, Default)]
623pub(crate) struct Equity {
624    pub(crate) equity_account: String,
625    pub(crate) acc_sel: AccountSelectors,
626}
627
628impl Equity {
629    fn from(eq_raw: &EquityRaw, report: &ReportRaw) -> Result<Equity, tackler::Error> {
630        Ok(Equity {
631            equity_account: eq_raw.equity_account.clone(),
632            acc_sel: get_account_selector(&eq_raw.acc_sel, report),
633        })
634    }
635}