okane_core/report/
error.rs1use 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#[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#[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 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}