sl_chat_log_parser/
utils.rs1#[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#[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#[cfg(test)]
83#[derive(Debug)]
84pub struct ChumskyError {
85 pub description: String,
87 pub source: String,
89 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}