1use serde::{Deserialize, Serialize};
2
3use enum_as_inner::EnumAsInner;
4use schemars::JsonSchema;
5
6#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
7pub struct Tokens(pub Vec<Token>);
8
9#[derive(Clone, PartialEq, Serialize, Deserialize, Eq, JsonSchema)]
10pub struct Token {
11 pub kind: TokenKind,
12 pub span: std::ops::Range<usize>,
13}
14
15#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
16pub enum TokenKind {
17 NewLine,
18
19 Ident(String),
20 Keyword(String),
21 #[cfg_attr(
22 feature = "serde_yaml",
23 serde(with = "serde_yaml::with::singleton_map"),
24 schemars(with = "Literal")
25 )]
26 Literal(Literal),
27 Param(String),
29
30 Range {
31 bind_left: bool,
34 bind_right: bool,
35 },
36 Interpolation(char, String),
37
38 Control(char),
40
41 ArrowThin, ArrowFat, Eq, Ne, Gte, Lte, RegexSearch, And, Or, Coalesce, DivInt, Pow, Annotate, Comment(String),
57 DocComment(String),
58 LineWrap(Vec<TokenKind>),
72
73 Start,
76}
77
78#[derive(
79 Debug, EnumAsInner, PartialEq, Clone, Serialize, Deserialize, strum::AsRefStr, JsonSchema,
80)]
81pub enum Literal {
82 Null,
83 Integer(i64),
84 Float(f64),
85 Boolean(bool),
86 String(String),
87 RawString(String),
88 Date(String),
89 Time(String),
90 Timestamp(String),
91 ValueAndUnit(ValueAndUnit),
92}
93
94impl TokenKind {
95 pub fn range(bind_left: bool, bind_right: bool) -> Self {
96 TokenKind::Range {
97 bind_left,
98 bind_right,
99 }
100 }
101}
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
104pub struct ValueAndUnit {
105 pub n: i64, pub unit: String, }
108
109impl std::fmt::Display for Literal {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 Literal::Null => write!(f, "null")?,
113 Literal::Integer(i) => write!(f, "{i}")?,
114 Literal::Float(i) => write!(f, "{i}")?,
115
116 Literal::String(s) => {
117 write!(
118 f,
119 "{}",
120 quote_string(escape_all_except_quotes(s).as_str(), true)
121 )?;
122 }
123
124 Literal::RawString(s) => {
125 write!(f, "r{}", quote_string(s, false))?;
126 }
127
128 Literal::Boolean(b) => {
129 f.write_str(if *b { "true" } else { "false" })?;
130 }
131
132 Literal::Date(inner) | Literal::Time(inner) | Literal::Timestamp(inner) => {
133 write!(f, "@{inner}")?;
134 }
135
136 Literal::ValueAndUnit(i) => {
137 write!(f, "{}{}", i.n, i.unit)?;
138 }
139 }
140 Ok(())
141 }
142}
143
144fn quote_string(s: &str, allow_escape: bool) -> String {
150 if !s.contains('"') {
151 return format!(r#""{s}""#);
152 }
153
154 if !s.contains('\'') {
155 return format!("'{s}'");
156 }
157
158 let double_safe = !s.starts_with('"') && !s.ends_with('"');
163 let single_safe = !s.starts_with('\'') && !s.ends_with('\'');
164
165 let quote = if double_safe {
166 '"'
167 } else if single_safe {
168 '\''
169 } else if allow_escape {
170 return format!("\"{}\"", s.replace('"', "\\\""));
173 } else {
174 '"'
178 };
179
180 let max_consecutive = s
189 .split(|c| c != quote)
190 .map(|quote_sequence| quote_sequence.len())
191 .max()
192 .unwrap_or(0);
193 let next_odd = max_consecutive.div_ceil(2) * 2 + 1;
194 let delim = quote.to_string().repeat(next_odd);
195
196 format!("{delim}{s}{delim}")
197}
198
199fn escape_all_except_quotes(s: &str) -> String {
200 let mut result = String::new();
201 for ch in s.chars() {
202 if ch == '"' || ch == '\'' {
203 result.push(ch);
204 } else {
205 result.extend(ch.escape_default());
206 }
207 }
208 result
209}
210
211#[allow(clippy::derived_hash_with_manual_eq)]
216impl std::hash::Hash for TokenKind {
217 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
218 core::mem::discriminant(self).hash(state);
219 }
220}
221
222impl std::cmp::Eq for TokenKind {}
223
224impl std::fmt::Display for TokenKind {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 TokenKind::NewLine => write!(f, "new line"),
228 TokenKind::Ident(s) => {
229 if s.is_empty() {
230 write!(f, "an identifier")
232 } else {
233 write!(f, "{s}")
234 }
235 }
236 TokenKind::Keyword(s) => write!(f, "keyword {s}"),
237 TokenKind::Literal(lit) => write!(f, "{lit}"),
238 TokenKind::Control(c) => write!(f, "{c}"),
239
240 TokenKind::ArrowThin => f.write_str("->"),
241 TokenKind::ArrowFat => f.write_str("=>"),
242 TokenKind::Eq => f.write_str("=="),
243 TokenKind::Ne => f.write_str("!="),
244 TokenKind::Gte => f.write_str(">="),
245 TokenKind::Lte => f.write_str("<="),
246 TokenKind::RegexSearch => f.write_str("~="),
247 TokenKind::And => f.write_str("&&"),
248 TokenKind::Or => f.write_str("||"),
249 TokenKind::Coalesce => f.write_str("??"),
250 TokenKind::DivInt => f.write_str("//"),
251 TokenKind::Pow => f.write_str("**"),
252 TokenKind::Annotate => f.write_str("@"),
253
254 TokenKind::Param(id) => write!(f, "${id}"),
255
256 TokenKind::Range {
257 bind_left,
258 bind_right,
259 } => write!(
260 f,
261 "'{}..{}'",
262 if *bind_left { "" } else { " " },
263 if *bind_right { "" } else { " " }
264 ),
265 TokenKind::Interpolation(c, s) => {
266 write!(f, "{c}\"{s}\"")
267 }
268 TokenKind::Comment(s) => {
269 writeln!(f, "#{s}")
270 }
271 TokenKind::DocComment(s) => {
272 writeln!(f, "#!{s}")
273 }
274 TokenKind::LineWrap(comments) => {
275 write!(f, "\n\\ ")?;
276 for comment in comments {
277 write!(f, "{comment}")?;
278 }
279 Ok(())
280 }
281 TokenKind::Start => write!(f, "start of input"),
282 }
283 }
284}
285
286impl std::fmt::Debug for Token {
287 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
288 write!(f, "{}..{}: {:?}", self.span.start, self.span.end, self.kind)
289 }
290}
291
292#[cfg(test)]
293mod test {
294 use insta::assert_snapshot;
295
296 use super::*;
297
298 #[test]
299 fn test_string_quoting() {
300 fn make_str(s: &str) -> Literal {
301 Literal::String(s.to_string())
302 }
303
304 assert_snapshot!(
305 make_str("hello").to_string(),
306 @r#""hello""#
307 );
308
309 assert_snapshot!(
310 make_str(r#"he's nice"#).to_string(),
311 @r#""he's nice""#
312 );
313
314 assert_snapshot!(
315 make_str(r#"he said "what up""#).to_string(),
316 @r#"'he said "what up"'"#
317 );
318
319 assert_snapshot!(
320 make_str(r#"he said "what's up""#).to_string(),
321 @r#"'''he said "what's up"'''"#
322 );
323
324 assert_snapshot!(
325 make_str(r#" single' three double""" four double"""" "#).to_string(),
326 @r#"""""" single' three double""" four double"""" """"""#
327
328 );
329
330 assert_snapshot!(
331 make_str(r#""Starts with a double quote and ' contains a single quote"#).to_string(),
332 @r#"'''"Starts with a double quote and ' contains a single quote'''"#
333 );
334 }
335
336 #[test]
340 fn test_string_quoting_both_boundary_quotes() {
341 assert_snapshot!(
342 Literal::String(r#""x'"#.to_string()).to_string(),
343 @r#""\"x'""#
344 );
345 assert_snapshot!(
346 Literal::String(r#"'x""#.to_string()).to_string(),
347 @r#""'x\"""#
348 );
349 }
350
351 #[test]
354 fn test_string_roundtrip_boundary() {
355 use crate::lexer::lex_source;
356 for original in [
357 r#""x'"#, r#"'x""#, r#"a"b'"#, r#"a'b""#, ] {
362 let formatted = Literal::String(original.to_string()).to_string();
363 let toks = lex_source(&formatted).unwrap();
364 let lexed: Vec<_> = toks
365 .0
366 .iter()
367 .filter_map(|t| match &t.kind {
368 TokenKind::Literal(Literal::String(s)) => Some(s.clone()),
369 _ => None,
370 })
371 .collect();
372 assert_eq!(
373 lexed,
374 vec![original.to_string()],
375 "roundtrip failed for {original:?}: formatted={formatted:?}, lexed={lexed:?}"
376 );
377 }
378 }
379
380 #[test]
381 fn test_string_escapes() {
382 assert_snapshot!(
383 Literal::String(r#"hello\nworld"#.to_string()).to_string(),
384 @r#""hello\\nworld""#
385 );
386
387 assert_snapshot!(
388 Literal::String(r#"hello\tworld"#.to_string()).to_string(),
389 @r#""hello\\tworld""#
390 );
391
392 assert_snapshot!(
408 Literal::String(r#"hello
409 world"#.to_string()).to_string(),
410 @r#""hello\n world""#
411 );
412 }
413
414 #[test]
415 fn test_raw_string_quoting() {
416 fn make_str(s: &str) -> Literal {
418 Literal::RawString(s.to_string())
419 }
420
421 assert_snapshot!(
422 make_str("hello").to_string(),
423 @r#"r"hello""#
424 );
425 }
426}