okane_core/report/
error.rs

1//! Defines error in report functions.
2
3use std::{fmt::Display, path::PathBuf};
4
5use annotate_snippets::{Annotation, AnnotationKind, Level, Snippet};
6use bumpalo::Bump;
7
8use crate::{
9    load,
10    parse::{self, ParsedSpan},
11};
12
13use super::{
14    book_keeping::{self, BookKeepError},
15    price_db,
16};
17
18/// Error arised in report APIs.
19#[derive(thiserror::Error, Debug)]
20pub enum ReportError {
21    Load(#[from] load::LoadError),
22    PriceDB(#[from] price_db::LoadError),
23    BookKeep(book_keeping::BookKeepError, Box<ErrorContext>),
24}
25
26impl Display for ReportError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            ReportError::Load(_) => write!(f, "failed to load the given file"),
30            ReportError::PriceDB(_) => write!(f, "failed to load the Price DB"),
31            ReportError::BookKeep(err, ctx) => ctx.print(f, err),
32        }
33    }
34}
35
36/// Context of [ReportError], to carry the failure information.
37#[derive(Debug)]
38pub struct ErrorContext {
39    renderer: annotate_snippets::Renderer,
40    path: PathBuf,
41    line_start: usize,
42    text: String,
43    parsed_span: ParsedSpan,
44}
45
46impl ErrorContext {
47    fn print(&self, f: &mut std::fmt::Formatter<'_>, err: &BookKeepError) -> std::fmt::Result {
48        let message = err.to_string();
49        let path = self.path.to_string_lossy();
50        let bump = Bump::new();
51        let annotations: Vec<Annotation> = match err {
52            BookKeepError::UndeduciblePostingAmount(first, second) => vec![
53                AnnotationKind::Context
54                    .span(self.parsed_span.resolve(&first.span()))
55                    .label("first posting without constraints"),
56                AnnotationKind::Primary
57                    .span(self.parsed_span.resolve(&second.span()))
58                    .label("cannot deduce this posting"),
59            ],
60            BookKeepError::BalanceAssertionFailure {
61                balance_span,
62                account_span,
63                computed,
64                ..
65            } => {
66                let msg = bumpalo::format!(
67                    in &bump,
68                    "computed balance: {}", computed,
69                );
70                vec![
71                    AnnotationKind::Primary
72                        .span(self.parsed_span.resolve(balance_span))
73                        .label("not match the computed balance"),
74                    AnnotationKind::Context
75                        .span(self.parsed_span.resolve(account_span))
76                        .label(msg.into_bump_str()),
77                ]
78            }
79            BookKeepError::ZeroAmountWithExchange(exchange) => vec![AnnotationKind::Primary
80                .span(self.parsed_span.resolve(exchange))
81                .label("absolute zero posting should not have exchange")],
82            BookKeepError::ZeroExchangeRate(exchange) => vec![AnnotationKind::Primary
83                .span(self.parsed_span.resolve(exchange))
84                .label("exchange with zero amount")],
85            BookKeepError::ExchangeWithAmountCommodity {
86                posting_amount,
87                exchange,
88            } => vec![
89                AnnotationKind::Context
90                    .span(self.parsed_span.resolve(posting_amount))
91                    .label("posting amount"),
92                AnnotationKind::Primary
93                    .span(self.parsed_span.resolve(exchange))
94                    .label("exchange cannot have the same commodity with posting"),
95            ],
96            _ => {
97                // TODO: Add more detailed error into this.
98                // Also, put these logic into BookKeepError.
99                vec![AnnotationKind::Primary
100                    .span(0..self.text.len())
101                    .label("error occured")]
102            }
103        };
104        let message = Level::ERROR.primary_title(&message).element(
105            Snippet::source(&self.text)
106                .path(&path)
107                .line_start(self.line_start)
108                .fold(false)
109                .annotations(annotations),
110        );
111        let rendered = self.renderer.render(&[message]);
112        rendered.fmt(f)
113    }
114
115    pub(super) fn new(
116        renderer: annotate_snippets::Renderer,
117        path: PathBuf,
118        pctx: &parse::ParsedContext,
119    ) -> Box<Self> {
120        Box::new(Self {
121            renderer,
122            path,
123            line_start: pctx.compute_line_start(),
124            text: pctx.as_str().to_owned(),
125            parsed_span: pctx.span(),
126        })
127    }
128}