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_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
25/// UI/CFG key value for none
26pub 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        // todo: checks
309        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 &reg_raw.timestamp_style {
677                Some(style) => TimestampStyle::from(style.as_str())?,
678                None => TimestampStyle::Date,
679            },
680            acc_sel: get_account_selector(&reg_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}