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