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_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
25pub 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 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 ®_raw.timestamp_style {
580 Some(style) => TimestampStyle::from(style.as_str())?,
581 None => TimestampStyle::Date,
582 },
583 acc_sel: get_account_selector(®_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}