1use crate::ast::{BinOp, Expr, Step};
8use crate::comment::CommentKind;
9use crate::lexer::Lx;
10use crate::value::Value;
11use logos::Logos;
12
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15#[error("{msg} (at offset {pos})")]
16pub struct ParseError {
17 pub msg: String,
18 pub pos: usize,
19}
20
21struct Tok {
22 kind: Lx,
23 text: String,
24 start: usize,
25}
26
27pub fn parse(src: &str) -> Result<Expr, ParseError> {
29 let toks = lex(src)?;
30 let mut p = Parser { toks, pos: 0 };
31 let e = match p.parse_program() {
32 Ok(e) => e,
33 Err(e) => return Err(with_hyphen_hint(e, src)),
34 };
35 if p.pos != p.toks.len() {
36 let t = &p.toks[p.pos];
37 return Err(with_hyphen_hint(
38 ParseError {
39 msg: format!("unexpected trailing token `{}`", t.text),
40 pos: t.start,
41 },
42 src,
43 ));
44 }
45 Ok(e)
46}
47
48fn with_hyphen_hint(err: ParseError, src: &str) -> ParseError {
62 if err.msg.contains("contains `-`") {
64 return err;
65 }
66 match hyphenated_key(src) {
67 Some((key, pos)) => ParseError {
69 msg: format!(
70 "key `{key}` contains `-` (parsed as subtraction); quote it: {}\"{key}\"",
71 &src[..pos]
72 ),
73 pos,
74 },
75 None => err,
76 }
77}
78
79pub fn hyphen_hint_for_unknown_function(src: &str, name: &str) -> Option<String> {
94 let (key, pos) = hyphenated_key(src)?;
95 if key.rsplit('-').next() != Some(name) {
96 return None;
97 }
98 Some(format!(
99 "key `{key}` contains `-` (parsed as subtraction); quote it: {}\"{key}\"",
100 &src[..pos]
101 ))
102}
103
104fn hyphenated_key(src: &str) -> Option<(String, usize)> {
110 let b = src.as_bytes();
111 let ident_start = |c: u8| c.is_ascii_alphabetic() || c == b'_';
112 let ident_char = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
113
114 let mut i = 0;
115 while i < b.len() {
116 if b[i] != b'.' || i + 1 >= b.len() || !ident_start(b[i + 1]) {
117 i += 1;
118 continue;
119 }
120 let start = i + 1;
121 let mut j = start;
122 let mut hyphenated = false;
123 while j < b.len() {
124 if ident_char(b[j]) {
125 j += 1;
126 } else if b[j] == b'-' && j + 1 < b.len() && ident_start(b[j + 1]) {
127 hyphenated = true;
128 j += 1;
129 } else {
130 break;
131 }
132 }
133 if hyphenated {
134 return Some((src[start..j].to_string(), start));
135 }
136 i = j;
137 }
138 None
139}
140
141fn lex(src: &str) -> Result<Vec<Tok>, ParseError> {
142 let mut lx = Lx::lexer(src);
143 let mut out = Vec::new();
144 while let Some(res) = lx.next() {
145 let span = lx.span();
146 match res {
147 Ok(kind) => out.push(Tok {
148 kind,
149 text: lx.slice().to_string(),
150 start: span.start,
151 }),
152 Err(_) => {
153 return Err(ParseError {
154 msg: format!("unexpected character `{}`", lx.slice()),
155 pos: span.start,
156 });
157 }
158 }
159 }
160 Ok(out)
161}
162
163struct Parser {
164 toks: Vec<Tok>,
165 pos: usize,
166}
167
168impl Parser {
169 fn peek(&self) -> Option<Lx> {
170 self.toks.get(self.pos).map(|t| t.kind)
171 }
172 fn text(&self) -> &str {
173 self.toks
174 .get(self.pos)
175 .map(|t| t.text.as_str())
176 .unwrap_or("")
177 }
178 fn at_end(&self) -> usize {
179 self.toks
180 .last()
181 .map(|t| t.start + t.text.len())
182 .unwrap_or(0)
183 }
184 fn err_here(&self, msg: impl Into<String>) -> ParseError {
185 let pos = self
186 .toks
187 .get(self.pos)
188 .map(|t| t.start)
189 .unwrap_or_else(|| self.at_end());
190 ParseError {
191 msg: msg.into(),
192 pos,
193 }
194 }
195 fn expect(&mut self, kind: Lx, what: &str) -> Result<(), ParseError> {
196 if self.peek() == Some(kind) {
197 self.pos += 1;
198 Ok(())
199 } else {
200 Err(self.err_here(format!("expected {what}")))
201 }
202 }
203
204 fn parse_program(&mut self) -> Result<Expr, ParseError> {
207 if self.peek() != Some(Lx::Caret) {
208 return self.parse_pipe();
209 }
210 self.pos += 1; let n = match self.peek() {
212 Some(Lx::Ident) => {
213 let digits = self.text().strip_prefix('d').ok_or_else(|| {
214 self.err_here("document selector is `^dN`, e.g. `^d0` (document 0)")
215 })?;
216 let n = digits.parse::<usize>().map_err(|_| {
217 self.err_here("`^dN` needs a document index, e.g. `^d0` (document 0)")
218 })?;
219 self.pos += 1;
220 n
221 }
222 _ => {
223 return Err(self.err_here("expected a document index after `^`, e.g. `^d0`"));
224 }
225 };
226 if self.peek() == Some(Lx::Pipe) {
229 self.pos += 1;
230 }
231 let body = if self.peek().is_none() {
232 Expr::Path(Vec::new())
233 } else {
234 self.parse_pipe()?
235 };
236 Ok(Expr::DocSelect(n, Box::new(body)))
237 }
238
239 fn parse_pipe(&mut self) -> Result<Expr, ParseError> {
240 let mut left = self.parse_comma()?;
241 while self.peek() == Some(Lx::Pipe) {
242 self.pos += 1;
243 let right = self.parse_comma()?;
244 left = Expr::Pipe(Box::new(left), Box::new(right));
245 }
246 Ok(left)
247 }
248
249 fn parse_comma(&mut self) -> Result<Expr, ParseError> {
250 let first = self.parse_assign()?;
251 if self.peek() != Some(Lx::Comma) {
252 return Ok(first);
253 }
254 let mut items = vec![first];
255 while self.peek() == Some(Lx::Comma) {
256 self.pos += 1;
257 items.push(self.parse_assign()?);
258 }
259 Ok(Expr::Comma(items))
260 }
261
262 fn parse_assign(&mut self) -> Result<Expr, ParseError> {
263 let lhs = match self.try_assign_target() {
264 Some(path) => path,
265 None => self.parse_alt()?,
266 };
267 match self.peek() {
268 Some(Lx::Assign) => {
269 self.reject_hyphen_key_lhs(&lhs)?;
270 self.pos += 1;
271 let rhs = self.parse_assign()?; Ok(Expr::Assign(Box::new(lhs), Box::new(rhs)))
273 }
274 Some(Lx::PipeAssign) => {
275 self.reject_hyphen_key_lhs(&lhs)?;
276 self.pos += 1;
277 let rhs = self.parse_assign()?;
278 Ok(Expr::UpdateAssign(Box::new(lhs), Box::new(rhs)))
279 }
280 Some(Lx::PlusAssign) => {
281 self.reject_hyphen_key_lhs(&lhs)?;
282 self.pos += 1;
283 let rhs = self.parse_assign()?;
284 Ok(Expr::AddAssign(Box::new(lhs), Box::new(rhs)))
285 }
286 _ => Ok(lhs),
287 }
288 }
289
290 fn try_assign_target(&mut self) -> Option<Expr> {
306 let save = self.pos;
307 let parsed = self.parse_hyphenated_path();
308 let is_target = parsed.is_some()
309 && matches!(
310 self.peek(),
311 Some(Lx::Assign | Lx::PipeAssign | Lx::PlusAssign)
312 );
313 if is_target {
314 return parsed;
315 }
316 self.pos = save;
317 None
318 }
319
320 fn parse_hyphenated_path(&mut self) -> Option<Expr> {
327 if self.peek() != Some(Lx::Dot) {
328 return None;
329 }
330 self.pos += 1;
331 let mut steps = Vec::new();
332 loop {
333 match self.peek() {
334 Some(Lx::Ident) => steps.push(Step::Field(self.take_hyphenated_ident())),
335 Some(Lx::Str) => {
336 steps.push(Step::Field(unescape(self.text())));
337 self.pos += 1;
338 }
339 Some(Lx::LBrack) => {
340 self.pos += 1;
341 if self.peek() == Some(Lx::Str) {
342 let key = unescape(self.text());
343 self.pos += 1;
344 if self.peek() != Some(Lx::RBrack) {
345 return None;
346 }
347 self.pos += 1;
348 steps.push(Step::Field(key));
349 } else {
350 let neg = self.peek() == Some(Lx::Minus);
351 if neg {
352 self.pos += 1;
353 }
354 if self.peek() != Some(Lx::Num) {
355 return None;
356 }
357 let n = parse_i64(self.text()).ok()?;
358 self.pos += 1;
359 if self.peek() != Some(Lx::RBrack) {
360 return None;
361 }
362 self.pos += 1;
363 steps.push(Step::Index(if neg { -n } else { n }));
364 }
365 }
366 _ => return None,
367 }
368 match self.peek() {
369 Some(Lx::Dot) => self.pos += 1,
370 Some(Lx::LBrack) => {}
371 _ => break,
372 }
373 }
374 Some(Expr::Path(steps))
375 }
376
377 fn take_hyphenated_ident(&mut self) -> String {
380 let mut name = self.text().to_string();
381 self.pos += 1;
382 while self.peek() == Some(Lx::Minus)
383 && self.abuts_previous(self.pos)
384 && self
385 .toks
386 .get(self.pos + 1)
387 .is_some_and(|t| t.kind == Lx::Ident)
388 && self.abuts_previous(self.pos + 1)
389 {
390 name.push('-');
391 name.push_str(&self.toks[self.pos + 1].text);
392 self.pos += 2;
393 }
394 name
395 }
396
397 fn abuts_previous(&self, i: usize) -> bool {
400 match (self.toks.get(i.wrapping_sub(1)), self.toks.get(i)) {
401 (Some(prev), Some(cur)) => prev.start + prev.text.len() == cur.start,
402 _ => false,
403 }
404 }
405
406 fn reject_hyphen_key_lhs(&self, lhs: &Expr) -> Result<(), ParseError> {
411 let Some((path, key)) = hyphen_key(lhs) else {
412 return Ok(());
413 };
414 Err(self.err_here(format!(
415 "key `{key}` contains `-` (parsed as subtraction); quote it: {path}"
416 )))
417 }
418
419 fn parse_alt(&mut self) -> Result<Expr, ParseError> {
422 let left = self.parse_cmp()?;
423 if self.peek() == Some(Lx::Alt) {
424 self.pos += 1;
425 let right = self.parse_alt()?;
426 return Ok(Expr::Alternative(Box::new(left), Box::new(right)));
427 }
428 Ok(left)
429 }
430
431 fn parse_cmp(&mut self) -> Result<Expr, ParseError> {
432 let left = self.parse_add()?;
433 let op = match self.peek() {
434 Some(Lx::EqEq) => BinOp::Eq,
435 Some(Lx::Ne) => BinOp::Ne,
436 Some(Lx::Lt) => BinOp::Lt,
437 Some(Lx::Gt) => BinOp::Gt,
438 Some(Lx::Le) => BinOp::Le,
439 Some(Lx::Ge) => BinOp::Ge,
440 _ => return Ok(left),
441 };
442 self.pos += 1;
443 let right = self.parse_add()?;
444 Ok(Expr::Binary(op, Box::new(left), Box::new(right)))
445 }
446
447 fn parse_add(&mut self) -> Result<Expr, ParseError> {
448 let mut left = self.parse_mul()?;
449 loop {
450 let op = match self.peek() {
451 Some(Lx::Plus) => BinOp::Add,
452 Some(Lx::Minus) => BinOp::Sub,
453 _ => break,
454 };
455 self.pos += 1;
456 let right = self.parse_mul()?;
457 left = Expr::Binary(op, Box::new(left), Box::new(right));
458 }
459 Ok(left)
460 }
461
462 fn parse_mul(&mut self) -> Result<Expr, ParseError> {
463 let mut left = self.parse_unary()?;
464 loop {
465 let op = match self.peek() {
466 Some(Lx::Star) => BinOp::Mul,
467 Some(Lx::Slash) => BinOp::Div,
468 Some(Lx::Percent) => BinOp::Mod,
469 _ => break,
470 };
471 self.pos += 1;
472 let right = self.parse_unary()?;
473 left = Expr::Binary(op, Box::new(left), Box::new(right));
474 }
475 Ok(left)
476 }
477
478 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
479 if self.peek() == Some(Lx::Minus) {
480 self.pos += 1;
481 return Ok(Expr::Neg(Box::new(self.parse_unary()?)));
482 }
483 self.parse_primary()
484 }
485
486 fn parse_primary(&mut self) -> Result<Expr, ParseError> {
487 match self.peek() {
488 Some(Lx::Dot) => self.parse_path(),
489 Some(Lx::LParen) => {
490 self.pos += 1;
491 let e = self.parse_pipe()?;
492 self.expect(Lx::RParen, "`)`")?;
493 Ok(e)
494 }
495 Some(Lx::LBrack) => {
496 self.pos += 1;
497 if self.peek() == Some(Lx::RBrack) {
498 self.pos += 1;
499 return Ok(Expr::Collect(None));
500 }
501 let inner = self.parse_pipe()?;
502 self.expect(Lx::RBrack, "`]`")?;
503 Ok(Expr::Collect(Some(Box::new(inner))))
504 }
505 Some(Lx::LBrace) => self.parse_object_construct(),
506 Some(Lx::Num) => {
507 let v = number_value(self.text());
508 self.pos += 1;
509 Ok(Expr::Literal(v))
510 }
511 Some(Lx::Str) => {
512 let v = Value::Str(unescape(self.text()));
513 self.pos += 1;
514 Ok(Expr::Literal(v))
515 }
516 Some(Lx::Ident) => self.parse_ident(),
517 _ => Err(self.err_here("expected an expression")),
518 }
519 }
520
521 fn parse_object_construct(&mut self) -> Result<Expr, ParseError> {
522 self.pos += 1; let mut pairs = Vec::new();
524 if self.peek() == Some(Lx::RBrace) {
525 self.pos += 1;
526 return Ok(Expr::ObjectConstruct(pairs));
527 }
528 loop {
529 let key = match self.peek() {
535 Some(Lx::Ident) => self.take_hyphenated_ident(),
536 Some(Lx::Str) => {
537 let key = unescape(self.text());
538 self.pos += 1;
539 key
540 }
541 _ => return Err(self.err_here("expected an object key")),
542 };
543 let value = match self.peek() {
553 Some(Lx::Colon) | Some(Lx::Assign) => {
554 self.pos += 1;
555 self.parse_cmp()?
557 }
558 Some(Lx::Comma) | Some(Lx::RBrace) => Expr::Path(vec![Step::Field(key.clone())]),
559 _ => return Err(self.err_here("expected `:`, `=`, `,` or `}`")),
560 };
561 pairs.push((key, value));
562 match self.peek() {
563 Some(Lx::Comma) => self.pos += 1,
564 Some(Lx::RBrace) => {
565 self.pos += 1;
566 break;
567 }
568 _ => return Err(self.err_here("expected `,` or `}`")),
569 }
570 }
571 Ok(Expr::ObjectConstruct(pairs))
572 }
573
574 fn parse_path(&mut self) -> Result<Expr, ParseError> {
575 self.pos += 1; let mut steps = Vec::new();
577 loop {
578 match self.peek() {
579 Some(Lx::Ident) => {
580 steps.push(Step::Field(self.text().to_string()));
581 self.pos += 1;
582 }
583 Some(Lx::Str) => {
584 steps.push(Step::Field(unescape(self.text())));
585 self.pos += 1;
586 }
587 Some(Lx::LBrack) => {
588 self.pos += 1;
589 if self.peek() == Some(Lx::RBrack) {
590 self.pos += 1;
591 steps.push(Step::Iterate);
592 } else if self.peek() == Some(Lx::Str) {
593 let key = unescape(self.text());
595 self.pos += 1;
596 self.expect(Lx::RBrack, "`]`")?;
597 steps.push(Step::Field(key));
598 } else {
599 let neg = self.peek() == Some(Lx::Minus);
600 if neg {
601 self.pos += 1;
602 }
603 if self.peek() != Some(Lx::Num) {
604 return Err(self.err_here("expected an array index or a string key"));
605 }
606 let n = parse_i64(self.text())
607 .map_err(|_| self.err_here("array index out of range"))?;
608 self.pos += 1;
609 self.expect(Lx::RBrack, "`]`")?;
610 steps.push(Step::Index(if neg { -n } else { n }));
611 }
612 }
613 Some(Lx::Hash) => {
614 self.pos += 1;
618 let mut kind = CommentKind::Head;
619 if self.peek() == Some(Lx::Dot)
620 && let Some(word) =
621 self.toks.get(self.pos + 1).filter(|t| t.kind == Lx::Ident)
622 && let Some(k) = comment_kind(&word.text)
623 {
624 self.pos += 2; kind = k;
626 }
627 steps.push(Step::Comment(kind));
628 break;
629 }
630 _ => break,
631 }
632 match self.peek() {
633 Some(Lx::Dot) => {
634 self.pos += 1;
635 continue;
636 }
637 Some(Lx::LBrack) => continue,
638 _ => break,
639 }
640 }
641 Ok(Expr::Path(steps))
642 }
643
644 fn parse_ident(&mut self) -> Result<Expr, ParseError> {
645 let name = self.text().to_string();
646 self.pos += 1;
647 match name.as_str() {
648 "true" => return Ok(Expr::Literal(Value::Bool(true))),
649 "false" => return Ok(Expr::Literal(Value::Bool(false))),
650 "null" => return Ok(Expr::Literal(Value::Null)),
651 _ => {}
652 }
653 if self.peek() == Some(Lx::LParen) {
654 self.pos += 1;
655 let mut args = vec![self.parse_pipe()?];
656 while self.peek() == Some(Lx::Semi) {
657 self.pos += 1;
658 args.push(self.parse_pipe()?);
659 }
660 self.expect(Lx::RParen, "`)`")?;
661 Ok(Expr::Call(name, args))
662 } else {
663 Ok(Expr::Call(name, Vec::new()))
664 }
665 }
666}
667
668fn hyphen_key(expr: &Expr) -> Option<(String, String)> {
676 let mut tail: Vec<&str> = Vec::new();
677 let mut cur = expr;
678 while let Expr::Binary(BinOp::Sub, l, r) = cur {
679 match r.as_ref() {
680 Expr::Call(name, args) if args.is_empty() => tail.push(name.as_str()),
681 _ => return None,
682 }
683 cur = l;
684 }
685 if tail.is_empty() {
686 return None;
687 }
688 let Expr::Path(steps) = cur else {
689 return None;
690 };
691 let Some((Step::Field(first), prefix)) = steps.split_last() else {
692 return None;
693 };
694 tail.push(first.as_str());
695 tail.reverse();
696 let key = tail.join("-");
697 let mut path = String::new();
698 for step in prefix {
699 match step {
700 Step::Field(f) if is_bare_key(f) => {
701 path.push('.');
702 path.push_str(f);
703 }
704 Step::Field(f) => {
705 path.push_str(&format!(".\"{f}\""));
706 }
707 Step::Index(i) => path.push_str(&format!("[{i}]")),
708 _ => return None,
709 }
710 }
711 path.push_str(&format!(".\"{key}\""));
712 Some((path, key))
713}
714
715fn is_bare_key(k: &str) -> bool {
717 let mut chars = k.chars();
718 chars
719 .next()
720 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
721 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
722}
723
724fn comment_kind(word: &str) -> Option<CommentKind> {
725 match word {
726 "head" => Some(CommentKind::Head),
727 "inline" => Some(CommentKind::Inline),
728 "foot" => Some(CommentKind::Foot),
729 _ => None,
730 }
731}
732
733fn number_value(t: &str) -> Value {
734 match t {
735 "Infinity" | "+Infinity" => Value::Float(f64::INFINITY),
737 "-Infinity" => Value::Float(f64::NEG_INFINITY),
738 "NaN" => Value::Float(f64::NAN),
739 _ => {
740 if t.contains(['.', 'e', 'E']) {
741 Value::Float(t.parse().unwrap_or(0.0))
742 } else {
743 match t.parse::<i64>() {
744 Ok(i) => Value::Int(i),
745 Err(_) => Value::Float(t.parse().unwrap_or(0.0)),
746 }
747 }
748 }
749 }
750}
751
752fn parse_i64(t: &str) -> Result<i64, std::num::ParseIntError> {
753 t.parse::<i64>()
754}
755
756fn unescape(tok: &str) -> String {
758 let inner = tok
759 .strip_prefix('"')
760 .and_then(|s| s.strip_suffix('"'))
761 .unwrap_or(tok);
762 let mut out = String::with_capacity(inner.len());
763 let mut chars = inner.chars();
764 while let Some(c) = chars.next() {
765 if c != '\\' {
766 out.push(c);
767 continue;
768 }
769 match chars.next() {
770 Some('"') => out.push('"'),
771 Some('\\') => out.push('\\'),
772 Some('/') => out.push('/'),
773 Some('n') => out.push('\n'),
774 Some('r') => out.push('\r'),
775 Some('t') => out.push('\t'),
776 Some('b') => out.push('\u{0008}'),
777 Some('f') => out.push('\u{000c}'),
778 Some('u') => {
779 let hex: String = chars.by_ref().take(4).collect();
780 if let Some(ch) = u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
781 out.push(ch);
782 }
783 }
784 Some(other) => {
785 out.push('\\');
786 out.push(other);
787 }
788 None => out.push('\\'),
789 }
790 }
791 out
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 fn p(s: &str) -> Expr {
799 parse(s).unwrap_or_else(|e| panic!("parse `{s}`: {e}"))
800 }
801
802 #[test]
803 fn identity() {
804 assert_eq!(p("."), Expr::Path(vec![]));
805 }
806
807 #[test]
808 fn doc_select_parses() {
809 assert_eq!(
811 p("^d0 | .kind"),
812 Expr::DocSelect(0, Box::new(Expr::Path(vec![Step::Field("kind".into())])))
813 );
814 assert_eq!(
815 p("^d2.spec"),
816 Expr::DocSelect(2, Box::new(Expr::Path(vec![Step::Field("spec".into())])))
817 );
818 assert_eq!(p("^d1"), Expr::DocSelect(1, Box::new(Expr::Path(vec![]))));
819 assert!(p("^d0 | .replicas = 3").is_mutation());
821 }
822
823 #[test]
824 fn doc_select_bad_index_errors() {
825 assert!(parse("^dfoo | .x").is_err());
826 assert!(parse("^x").is_err());
827 assert!(parse("^ | .x").is_err());
828 }
829
830 #[test]
831 fn dotted_path() {
832 assert_eq!(
833 p(".a.b"),
834 Expr::Path(vec![Step::Field("a".into()), Step::Field("b".into())])
835 );
836 }
837
838 #[test]
839 fn index_and_iterate() {
840 assert_eq!(
841 p(".arr[0][]"),
842 Expr::Path(vec![
843 Step::Field("arr".into()),
844 Step::Index(0),
845 Step::Iterate
846 ])
847 );
848 assert_eq!(
849 p(".x[-1]"),
850 Expr::Path(vec![Step::Field("x".into()), Step::Index(-1)])
851 );
852 }
853
854 #[test]
855 fn quoted_field() {
856 assert_eq!(
857 p(r#"."weird key""#),
858 Expr::Path(vec![Step::Field("weird key".into())])
859 );
860 }
861
862 #[test]
863 fn literals() {
864 assert_eq!(p("true"), Expr::Literal(Value::Bool(true)));
865 assert_eq!(p("null"), Expr::Literal(Value::Null));
866 assert_eq!(p("42"), Expr::Literal(Value::Int(42)));
867 assert_eq!(p("1.5"), Expr::Literal(Value::Float(1.5)));
868 assert_eq!(p(r#""hi""#), Expr::Literal(Value::Str("hi".into())));
869 }
870
871 #[test]
872 fn arithmetic_precedence() {
873 assert_eq!(
875 p("1 + 2 * 3"),
876 Expr::Binary(
877 BinOp::Add,
878 Box::new(Expr::Literal(Value::Int(1))),
879 Box::new(Expr::Binary(
880 BinOp::Mul,
881 Box::new(Expr::Literal(Value::Int(2))),
882 Box::new(Expr::Literal(Value::Int(3))),
883 )),
884 )
885 );
886 }
887
888 #[test]
889 fn pipe_and_select() {
890 let e = p(r#".items[] | select(.name == "x")"#);
891 match e {
892 Expr::Pipe(l, r) => {
893 assert_eq!(
894 *l,
895 Expr::Path(vec![Step::Field("items".into()), Step::Iterate])
896 );
897 match *r {
898 Expr::Call(ref name, ref args) => {
899 assert_eq!(name, "select");
900 assert_eq!(args.len(), 1);
901 }
902 _ => panic!("expected select call"),
903 }
904 }
905 _ => panic!("expected pipe"),
906 }
907 }
908
909 #[test]
910 fn errors() {
911 assert!(parse(".a.").is_ok()); assert!(parse("(").is_err());
913 assert!(parse(".a b").is_err()); assert!(parse("@").is_err()); }
916
917 #[test]
918 fn comment_accessor() {
919 use crate::comment::CommentKind;
920 assert_eq!(
922 p(".foo.#"),
923 Expr::Path(vec![
924 Step::Field("foo".into()),
925 Step::Comment(CommentKind::Head)
926 ])
927 );
928 assert_eq!(
930 p(".foo.#.inline"),
931 Expr::Path(vec![
932 Step::Field("foo".into()),
933 Step::Comment(CommentKind::Inline)
934 ])
935 );
936 assert_eq!(
937 p(".a.#.foot"),
938 Expr::Path(vec![
939 Step::Field("a".into()),
940 Step::Comment(CommentKind::Foot)
941 ])
942 );
943 assert_eq!(p(".#"), Expr::Path(vec![Step::Comment(CommentKind::Head)]));
945 assert_eq!(
947 p(".items[].#"),
948 Expr::Path(vec![
949 Step::Field("items".into()),
950 Step::Iterate,
951 Step::Comment(CommentKind::Head)
952 ])
953 );
954 assert!(parse(".foo.#.bar").is_err());
956 }
957
958 #[test]
959 fn object_construct_accepts_toml_equals() {
960 assert_eq!(
963 p(r#"{version = "1", optional: true}"#),
964 Expr::ObjectConstruct(vec![
965 ("version".into(), Expr::Literal(Value::Str("1".into()))),
966 ("optional".into(), Expr::Literal(Value::Bool(true))),
967 ])
968 );
969 }
970
971 #[test]
972 fn object_construct_names_both_separators() {
973 let e = parse("{a 1}").unwrap_err();
974 assert!(
975 e.to_string().contains("expected `:`, `=`, `,` or `}`"),
976 "got: {e}"
977 );
978 }
979
980 #[test]
981 fn object_construct_plucks_bare_keys() {
982 let pluck = |k: &str| Expr::Path(vec![Step::Field(k.into())]);
984 assert_eq!(
985 p("{MemoryMiB}"),
986 Expr::ObjectConstruct(vec![("MemoryMiB".into(), pluck("MemoryMiB"))])
987 );
988 assert_eq!(
989 p("{MemoryMiB, UseGrpcfuse}"),
990 Expr::ObjectConstruct(vec![
991 ("MemoryMiB".into(), pluck("MemoryMiB")),
992 ("UseGrpcfuse".into(), pluck("UseGrpcfuse")),
993 ])
994 );
995 assert_eq!(
998 p(r#"{"a.b", c: 1}"#),
999 Expr::ObjectConstruct(vec![
1000 ("a.b".into(), pluck("a.b")),
1001 ("c".into(), Expr::Literal(Value::Int(1))),
1002 ])
1003 );
1004 assert_eq!(
1007 p("{default-features}"),
1008 Expr::ObjectConstruct(vec![("default-features".into(), pluck("default-features"))])
1009 );
1010 }
1011
1012 #[test]
1013 fn hyphen_hint_leaves_real_subtraction_alone() {
1014 assert_eq!(
1016 p(".a-b"),
1017 Expr::Binary(
1018 BinOp::Sub,
1019 Box::new(Expr::Path(vec![Step::Field("a".into())])),
1020 Box::new(Expr::Call("b".into(), vec![]))
1021 )
1022 );
1023 assert!(parse(".a - 1 = 2").is_ok());
1027 p(".x = .a - .b");
1029 }
1030
1031 #[test]
1034 fn a_hyphenated_key_assigns_without_quoting() {
1035 let e = p(r#".dev-dependencies.serde_json = "1.0""#);
1036 let Expr::Assign(lhs, _) = e else {
1037 panic!("expected an assignment")
1038 };
1039 assert_eq!(
1040 *lhs,
1041 Expr::Path(vec![
1042 Step::Field("dev-dependencies".into()),
1043 Step::Field("serde_json".into()),
1044 ])
1045 );
1046 }
1047
1048 #[test]
1049 fn hyphenated_keys_work_for_every_assignment_operator() {
1050 for src in [
1051 r#".package.rust-version = "1.85""#,
1052 ".lib.crate-type |= 1",
1053 ".a.b-c += 1",
1054 ".rust-version = 1",
1055 ] {
1056 p(src);
1057 }
1058 }
1059
1060 #[test]
1061 fn a_multi_hyphen_key_is_one_field() {
1062 let Expr::Assign(lhs, _) = p(".lib.crate-type-x = 1") else {
1063 panic!("expected an assignment")
1064 };
1065 assert_eq!(
1066 *lhs,
1067 Expr::Path(vec![
1068 Step::Field("lib".into()),
1069 Step::Field("crate-type-x".into()),
1070 ])
1071 );
1072 }
1073
1074 #[test]
1076 fn a_hyphenated_path_still_takes_indices_and_quoted_keys() {
1077 p(r#".bin[0].required-features = ["x"]"#);
1078 p(r#".target."cfg(unix)".dev-dependencies.libc = "0.2""#);
1079 p(r#".a.b-c[-1] = 1"#);
1080 }
1081
1082 #[test]
1085 fn a_spaced_hyphen_is_not_absorbed_into_a_key() {
1086 assert!(parse(".a - b = 1").is_err());
1087 }
1088
1089 #[test]
1091 fn queries_are_untouched_by_the_assignment_rule() {
1092 p(".total - length");
1093 p(".a - .b");
1094 p(".count-1");
1095 p(".a.b.c");
1096 }
1097
1098 #[test]
1102 fn a_hyphenated_object_key_needs_no_quoting() {
1103 let e = p(r#"{version: "0.13", default-features: false}"#);
1104 let Expr::ObjectConstruct(pairs) = e else {
1105 panic!("expected an object")
1106 };
1107 let keys: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
1108 assert_eq!(keys, vec!["version", "default-features"]);
1109 }
1110
1111 #[test]
1114 fn hyphenated_object_keys_work_with_the_equals_spelling() {
1115 let Expr::ObjectConstruct(pairs) = p(r#"{default-features = false}"#) else {
1116 panic!("expected an object")
1117 };
1118 assert_eq!(pairs[0].0, "default-features");
1119 }
1120
1121 #[test]
1122 fn a_multi_hyphen_object_key_is_one_name() {
1123 let Expr::ObjectConstruct(pairs) = p("{a-b-c: 1}") else {
1124 panic!("expected an object")
1125 };
1126 assert_eq!(pairs[0].0, "a-b-c");
1127 }
1128
1129 #[test]
1130 fn quoted_object_keys_still_work() {
1131 let Expr::ObjectConstruct(pairs) = p(r#"{"default-features": false, plain: 1}"#) else {
1132 panic!("expected an object")
1133 };
1134 assert_eq!(pairs[0].0, "default-features");
1135 assert_eq!(pairs[1].0, "plain");
1136 }
1137
1138 #[test]
1140 fn an_object_value_can_still_subtract() {
1141 p("{n: .total - length}");
1142 p("{n: .count-1}");
1143 }
1144
1145 #[test]
1148 fn a_real_cargo_dependency_table_parses() {
1149 p(
1150 r#".dependencies.pulldown-cmark = {version: "0.13", default-features: false, features: ["html"]}"#,
1151 );
1152 }
1153
1154 #[test]
1157 fn a_hyphenated_key_in_a_query_still_explains_itself() {
1158 let e = parse(".dev-dependencies.serde_json").unwrap_err();
1159 assert!(e.to_string().contains("contains `-`"), "{e}");
1160 assert!(e.to_string().contains(r#"."dev-dependencies""#), "{e}");
1161 }
1162
1163 #[test]
1164 fn the_scanner_ignores_what_is_not_a_hyphenated_key() {
1165 assert!(hyphenated_key(".total - length").is_none());
1166 assert!(hyphenated_key(".count-1").is_none());
1167 assert!(hyphenated_key(".a.b.c").is_none());
1168 assert!(hyphenated_key("-").is_none());
1169 assert!(hyphenated_key("").is_none());
1170 }
1171
1172 #[test]
1173 fn the_scanner_finds_the_key_and_its_offset() {
1174 assert_eq!(
1175 hyphenated_key(".dev-dependencies.x"),
1176 Some(("dev-dependencies".to_string(), 1))
1177 );
1178 assert_eq!(
1179 hyphenated_key(".a-b-c"),
1180 Some(("a-b-c".to_string(), 1)),
1181 "multiple hyphens are one key"
1182 );
1183 }
1184
1185 #[test]
1188 fn an_unrelated_error_keeps_its_own_message() {
1189 let e = parse(".a | ").unwrap_err();
1190 assert!(!e.msg.contains("contains `-`"), "{}", e.msg);
1191 }
1192}