hledger_parser/
lib.rs

1//! Parser for Hledger journals.
2//! See [hledger documentation](https://hledger.org/hledger.html)
3//! for journal format description.
4
5mod component;
6mod directive;
7mod state;
8mod utils;
9
10use chumsky::error::RichReason;
11use chumsky::prelude::*;
12
13use crate::directive::directives;
14use crate::state::State;
15
16pub use crate::component::amount::Amount;
17pub use crate::component::period::interval::Interval;
18pub use crate::component::period::Period;
19pub use crate::component::price::AmountPrice;
20pub use crate::directive::{
21    Account, Assertion, AutoPosting, AutosPostingRule, Commodity, DecimalMark, Directive, Format,
22    Include, Payee, PeriodicTransaction, Posting, Price, Query, Status, Tag, Term, Transaction,
23    Year,
24};
25
26/// Parses the given content into a list of Hledger journal directives.
27///
28/// # Errors
29///
30/// Will return a list of parsing errors if input is not a valid hledger journal.
31pub fn parse<I: AsRef<str>>(contents: I) -> Result<Vec<Directive>, Vec<ParseError>> {
32    directives()
33        .then_ignore(end())
34        .parse_with_state(contents.as_ref(), &mut State::default())
35        .into_result()
36        .map_err(|errors| errors.into_iter().map(ParseError::from).collect())
37}
38
39/// Error type representing failures during parsing.
40#[derive(Debug)]
41pub struct ParseError {
42    /// The span of text where the error occurred.
43    pub span: std::ops::Range<usize>,
44    /// A human-readable description of the error.
45    pub message: String,
46}
47
48impl<'a, T: std::fmt::Debug> From<Rich<'a, T>> for ParseError {
49    fn from(rich_error: Rich<'a, T>) -> Self {
50        let span = rich_error.span().start..rich_error.span().end;
51
52        let message = match rich_error.into_reason() {
53            RichReason::Custom(msg) => msg,
54            RichReason::ExpectedFound { expected, found } => {
55                let expected_items: Vec<_> = expected
56                    .into_iter()
57                    .map(|pattern| format!("{pattern:?}"))
58                    .collect();
59
60                format!(
61                    "Expected {}{}",
62                    if expected_items.is_empty() {
63                        String::from("something else")
64                    } else {
65                        format!("one of: [{}]", expected_items.join(", "))
66                    },
67                    if let Some(found) = found {
68                        format!(", found: {found:?}")
69                    } else {
70                        String::new()
71                    }
72                )
73            }
74            RichReason::Many(reasons) => reasons
75                .into_iter()
76                .map(|reason| format!("{reason:?}"))
77                .collect::<Vec<_>>()
78                .join("; "),
79        };
80
81        ParseError { span, message }
82    }
83}