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!(f, "{}", quote_string(escape_all_except_quotes(s).as_str()))?;
118 }
119
120 Literal::RawString(s) => {
121 write!(f, "r{}", quote_string(s))?;
122 }
123
124 Literal::Boolean(b) => {
125 f.write_str(if *b { "true" } else { "false" })?;
126 }
127
128 Literal::Date(inner) | Literal::Time(inner) | Literal::Timestamp(inner) => {
129 write!(f, "@{inner}")?;
130 }
131
132 Literal::ValueAndUnit(i) => {
133 write!(f, "{}{}", i.n, i.unit)?;
134 }
135 }
136 Ok(())
137 }
138}
139
140fn quote_string(s: &str) -> String {
141 if !s.contains('"') {
142 return format!(r#""{}""#, s);
143 }
144
145 if !s.contains('\'') {
146 return format!("'{}'", s);
147 }
148
149 let quote = if s.starts_with('"') || s.ends_with('"') {
157 '\''
158 } else {
159 '"'
160 };
161
162 let max_consecutive = s
171 .split(|c| c != quote)
172 .map(|quote_sequence| quote_sequence.len())
173 .max()
174 .unwrap_or(0);
175 let next_odd = (max_consecutive + 1) / 2 * 2 + 1;
176 let delim = quote.to_string().repeat(next_odd);
177
178 format!("{}{}{}", delim, s, delim)
179}
180
181fn escape_all_except_quotes(s: &str) -> String {
182 let mut result = String::new();
183 for ch in s.chars() {
184 if ch == '"' || ch == '\'' {
185 result.push(ch);
186 } else {
187 result.extend(ch.escape_default());
188 }
189 }
190 result
191}
192
193#[allow(clippy::derived_hash_with_manual_eq)]
198impl std::hash::Hash for TokenKind {
199 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
200 core::mem::discriminant(self).hash(state);
201 }
202}
203
204impl std::cmp::Eq for TokenKind {}
205
206impl std::fmt::Display for TokenKind {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 match self {
209 TokenKind::NewLine => write!(f, "new line"),
210 TokenKind::Ident(s) => {
211 if s.is_empty() {
212 write!(f, "an identifier")
214 } else {
215 write!(f, "{s}")
216 }
217 }
218 TokenKind::Keyword(s) => write!(f, "keyword {s}"),
219 TokenKind::Literal(lit) => write!(f, "{}", lit),
220 TokenKind::Control(c) => write!(f, "{c}"),
221
222 TokenKind::ArrowThin => f.write_str("->"),
223 TokenKind::ArrowFat => f.write_str("=>"),
224 TokenKind::Eq => f.write_str("=="),
225 TokenKind::Ne => f.write_str("!="),
226 TokenKind::Gte => f.write_str(">="),
227 TokenKind::Lte => f.write_str("<="),
228 TokenKind::RegexSearch => f.write_str("~="),
229 TokenKind::And => f.write_str("&&"),
230 TokenKind::Or => f.write_str("||"),
231 TokenKind::Coalesce => f.write_str("??"),
232 TokenKind::DivInt => f.write_str("//"),
233 TokenKind::Pow => f.write_str("**"),
234 TokenKind::Annotate => f.write_str("@{"),
235
236 TokenKind::Param(id) => write!(f, "${id}"),
237
238 TokenKind::Range {
239 bind_left,
240 bind_right,
241 } => write!(
242 f,
243 "'{}..{}'",
244 if *bind_left { "" } else { " " },
245 if *bind_right { "" } else { " " }
246 ),
247 TokenKind::Interpolation(c, s) => {
248 write!(f, "{c}\"{}\"", s)
249 }
250 TokenKind::Comment(s) => {
251 writeln!(f, "#{}", s)
252 }
253 TokenKind::DocComment(s) => {
254 writeln!(f, "#!{}", s)
255 }
256 TokenKind::LineWrap(comments) => {
257 write!(f, "\n\\ ")?;
258 for comment in comments {
259 write!(f, "{}", comment)?;
260 }
261 Ok(())
262 }
263 TokenKind::Start => write!(f, "start of input"),
264 }
265 }
266}
267
268impl std::fmt::Debug for Token {
269 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
270 write!(f, "{}..{}: {:?}", self.span.start, self.span.end, self.kind)
271 }
272}
273
274#[cfg(test)]
275mod test {
276 use insta::assert_snapshot;
277
278 use super::*;
279
280 #[test]
281 fn test_string_quoting() {
282 fn make_str(s: &str) -> Literal {
283 Literal::String(s.to_string())
284 }
285
286 assert_snapshot!(
287 make_str("hello").to_string(),
288 @r#""hello""#
289 );
290
291 assert_snapshot!(
292 make_str(r#"he's nice"#).to_string(),
293 @r#""he's nice""#
294 );
295
296 assert_snapshot!(
297 make_str(r#"he said "what up""#).to_string(),
298 @r#"'he said "what up"'"#
299 );
300
301 assert_snapshot!(
302 make_str(r#"he said "what's up""#).to_string(),
303 @r#"'''he said "what's up"'''"#
304 );
305
306 assert_snapshot!(
307 make_str(r#" single' three double""" four double"""" "#).to_string(),
308 @r#"""""" single' three double""" four double"""" """"""#
309
310 );
311
312 assert_snapshot!(
313 make_str(r#""Starts with a double quote and ' contains a single quote"#).to_string(),
314 @r#"'''"Starts with a double quote and ' contains a single quote'''"#
315 );
316 }
317
318 #[test]
319 fn test_string_escapes() {
320 assert_snapshot!(
321 Literal::String(r#"hello\nworld"#.to_string()).to_string(),
322 @r#""hello\\nworld""#
323 );
324
325 assert_snapshot!(
326 Literal::String(r#"hello\tworld"#.to_string()).to_string(),
327 @r#""hello\\tworld""#
328 );
329
330 assert_snapshot!(
346 Literal::String(r#"hello
347 world"#.to_string()).to_string(),
348 @r#""hello\n world""#
349 );
350 }
351
352 #[test]
353 fn test_raw_string_quoting() {
354 fn make_str(s: &str) -> Literal {
356 Literal::RawString(s.to_string())
357 }
358
359 assert_snapshot!(
360 make_str("hello").to_string(),
361 @r#"r"hello""#
362 );
363 }
364}