okane_core/report/
error.rs

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