Skip to main content

sl_chat_log_parser/
utils.rs

1//! Parsing utilities and general parsers
2
3#[cfg(test)]
4use ariadne::{Color, Fmt as _, Label, Report, ReportKind, Source};
5use chumsky::IterParser as _;
6use chumsky::Parser;
7use chumsky::prelude::{just, one_of};
8
9/// parse an iso8601 timestamp into a time::OffsetDateTime
10///
11/// # Errors
12///
13/// returns an error if the string could not be parsed
14#[must_use]
15pub fn offset_datetime_parser<'src>() -> impl Parser<
16    'src,
17    &'src str,
18    time::OffsetDateTime,
19    chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
20> {
21    one_of("0123456789")
22        .repeated()
23        .exactly(4)
24        .collect::<String>()
25        .then_ignore(just('-'))
26        .then(
27            one_of("0123456789")
28                .repeated()
29                .exactly(2)
30                .collect::<String>(),
31        )
32        .then_ignore(just('-'))
33        .then(
34            one_of("0123456789")
35                .repeated()
36                .exactly(2)
37                .collect::<String>(),
38        )
39        .then_ignore(just('T'))
40        .then(
41            one_of("0123456789")
42                .repeated()
43                .exactly(2)
44                .collect::<String>(),
45        )
46        .then_ignore(just(':'))
47        .then(
48            one_of("0123456789")
49                .repeated()
50                .exactly(2)
51                .collect::<String>(),
52        )
53        .then_ignore(just(':'))
54        .then(
55            one_of("0123456789")
56                .repeated()
57                .exactly(2)
58                .collect::<String>(),
59        )
60        .then_ignore(just('.'))
61        .then(
62            one_of("0123456789")
63                .repeated()
64                .exactly(6)
65                .collect::<String>(),
66        )
67        .then_ignore(just('Z'))
68        .try_map(
69            |((((((year, month), day), hour), minute), second), microsecond), span| {
70                let input = format!("{year}-{month}-{day}T{hour}:{minute}:{second}.{microsecond}Z");
71                let format = time::macros::format_description!(
72                    "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6]Z"
73                );
74                time::PrimitiveDateTime::parse(&input, format)
75                    .map(time::PrimitiveDateTime::assume_utc)
76                    .map_err(|e| chumsky::error::Rich::custom(span, format!("{e:?}")))
77            },
78        )
79}
80
81/// a wrapped error in case parsing fails to get proper error output
82/// the chumsky errors themselves lack Display and std::error::Error
83/// implementations
84#[cfg(test)]
85#[derive(Debug)]
86pub struct ChumskyError<E> {
87    /// description of the object we were trying to parse
88    pub description: String,
89    /// source string for parsing
90    pub source: String,
91    /// errors encountered during parsing
92    pub errors: Vec<E>,
93}
94
95#[cfg(test)]
96impl std::fmt::Display for ChumskyError<chumsky::error::Rich<'static, char>> {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        for e in &self.errors {
99            let msg = format!(
100                "While parsing {}: {}{}, expected {}",
101                self.description,
102                if e.found().is_some() {
103                    "Unexpected token"
104                } else {
105                    "Unexpected end of input"
106                },
107                format_args!(" while parsing {:?}", e.contexts().collect::<Vec<_>>()),
108                if e.expected().len() == 0 {
109                    "end of input".to_string()
110                } else {
111                    e.expected()
112                        .map(|rich_pattern| rich_pattern.to_string())
113                        .collect::<Vec<_>>()
114                        .join(", ")
115                },
116            );
117
118            let report = Report::build(ReportKind::Error, e.span().start..e.span().end)
119                .with_code(3)
120                .with_message(msg)
121                .with_label(
122                    Label::new(e.span().start..e.span().end)
123                        .with_message(format!(
124                            "Unexpected {}",
125                            e.found().map_or_else(
126                                || "end of input".to_string(),
127                                |c| format!("token {}", c.fg(Color::Red))
128                            )
129                        ))
130                        .with_color(Color::Red),
131                );
132
133            let report = match e.reason() {
134                chumsky::error::RichReason::ExpectedFound {
135                    expected: _,
136                    found: _,
137                } => report,
138                chumsky::error::RichReason::Custom(msg) => report.with_label(
139                    Label::new(e.span().start..e.span().end)
140                        .with_message(format!("{}", msg.fg(Color::Yellow)))
141                        .with_color(Color::Yellow),
142                ),
143            };
144
145            let mut s: Vec<u8> = Vec::new();
146            report
147                .finish()
148                .write(Source::from(&self.source), &mut s)
149                .map_err(|_err| <std::fmt::Error as std::default::Default>::default())?;
150            let s = std::str::from_utf8(&s)
151                .map_err(|_err| <std::fmt::Error as std::default::Default>::default())?;
152            write!(f, "{s}")?;
153        }
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159impl<E> std::error::Error for ChumskyError<E>
160where
161    E: std::fmt::Debug,
162    Self: std::fmt::Display,
163{
164    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
165        None
166    }
167}