sl_chat_log_parser/
utils.rs

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