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
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 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 ®_raw.timestamp_style {
564 Some(style) => TimestampStyle::from(style.as_str())?,
565 None => TimestampStyle::Date,
566 },
567 acc_sel: get_account_selector(®_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}