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