Skip to main content

apollo_federation/connectors/json_selection/
parser.rs

1use std::fmt::Display;
2use std::hash::Hash;
3use std::str::FromStr;
4
5use apollo_compiler::collections::IndexSet;
6use itertools::Itertools;
7use nom::IResult;
8use nom::Input;
9use nom::Parser;
10use nom::branch::alt;
11use nom::character::complete::char;
12use nom::character::complete::one_of;
13use nom::combinator::all_consuming;
14use nom::combinator::map;
15use nom::combinator::opt;
16use nom::combinator::recognize;
17use nom::error::ParseError;
18use nom::multi::many0;
19use nom::sequence::pair;
20use nom::sequence::preceded;
21use nom::sequence::terminated;
22use serde_json_bytes::Value as JSON;
23
24use super::helpers::spaces_or_comments;
25use super::helpers::vec_push;
26use super::known_var::KnownVariable;
27use super::lit_expr::LitExpr;
28use super::location::OffsetRange;
29use super::location::Ranged;
30use super::location::Span;
31use super::location::SpanExtra;
32use super::location::WithRange;
33use super::location::merge_ranges;
34use super::location::new_span_with_spec;
35use super::location::ranged_span;
36use crate::connectors::ConnectSpec;
37use crate::connectors::Namespace;
38use crate::connectors::json_selection::location::get_connect_spec;
39use crate::connectors::json_selection::methods::ArrowMethod;
40use crate::connectors::variable::VariableNamespace;
41use crate::connectors::variable::VariableReference;
42
43// ParseResult is the internal type returned by most ::parse methods, as it is
44// convenient to use with nom's combinators. The top-level JSONSelection::parse
45// method returns a slightly different IResult type that hides implementation
46// details of the nom-specific types.
47//
48// TODO Consider switching the third IResult type parameter to VerboseError
49// here, if error messages can be improved with additional context.
50pub(super) type ParseResult<'a, T> = IResult<Span<'a>, T>;
51
52// Generates a non-fatal error with the given suffix and message, allowing the
53// parser to recover and continue.
54pub(super) fn nom_error_message(
55    suffix: Span,
56    // This message type forbids computing error messages with format!, which
57    // might be worthwhile in the future. For now, it's convenient to avoid
58    // String messages so the Span type can remain Copy, so we don't have to
59    // clone spans frequently in the parsing code. In most cases, the suffix
60    // provides the dynamic context needed to interpret the static message.
61    message: impl Into<String>,
62) -> nom::Err<nom::error::Error<Span>> {
63    let offset = suffix.location_offset();
64    nom::Err::Error(nom::error::Error::from_error_kind(
65        suffix.map_extra(|extra| SpanExtra {
66            errors: vec_push(extra.errors, (message.into(), offset)),
67            ..extra
68        }),
69        nom::error::ErrorKind::IsNot,
70    ))
71}
72
73// Generates a fatal error with the given suffix Span and message, causing the
74// parser to abort with the given error message, which is useful after
75// recognizing syntax that completely constrains what follows (like the -> token
76// before a method name), and what follows does not parse as required.
77pub(super) fn nom_fail_message(
78    suffix: Span,
79    message: impl Into<String>,
80) -> nom::Err<nom::error::Error<Span>> {
81    let offset = suffix.location_offset();
82    nom::Err::Failure(nom::error::Error::from_error_kind(
83        suffix.map_extra(|extra| SpanExtra {
84            errors: vec_push(extra.errors, (message.into(), offset)),
85            ..extra
86        }),
87        nom::error::ErrorKind::IsNot,
88    ))
89}
90
91pub(crate) trait VarPaths {
92    /// Implementers of `VarPaths` must implement this `var_paths` method, which
93    /// should return all variable-referencing paths where the variable is a
94    /// `KnownVariable::External(String)` or `KnownVariable::Local(String)`
95    /// (that is, not internal variable references like `$` or `@`).
96    fn var_paths(&self) -> Vec<&PathSelection>;
97
98    fn external_var_paths(&self) -> Vec<&PathSelection> {
99        self.var_paths()
100            .into_iter()
101            .filter(|var_path| {
102                if let PathList::Var(known_var, _) = var_path.path.as_ref() {
103                    matches!(known_var.as_ref(), KnownVariable::External(_))
104                } else {
105                    false
106                }
107            })
108            .collect()
109    }
110
111    /// Returns all locally bound variable names in the selection, without
112    /// regard for which ones are available where.
113    fn local_var_names(&self) -> IndexSet<String> {
114        self.var_paths()
115            .into_iter()
116            .flat_map(|var_path| {
117                if let PathList::Var(known_var, _) = var_path.path.as_ref() {
118                    match known_var.as_ref() {
119                        KnownVariable::Local(var_name) => Some(var_name.to_string()),
120                        _ => None,
121                    }
122                } else {
123                    None
124                }
125            })
126            .collect()
127    }
128}
129
130// JSONSelection     ::= PathSelection | NakedSubSelection
131// NakedSubSelection ::= NamedSelection* StarSelection?
132
133#[derive(Debug, PartialEq, Eq, Clone)]
134pub struct JSONSelection {
135    pub(super) inner: TopLevelSelection,
136    pub spec: ConnectSpec,
137}
138
139/// Non-lossy top-level classification of a [`JSONSelection`]. A parse
140/// produces either a multi-entry `NamedSelectionList` ([`Self::Named`]) or
141/// a single [`LitExpr`] value ([`Self::Value`]). The [`LitExpr::Path`]
142/// variant covers the path-shaped case (e.g. `$args.foo.bar`); callers
143/// that care about paths specifically match on
144/// `TopLevelSelection::Value(lit)` and then on `lit.as_ref()` for
145/// `LitExpr::Path(_)`.
146#[derive(Debug, PartialEq, Eq, Clone)]
147pub(crate) enum TopLevelSelection {
148    /// One or more [`NamedSelection`] items at the root (e.g. `id name`).
149    ///
150    /// Although we reuse the [`SubSelection`] type here, we parse it as a
151    /// sequence of `NamedSelection` items without the `{...}` curly braces
152    /// that [`SubSelection::parse`] expects.
153    Named(SubSelection),
154    /// A single [`LitExpr`] at the root — bare path, bare literal, `LitPath`
155    /// method chain on a literal, object literal with trailing key access,
156    /// array, `OpChain`, etc. A `PathList::Expr` node appears here only
157    /// when the source contained an explicit `$(...)` wrapper (`ExprPath`).
158    /// Nothing in this module synthesizes `Expr` nodes from unwrapped input.
159    Value(WithRange<LitExpr>),
160}
161
162// To keep JSONSelection::parse consumers from depending on details of the nom
163// error types, JSONSelection::parse reports this custom error type. Other
164// ::parse methods still internally report nom::error::Error for the most part.
165#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
166#[error("{message}: {fragment}")]
167pub struct JSONSelectionParseError {
168    // The message will be a meaningful error message in many cases, but may
169    // fall back to a formatted nom::error::ErrorKind in some cases, e.g. when
170    // an alt(...) runs out of options and we can't determine which underlying
171    // error was "most" responsible.
172    pub message: String,
173
174    // Since we are not exposing the nom_locate-specific Span type, we report
175    // span.fragment() and span.location_offset() here.
176    pub fragment: String,
177
178    // While it might be nice to report a range rather than just an offset, not
179    // all parsing errors have an unambiguous end offset, so the best we can do
180    // is point to the suffix of the input that failed to parse (which
181    // corresponds to where the fragment starts).
182    pub offset: usize,
183
184    // The ConnectSpec version used to parse and apply the selection.
185    pub spec: ConnectSpec,
186}
187
188impl JSONSelection {
189    pub fn spec(&self) -> ConnectSpec {
190        self.spec
191    }
192
193    pub fn named(sub: SubSelection) -> Self {
194        Self {
195            inner: TopLevelSelection::Named(sub),
196            spec: ConnectSpec::latest(),
197        }
198    }
199
200    pub fn path(path: PathSelection) -> Self {
201        let range = path.range();
202        Self {
203            inner: TopLevelSelection::Value(WithRange::new(LitExpr::Path(path), range)),
204            spec: ConnectSpec::latest(),
205        }
206    }
207
208    /// Borrow the top-level structure of the selection. A parsed
209    /// [`JSONSelection`] is either a multi-entry [`TopLevelSelection::Named`]
210    /// list or a single [`TopLevelSelection::Value`] (itself covering bare
211    /// paths, primitives, object literals, arrays, `LitPath` method chains,
212    /// and `OpChain`). Callers that care about the path-shaped case match
213    /// the `Value` arm and then inspect the inner `LitExpr` for `Path`.
214    pub(crate) fn top_level(&self) -> &TopLevelSelection {
215        &self.inner
216    }
217
218    pub fn empty() -> Self {
219        Self {
220            inner: TopLevelSelection::Named(SubSelection::default()),
221            spec: ConnectSpec::latest(),
222        }
223    }
224
225    pub fn is_empty(&self) -> bool {
226        match &self.inner {
227            TopLevelSelection::Named(subselect) => subselect.selections.is_empty(),
228            TopLevelSelection::Value(lit) => match lit.as_ref() {
229                LitExpr::Path(path) => *path.path == PathList::Empty,
230                // A non-Path top-level `LitExpr` always carries content
231                // (even `null` is a valid non-empty selection), so nothing
232                // else is considered empty.
233                _ => false,
234            },
235        }
236    }
237
238    // JSONSelection::parse is possibly the "most public" method in the entire
239    // file, so it's important that the method signature can remain stable even
240    // if we drastically change implementation details. That's why we use &str
241    // as the input type and a custom JSONSelectionParseError type as the error
242    // type, rather than using Span or nom::error::Error directly.
243    pub fn parse(input: &str) -> Result<Self, JSONSelectionParseError> {
244        JSONSelection::parse_with_spec(input, ConnectSpec::latest())
245    }
246
247    pub fn parse_with_spec(
248        input: &str,
249        spec: ConnectSpec,
250    ) -> Result<Self, JSONSelectionParseError> {
251        let span = new_span_with_spec(input, spec);
252
253        match JSONSelection::parse_span(span) {
254            Ok((remainder, selection)) => {
255                let fragment = remainder.fragment();
256                let produced_errors = !remainder.extra.errors.is_empty();
257                if fragment.is_empty() && !produced_errors {
258                    Ok(selection)
259                } else {
260                    let mut message = remainder
261                        .extra
262                        .errors
263                        .iter()
264                        .map(|(msg, _offset)| msg.as_str())
265                        .collect::<Vec<_>>()
266                        .join("\n");
267
268                    // Use offset and fragment from first error if available
269                    let (error_offset, error_fragment) =
270                        if let Some((_, first_error_offset)) = remainder.extra.errors.first() {
271                            let error_span =
272                                new_span_with_spec(input, spec).take_from(*first_error_offset);
273                            (
274                                error_span.location_offset(),
275                                error_span.fragment().to_string(),
276                            )
277                        } else {
278                            (remainder.location_offset(), fragment.to_string())
279                        };
280
281                    if !fragment.is_empty() {
282                        message
283                            .push_str(&format!("\nUnexpected trailing characters: {}", fragment));
284                    }
285                    Err(JSONSelectionParseError {
286                        message,
287                        fragment: error_fragment,
288                        offset: error_offset,
289                        spec: remainder.extra.spec,
290                    })
291                }
292            }
293
294            Err(e) => match e {
295                nom::Err::Error(e) | nom::Err::Failure(e) => Err(JSONSelectionParseError {
296                    message: if e.input.extra.errors.is_empty() {
297                        format!("nom::error::ErrorKind::{:?}", e.code)
298                    } else {
299                        e.input
300                            .extra
301                            .errors
302                            .iter()
303                            .map(|(msg, _offset)| msg.clone())
304                            .join("\n")
305                    },
306                    fragment: e.input.fragment().to_string(),
307                    offset: e.input.location_offset(),
308                    spec: e.input.extra.spec,
309                }),
310
311                nom::Err::Incomplete(_) => unreachable!("nom::Err::Incomplete not expected here"),
312            },
313        }
314    }
315
316    fn parse_span(input: Span) -> ParseResult<Self> {
317        match get_connect_spec(&input) {
318            ConnectSpec::V0_1 | ConnectSpec::V0_2 => Self::parse_span_v0_2(input),
319            ConnectSpec::V0_3 => Self::parse_span_v0_3(input),
320            ConnectSpec::V0_4 => Self::parse_span_v0_4(input),
321        }
322    }
323
324    fn parse_span_v0_2(input: Span) -> ParseResult<Self> {
325        let spec = get_connect_spec(&input);
326
327        match alt((
328            all_consuming(terminated(
329                map(PathSelection::parse, |path| {
330                    let range = path.range();
331                    Self {
332                        inner: TopLevelSelection::Value(WithRange::new(LitExpr::Path(path), range)),
333                        spec,
334                    }
335                }),
336                // By convention, most ::parse methods do not consume trailing
337                // spaces_or_comments, so we need to consume them here in order
338                // to satisfy the all_consuming requirement.
339                spaces_or_comments,
340            )),
341            all_consuming(terminated(
342                map(SubSelection::parse_naked, |sub| Self {
343                    inner: TopLevelSelection::Named(sub),
344                    spec,
345                }),
346                // It's tempting to hoist the all_consuming(terminated(...))
347                // checks outside the alt((...)) so we only need to handle
348                // trailing spaces_or_comments once, but that won't work because
349                // the Self::Path case should fail when a single PathSelection
350                // cannot be parsed, and that failure typically happens because
351                // the PathSelection::parse method does not consume the entire
352                // input, which is caught by the first all_consuming above.
353                spaces_or_comments,
354            )),
355        ))
356        .parse(input)
357        {
358            Ok((remainder, selection)) => {
359                if remainder.fragment().is_empty() {
360                    Ok((remainder, selection))
361                } else {
362                    Err(nom_fail_message(
363                        // Usually our nom errors report the original input that
364                        // failed to parse, but that's not helpful here, since
365                        // input corresponds to the entire string, whereas this
366                        // error message is reporting junk at the end of the
367                        // string that should not be there.
368                        remainder,
369                        "Unexpected trailing characters",
370                    ))
371                }
372            }
373            Err(e) => Err(e),
374        }
375    }
376
377    fn parse_span_v0_3(input: Span) -> ParseResult<Self> {
378        let spec = get_connect_spec(&input);
379
380        match all_consuming(terminated(
381            map(SubSelection::parse_naked, |sub| {
382                if let (1, Some(only)) = (sub.selections.len(), sub.selections.first()) {
383                    // SubSelection::parse_naked already enforces that there
384                    // cannot be more than one NamedSelection if that
385                    // NamedSelection is anonymous, and here's where we divert
386                    // that case into TopLevelSelection::Path rather than
387                    // TopLevelSelection::Named for easier processing later.
388                    //
389                    // The SubSelection may contain multiple inlined selections
390                    // with NamingPrefix::Spread(None) (that is, an anonymous
391                    // path with a trailing SubSelection), which are not
392                    // considered anonymous in that context (because they may
393                    // have zero or more output properties, which they spread
394                    // into the larger result). However, if there is only one
395                    // such ::Spread(None) selection in sub, then "spreading"
396                    // its value into the larger SubSelection is equivalent to
397                    // using its value as the entire output, so we can treat the
398                    // whole thing as a TopLevelSelection::Path selection.
399                    //
400                    // Putting ... first causes NamingPrefix::Spread(Some(_)) to
401                    // be used instead, so the whole selection remains a
402                    // TopLevelSelection::Named, with the additional restriction
403                    // that the argument of the ... must be an object or null
404                    // (not an array). Eventually, we should deprecate spread
405                    // selections without ..., and this complexity will go away.
406                    if only.is_anonymous() || matches!(only.prefix, NamingPrefix::Spread(None)) {
407                        return Self {
408                            inner: TopLevelSelection::Value(only.path.clone()),
409                            spec,
410                        };
411                    }
412                }
413                Self {
414                    inner: TopLevelSelection::Named(sub),
415                    spec,
416                }
417            }),
418            // Most ::parse methods do not consume trailing spaces_or_comments,
419            // but here (at the top level) we need to make sure anything left at
420            // the end of the string is inconsequential, in order to satisfy the
421            // all_consuming combinator above.
422            spaces_or_comments,
423        ))
424        .parse(input)
425        {
426            Ok((remainder, selection)) => {
427                if remainder.fragment().is_empty() {
428                    Ok((remainder, selection))
429                } else {
430                    Err(nom_fail_message(
431                        // Usually our nom errors report the original input that
432                        // failed to parse, but that's not helpful here, since
433                        // input corresponds to the entire string, whereas this
434                        // error message is reporting junk at the end of the
435                        // string that should not be there.
436                        remainder,
437                        "Unexpected trailing characters",
438                    ))
439                }
440            }
441            Err(e) => Err(e),
442        }
443    }
444
445    // connect/v0.4 simplifies the top-level grammar to
446    //
447    //     JSONSelection ::= LitExpr | NamedSelectionList
448    //
449    // where
450    //
451    //     LitExpr ::= LitOpChain | LitPath | LitPrimitive | LitObject | LitArray | PathSelection
452    //
453    // A single LitExpr covers every literal-headed and path-headed shape
454    // (including `{…}`, `[…]`, `123->add(1)`, `$var.foo`, and method chains
455    // rooted on any of those), leaving NamedSelectionList to cover only the
456    // multi-entry naked forms like `id name` that a single expression cannot
457    // represent. The result is stored in `TopLevelSelection::Value(...)`
458    // exactly as `LitExpr::parse` produced it — no synthetic `PathList::Expr`
459    // wrapping; `PathList::Expr` appears only from a real `$(...)` in source.
460    fn parse_span_v0_4(input: Span) -> ParseResult<Self> {
461        let spec = get_connect_spec(&input);
462
463        fn lit_expr_top_level(input: Span) -> ParseResult<JSONSelection> {
464            let spec = get_connect_spec(&input);
465            let (input, _) = spaces_or_comments(input)?;
466            let (remainder, lit) = LitExpr::parse(input)?;
467            // Backtrack (non-fatal error) if the LitExpr did not consume the
468            // rest of the input (modulo trailing whitespace/comments), so
469            // `alt` can fall through to `naked_named` for multi-entry naked
470            // selections like `id name` that no single LitExpr can represent.
471            let (after_ws, _) = spaces_or_comments(remainder.clone())?;
472            if !after_ws.fragment().is_empty() {
473                return Err(nom::Err::Error(nom::error::Error::from_error_kind(
474                    remainder,
475                    nom::error::ErrorKind::Verify,
476                )));
477            }
478
479            // A naked single-key PathSelection like `a` or `a { b }` is
480            // ambiguous: it could mean "return `a`'s value" (flat) or "return
481            // an object whose `a` key holds `a`'s value" (Named-wrapped). The
482            // historical behavior is the latter, handled by `naked_named`, so
483            // fall through here to let that arm claim single-key paths.
484            if let LitExpr::Path(path) = lit.as_ref()
485                && path.is_single_key()
486            {
487                return Err(nom::Err::Error(nom::error::Error::from_error_kind(
488                    remainder,
489                    nom::error::ErrorKind::Verify,
490                )));
491            }
492
493            // Store the LitExpr verbatim. A source `$(…)` parses into
494            // `LitExpr::Path(PathSelection { Expr(…, …) })`; a bare literal,
495            // `LitPath`, array, operator chain, or explicit `{…}` (which is
496            // a `LitExpr::Object`) all land honestly as their own `LitExpr`
497            // variants with no wrapper synthesized around them.
498            Ok((
499                remainder,
500                JSONSelection {
501                    inner: TopLevelSelection::Value(lit),
502                    spec,
503                },
504            ))
505        }
506
507        let naked_named = map(SubSelection::parse_naked, |sub| {
508            if let (1, Some(only)) = (sub.selections.len(), sub.selections.first())
509                && (only.is_anonymous() || matches!(only.prefix, NamingPrefix::Spread(None)))
510            {
511                return Self {
512                    inner: TopLevelSelection::Value(only.path.clone()),
513                    spec,
514                };
515            }
516            Self {
517                inner: TopLevelSelection::Named(sub),
518                spec,
519            }
520        });
521
522        match all_consuming(terminated(
523            alt((lit_expr_top_level, naked_named)),
524            spaces_or_comments,
525        ))
526        .parse(input)
527        {
528            Ok((remainder, selection)) => {
529                if remainder.fragment().is_empty() {
530                    Ok((remainder, selection))
531                } else {
532                    Err(nom_fail_message(
533                        remainder,
534                        "Unexpected trailing characters",
535                    ))
536                }
537            }
538            Err(e) => Err(e),
539        }
540    }
541
542    pub(crate) fn next_subselection(&self) -> Option<&SubSelection> {
543        match &self.inner {
544            TopLevelSelection::Named(subselect) => Some(subselect),
545            TopLevelSelection::Value(lit) => lit.as_ref().next_subselection(),
546        }
547    }
548
549    #[allow(unused)]
550    pub(crate) fn next_mut_subselection(&mut self) -> Option<&mut SubSelection> {
551        match &mut self.inner {
552            TopLevelSelection::Named(subselect) => Some(subselect),
553            TopLevelSelection::Value(lit) => lit.as_mut().next_mut_subselection(),
554        }
555    }
556
557    pub fn variable_references(&self) -> impl Iterator<Item = VariableReference<Namespace>> + '_ {
558        self.external_var_paths()
559            .into_iter()
560            .flat_map(|var_path| var_path.variable_reference())
561    }
562}
563
564impl VarPaths for JSONSelection {
565    fn var_paths(&self) -> Vec<&PathSelection> {
566        match &self.inner {
567            TopLevelSelection::Named(subselect) => subselect.var_paths(),
568            TopLevelSelection::Value(lit) => lit.as_ref().var_paths(),
569        }
570    }
571}
572
573// NamedSelection       ::= (Alias | "...")? PathSelection | Alias SubSelection
574// PathSelection        ::= Path SubSelection?
575
576#[derive(Debug, PartialEq, Eq, Clone)]
577pub struct NamedSelection {
578    pub(super) prefix: NamingPrefix,
579    // The RHS of a `NamedSelection` is any `LitExpr`. A `PathList::Expr`
580    // node appears here only if the source contained an explicit `$(...)`
581    // wrapper — bare literal values like `alias: "Book"` are stored as
582    // their own `LitExpr` variant (e.g. `LitExpr::String`) without any
583    // synthetic wrapping. The field is kept named `path` for continuity
584    // with call sites that historically worked with a `PathSelection`,
585    // but it now accepts the full `LitExpr` surface.
586    pub(super) path: WithRange<LitExpr>,
587}
588
589#[derive(Debug, PartialEq, Eq, Clone)]
590pub(super) enum NamingPrefix {
591    // When a NamedSelection has an Alias, it fully determines the output key,
592    // and any applied values from the path will be assigned to that key.
593    Alias(Alias),
594    // A path can be spread without an explicit ... token, provided it has a
595    // trailing SubSelection (guaranteeing it outputs a static set of object
596    // properties). In those cases, the OffsetRange will be None. When there is
597    // an actual ... token, the OffsetRange will be Some(token_range).
598    Spread(OffsetRange),
599    // When there is no Alias or ... spread token, and the path is not inlined
600    // implicitly due to a trailing SubSelection (which would be represented by
601    // ::Spread(None)), the NamingPrefix is ::None. The NamedSelection may still
602    // produce a single output key if self.path.get_single_key() returns
603    // Some(key), but otherwise it's an anonymous path, which produces only a
604    // JSON value. Singular anonymous paths are allowed at the top level, where
605    // any value they produce directly determines the output of the selection,
606    // but anonymous NamedSelections cannot be mixed together with other
607    // NamedSelections that produce names (in a SubSelection or anywhere else).
608    None,
609}
610
611// Like PathSelection, NamedSelection is an AST structure that takes its range
612// entirely from its children, so NamedSelection itself does not need to provide
613// separate storage for its own range, and therefore does not need to be wrapped
614// as WithRange<NamedSelection>, but merely needs to implement the Ranged trait.
615impl Ranged for NamedSelection {
616    fn range(&self) -> OffsetRange {
617        let alias_or_spread_range = match &self.prefix {
618            NamingPrefix::None => None,
619            NamingPrefix::Alias(alias) => alias.range(),
620            NamingPrefix::Spread(range) => range.clone(),
621        };
622        merge_ranges(alias_or_spread_range, self.path.range())
623    }
624}
625
626// Lift a `PathSelection` into the `WithRange<LitExpr>` shape that
627// `NamedSelection.path` now uses. Used at call sites that historically
628// produced a `PathSelection` directly.
629fn path_value(path: PathSelection) -> WithRange<LitExpr> {
630    let range = path.range();
631    WithRange::new(LitExpr::Path(path), range)
632}
633
634impl NamedSelection {
635    pub(super) fn has_single_output_key(&self) -> bool {
636        self.get_single_key().is_some()
637    }
638
639    pub(super) fn get_single_key(&self) -> Option<&WithRange<Key>> {
640        match &self.prefix {
641            NamingPrefix::None => match self.path.as_ref() {
642                LitExpr::Path(path) => path.get_single_key(),
643                // Without a prefix, a bare non-path LitExpr is anonymous;
644                // it does not contribute a single key.
645                _ => None,
646            },
647            NamingPrefix::Spread(_) => None,
648            NamingPrefix::Alias(alias) => Some(&alias.name),
649        }
650    }
651
652    pub(super) fn is_anonymous(&self) -> bool {
653        match &self.prefix {
654            NamingPrefix::None => match self.path.as_ref() {
655                LitExpr::Path(path) => path.is_anonymous(),
656                // A non-path LitExpr (primitive, object, array, LitPath,
657                // LitOpChain) has no key of its own, so without an Alias
658                // or Spread prefix it is anonymous.
659                _ => true,
660            },
661            NamingPrefix::Alias(_) => false,
662            NamingPrefix::Spread(_) => false,
663        }
664    }
665
666    pub(super) fn field(
667        alias: Option<Alias>,
668        name: WithRange<Key>,
669        selection: Option<SubSelection>,
670    ) -> Self {
671        let name_range = name.range();
672        let tail = if let Some(selection) = selection.as_ref() {
673            WithRange::new(PathList::Selection(selection.clone()), selection.range())
674        } else {
675            // The empty range is a collapsed range at the end of the
676            // preceding path, i.e. at the end of the field name.
677            let empty_range = name_range.as_ref().map(|range| range.end..range.end);
678            WithRange::new(PathList::Empty, empty_range)
679        };
680        let tail_range = tail.range();
681        let name_tail_range = merge_ranges(name_range, tail_range);
682        let prefix = if let Some(alias) = alias {
683            NamingPrefix::Alias(alias)
684        } else {
685            NamingPrefix::None
686        };
687        Self {
688            prefix,
689            path: path_value(PathSelection {
690                path: WithRange::new(PathList::Key(name, tail), name_tail_range),
691            }),
692        }
693    }
694
695    pub(crate) fn parse(input: Span) -> ParseResult<Self> {
696        match get_connect_spec(&input) {
697            ConnectSpec::V0_1 | ConnectSpec::V0_2 => Self::parse_v0_2(input),
698            ConnectSpec::V0_3 => Self::parse_v0_3(input),
699            ConnectSpec::V0_4 => Self::parse_v0_4(input),
700        }
701    }
702
703    pub(crate) fn parse_v0_2(input: Span) -> ParseResult<Self> {
704        alt((
705            // We must try parsing NamedPathSelection before NamedFieldSelection
706            // and NamedQuotedSelection because a NamedPathSelection without a
707            // leading `.`, such as `alias: some.nested.path` has a prefix that
708            // can be parsed as a NamedFieldSelection: `alias: some`. Parsing
709            // then fails when it finds the remaining `.nested.path` text. Some
710            // parsers would solve this by forbidding `.` in the "lookahead" for
711            // Named{Field,Quoted}Selection, but negative lookahead is tricky in
712            // nom, so instead we greedily parse NamedPathSelection first.
713            Self::parse_path,
714            Self::parse_field,
715            Self::parse_group,
716        ))
717        .parse(input)
718    }
719
720    fn parse_field(input: Span) -> ParseResult<Self> {
721        (
722            opt(Alias::parse),
723            Key::parse,
724            spaces_or_comments,
725            opt(SubSelection::parse),
726        )
727            .parse(input)
728            .map(|(remainder, (alias, name, _, selection))| {
729                (remainder, Self::field(alias, name, selection))
730            })
731    }
732
733    // Parses either NamedPathSelection or PathWithSubSelection.
734    fn parse_path(input: Span) -> ParseResult<Self> {
735        if let Ok((remainder, alias)) = Alias::parse(input.clone()) {
736            match PathSelection::parse(remainder) {
737                Ok((remainder, path)) => Ok((
738                    remainder,
739                    Self {
740                        prefix: NamingPrefix::Alias(alias),
741                        path: path_value(path),
742                    },
743                )),
744                Err(nom::Err::Failure(e)) => Err(nom::Err::Failure(e)),
745                Err(_) => Err(nom_error_message(
746                    input.clone(),
747                    "Path selection alias must be followed by a path",
748                )),
749            }
750        } else {
751            match PathSelection::parse(input.clone()) {
752                Ok((remainder, path)) => {
753                    if path.is_anonymous() && path.has_subselection() {
754                        // This covers the old PathWithSubSelection syntax,
755                        // which is like ... in behavior (object properties
756                        // spread into larger object) but without the explicit
757                        // ... token. This syntax still works, provided the path
758                        // is both anonymous and has a trailing SubSelection.
759                        Ok((
760                            remainder,
761                            Self {
762                                prefix: NamingPrefix::Spread(None),
763                                path: path_value(path),
764                            },
765                        ))
766                    } else {
767                        Err(nom_fail_message(
768                            input.clone(),
769                            "Named path selection must either begin with alias or ..., or end with subselection",
770                        ))
771                    }
772                }
773                Err(nom::Err::Failure(e)) => Err(nom::Err::Failure(e)),
774                Err(_) => Err(nom_error_message(
775                    input.clone(),
776                    "Path selection must either begin with alias or ..., or end with subselection",
777                )),
778            }
779        }
780    }
781
782    fn parse_group(input: Span) -> ParseResult<Self> {
783        (Alias::parse, SubSelection::parse)
784            .parse(input)
785            .map(|(input, (alias, group))| {
786                let group_range = group.range();
787                (
788                    input,
789                    NamedSelection {
790                        prefix: NamingPrefix::Alias(alias),
791                        path: path_value(PathSelection {
792                            path: WithRange::new(PathList::Selection(group), group_range),
793                        }),
794                    },
795                )
796            })
797    }
798
799    // NamedSelection ::= (Alias | "...")? PathSelection | Alias SubSelection
800    // V0_3 version: Spread syntax (...) is NOT supported. Preserved for backwards compatibility.
801    fn parse_v0_3(input: Span) -> ParseResult<Self> {
802        let (after_alias, alias) = opt(Alias::parse).parse(input.clone())?;
803
804        if let Some(alias) = alias {
805            if let Ok((remainder, sub)) = SubSelection::parse(after_alias.clone()) {
806                let sub_range = sub.range();
807                return Ok((
808                    remainder,
809                    Self {
810                        prefix: NamingPrefix::Alias(alias),
811                        // This is what used to be called a NamedGroupSelection
812                        // in the grammar, where an Alias SubSelection can be
813                        // used to assign a nested name (the Alias) to a
814                        // selection of fields from the current object.
815                        // Logically, this corresponds to an Alias followed by a
816                        // PathSelection with an empty/missing Path. While there
817                        // is no way to write such a PathSelection normally, we
818                        // can construct a PathList consisting of only a
819                        // SubSelection here, for the sake of using the same
820                        // machinery to process all NamedSelection nodes.
821                        path: path_value(PathSelection {
822                            path: WithRange::new(PathList::Selection(sub), sub_range),
823                        }),
824                    },
825                ));
826            }
827
828            PathSelection::parse(after_alias.clone()).map(|(remainder, path)| {
829                (
830                    remainder,
831                    Self {
832                        prefix: NamingPrefix::Alias(alias),
833                        path: path_value(path),
834                    },
835                )
836            })
837        } else {
838            (
839                spaces_or_comments,
840                opt(ranged_span("...")),
841                PathSelection::parse,
842            )
843                .parse(input.clone())
844                .map(|(mut remainder, (_spaces, spread, path))| {
845                    let prefix = if let Some(spread) = spread {
846                        // V0_3 does NOT support spread syntax - always add error
847                        remainder.extra.errors.push((
848                        "Spread syntax (...) is not supported in connect/v0.3 (use connect/v0.4)"
849                            .to_string(),
850                        input.location_offset(),
851                    ));
852                        // An explicit ... spread token was used, so we record
853                        // NamingPrefix::Spread(Some(_)). If the path produces
854                        // something other than an object or null, we will catch
855                        // that in apply_to_path and compute_output_shape (not a
856                        // parsing concern).
857                        NamingPrefix::Spread(spread.range())
858                    } else if path.is_anonymous() && path.has_subselection() {
859                        // If there is no Alias or ... and the path is anonymous and
860                        // it has a trailing SubSelection, then it should be spread
861                        // into the larger SubSelection. This is an older syntax
862                        // (PathWithSubSelection) that provided some of the benefits
863                        // of ..., before ... was supported (in connect/v0.3). It's
864                        // important the path is anonymous, since regular field
865                        // selections like `user { id name }` meet all the criteria
866                        // above but should not be spread because they do produce an
867                        // output key.
868                        NamingPrefix::Spread(None)
869                    } else {
870                        // Otherwise, the path has no prefix, so it either produces
871                        // a single Key according to path.get_single_key(), or this
872                        // is an anonymous NamedSelection, which are only allowed at
873                        // the top level. However, since we don't know about other
874                        // NamedSelections here, these rules have to be enforced at
875                        // a higher level.
876                        NamingPrefix::None
877                    };
878                    (
879                        remainder,
880                        Self {
881                            prefix,
882                            path: path_value(path),
883                        },
884                    )
885                })
886        }
887    }
888
889    // NamedSelection ::= (Alias | "...")? PathSelection | Alias SubSelection
890    // This version enables spread syntax (...) for abstract types support.
891    fn parse_v0_4(input: Span) -> ParseResult<Self> {
892        let (after_alias, alias) = opt(Alias::parse).parse(input.clone())?;
893
894        if let Some(alias) = alias {
895            if let Ok((remainder, sub)) = SubSelection::parse(after_alias.clone()) {
896                let sub_range = sub.range();
897                return Ok((
898                    remainder,
899                    Self {
900                        prefix: NamingPrefix::Alias(alias),
901                        // This is what used to be called a NamedGroupSelection
902                        // in the grammar, where an Alias SubSelection can be
903                        // used to assign a nested name (the Alias) to a
904                        // selection of fields from the current object.
905                        // Logically, this corresponds to an Alias followed by a
906                        // PathSelection with an empty/missing Path. While there
907                        // is no way to write such a PathSelection normally, we
908                        // can construct a PathList consisting of only a
909                        // SubSelection here, for the sake of using the same
910                        // machinery to process all NamedSelection nodes.
911                        path: path_value(PathSelection {
912                            path: WithRange::new(PathList::Selection(sub), sub_range),
913                        }),
914                    },
915                ));
916            }
917
918            // In v0.4, a NamedSelection value after `alias:` accepts any
919            // `LitExpr` — bare JSON literals (`1`, `"x"`, `{...}`, `[...]`,
920            // `LitPath`, `LitOpChain`) as well as paths. We store the parsed
921            // `LitExpr` verbatim: a source `$(...)` produces `LitExpr::Path`
922            // with a `PathList::Expr` head, while a bare literal stays as its
923            // own `LitExpr` variant — no synthetic `PathList::Expr` wrapping.
924            LitExpr::parse(after_alias.clone()).map(|(remainder, lit)| {
925                (
926                    remainder,
927                    Self {
928                        prefix: NamingPrefix::Alias(alias),
929                        path: lit,
930                    },
931                )
932            })
933        } else {
934            (
935                spaces_or_comments,
936                opt(ranged_span("...")),
937                PathSelection::parse,
938            )
939                .parse(input.clone())
940                .map(|(remainder, (_spaces, spread, path))| {
941                    let prefix = if let Some(spread) = spread {
942                        // Spread syntax is fully supported in V0_4
943                        NamingPrefix::Spread(spread.range())
944                    } else if path.is_anonymous() && path.has_subselection() {
945                        // If there is no Alias or ... and the path is anonymous and
946                        // it has a trailing SubSelection, then it should be spread
947                        // into the larger SubSelection. This is an older syntax
948                        // (PathWithSubSelection) that provided some of the benefits
949                        // of ..., before ... was supported (in connect/v0.3). It's
950                        // important the path is anonymous, since regular field
951                        // selections like `user { id name }` meet all the criteria
952                        // above but should not be spread because they do produce an
953                        // output key.
954                        NamingPrefix::Spread(None)
955                    } else {
956                        // Otherwise, the path has no prefix, so it either produces
957                        // a single Key according to path.get_single_key(), or this
958                        // is an anonymous NamedSelection, which are only allowed at
959                        // the top level. However, since we don't know about other
960                        // NamedSelections here, these rules have to be enforced at
961                        // a higher level.
962                        NamingPrefix::None
963                    };
964                    (
965                        remainder,
966                        Self {
967                            prefix,
968                            path: path_value(path),
969                        },
970                    )
971                })
972        }
973    }
974
975    pub(crate) fn names(&self) -> Vec<&str> {
976        if let Some(single_key) = self.get_single_key() {
977            vec![single_key.as_str()]
978        } else if let Some(sub) = self.next_subselection() {
979            // Flatten and deduplicate the names of the NamedSelection
980            // items in the SubSelection.
981            let mut name_set = IndexSet::default();
982            for selection in sub.selections_iter() {
983                name_set.extend(selection.names());
984            }
985            name_set.into_iter().collect()
986        } else {
987            Vec::new()
988        }
989    }
990
991    /// Find the next subselection, if present. Delegates to
992    /// [`LitExpr::next_subselection`].
993    pub(crate) fn next_subselection(&self) -> Option<&SubSelection> {
994        self.path.as_ref().next_subselection()
995    }
996
997    /// Mutable counterpart of [`Self::next_subselection`].
998    #[allow(unused)]
999    pub(crate) fn next_mut_subselection(&mut self) -> Option<&mut SubSelection> {
1000        self.path.as_mut().next_mut_subselection()
1001    }
1002}
1003
1004impl VarPaths for NamedSelection {
1005    fn var_paths(&self) -> Vec<&PathSelection> {
1006        self.path.as_ref().var_paths()
1007    }
1008}
1009
1010// Path                 ::= VarPath | KeyPath | AtPath | ExprPath
1011// PathSelection        ::= Path SubSelection?
1012// VarPath              ::= "$" (NO_SPACE Identifier)? PathTail
1013// KeyPath              ::= Key PathTail
1014// AtPath               ::= "@" PathTail
1015// ExprPath             ::= "$(" LitExpr ")" PathTail
1016// PathTail             ::= "?"? (PathStep "?"?)*
1017// PathStep             ::= "." Key | "->" Identifier MethodArgs?
1018
1019#[derive(Debug, PartialEq, Eq, Clone)]
1020pub struct PathSelection {
1021    pub(super) path: WithRange<PathList>,
1022}
1023
1024// Like NamedSelection, PathSelection is an AST structure that takes its range
1025// entirely from self.path (a WithRange<PathList>), so PathSelection itself does
1026// not need to be wrapped as WithRange<PathSelection>, but merely needs to
1027// implement the Ranged trait.
1028impl Ranged for PathSelection {
1029    fn range(&self) -> OffsetRange {
1030        self.path.range()
1031    }
1032}
1033
1034impl PathSelection {
1035    pub(crate) fn parse(input: Span) -> ParseResult<Self> {
1036        PathList::parse(input).map(|(input, path)| (input, Self { path }))
1037    }
1038
1039    pub(crate) fn variable_reference<N: FromStr + ToString>(&self) -> Option<VariableReference<N>> {
1040        match self.path.as_ref() {
1041            PathList::Var(var, tail) => match var.as_ref() {
1042                KnownVariable::External(namespace) => {
1043                    // Build the variable's consumption trie via the fused
1044                    // shape-computation machinery so we correctly traverse
1045                    // through `->method(...)` arrow chains, `?` operators,
1046                    // and nested subselections like
1047                    // `$this.items->filter(@.product) { id name }`. The legacy
1048                    // walker (`tail.compute_selection_trie()`) treats those as
1049                    // opaque leaves; this one keeps walking. (RH-1345 / CNN-1093)
1050                    let selection = tail.compute_consumption_trie(namespace);
1051                    let full_range = merge_ranges(var.range(), tail.range());
1052                    Some(VariableReference {
1053                        namespace: VariableNamespace {
1054                            namespace: N::from_str(namespace).ok()?,
1055                            location: var.range(),
1056                        },
1057                        selection,
1058                        location: full_range,
1059                    })
1060                }
1061                _ => None,
1062            },
1063            _ => None,
1064        }
1065    }
1066
1067    pub(super) fn is_single_key(&self) -> bool {
1068        self.path.is_single_key()
1069    }
1070
1071    pub(super) fn get_single_key(&self) -> Option<&WithRange<Key>> {
1072        self.path.get_single_key()
1073    }
1074
1075    pub(super) fn is_anonymous(&self) -> bool {
1076        self.path.is_anonymous()
1077    }
1078
1079    #[allow(unused)]
1080    pub(super) fn from_slice(keys: &[Key], selection: Option<SubSelection>) -> Self {
1081        Self {
1082            path: WithRange::new(PathList::from_slice(keys, selection), None),
1083        }
1084    }
1085
1086    #[allow(unused)]
1087    pub(super) fn has_subselection(&self) -> bool {
1088        self.path.has_subselection()
1089    }
1090
1091    pub(super) fn next_subselection(&self) -> Option<&SubSelection> {
1092        self.path.next_subselection()
1093    }
1094
1095    #[allow(unused)]
1096    pub(super) fn next_mut_subselection(&mut self) -> Option<&mut SubSelection> {
1097        self.path.next_mut_subselection()
1098    }
1099}
1100
1101impl VarPaths for PathSelection {
1102    fn var_paths(&self) -> Vec<&PathSelection> {
1103        let mut paths = Vec::new();
1104        match self.path.as_ref() {
1105            PathList::Var(var_name, tail) => {
1106                // At this point, we're collecting both external and local
1107                // variable references (but not references to internal variables
1108                // like $ and @). These mixed variables will be filtered in
1109                // VarPaths::external_var_paths and ::local_var_paths.
1110                if matches!(
1111                    var_name.as_ref(),
1112                    KnownVariable::External(_) | KnownVariable::Local(_)
1113                ) {
1114                    paths.push(self);
1115                }
1116                paths.extend(tail.var_paths());
1117            }
1118            other => {
1119                paths.extend(other.var_paths());
1120            }
1121        };
1122        paths
1123    }
1124}
1125
1126impl From<PathList> for PathSelection {
1127    fn from(path: PathList) -> Self {
1128        Self {
1129            path: WithRange::new(path, None),
1130        }
1131    }
1132}
1133
1134#[derive(Debug, PartialEq, Eq, Clone)]
1135pub(crate) enum PathList {
1136    // A VarPath must start with a variable (either $identifier, $, or @),
1137    // followed by any number of PathStep items (the WithRange<PathList>).
1138    // Because we represent the @ quasi-variable using PathList::Var, this
1139    // variant handles both VarPath and AtPath from the grammar. The
1140    // PathList::Var variant may only appear at the beginning of a
1141    // PathSelection's PathList, not in the middle.
1142    Var(WithRange<KnownVariable>, WithRange<PathList>),
1143
1144    // A PathSelection that starts with a PathList::Key is a KeyPath, but a
1145    // PathList::Key also counts as PathStep item, so it may also appear in the
1146    // middle/tail of a PathList.
1147    Key(WithRange<Key>, WithRange<PathList>),
1148
1149    // An ExprPath, which begins with a LitExpr enclosed by $(...). Must appear
1150    // only at the beginning of a PathSelection, like PathList::Var.
1151    Expr(WithRange<LitExpr>, WithRange<PathList>),
1152
1153    // A PathList::Method is a PathStep item that may appear only in the
1154    // middle/tail (not the beginning) of a PathSelection.
1155    Method(WithRange<String>, Option<MethodArgs>, WithRange<PathList>),
1156
1157    // Represents the ? syntax used for some.path?->method(...) optional
1158    // chaining. If the preceding some.path value is missing (None) or null,
1159    // some.path? evaluates to None, terminating path evaluation without an
1160    // error. All other (non-null) values are passed along without change.
1161    //
1162    // The WithRange<PathList> parameter represents the rest of the path
1163    // following the `?` token.
1164    Question(WithRange<PathList>),
1165
1166    // Optionally, a PathList may end with a SubSelection, which applies a set
1167    // of named selections to the final value of the path. PathList::Selection
1168    // by itself is not a valid PathList.
1169    Selection(SubSelection),
1170
1171    // Every PathList must be terminated by either PathList::Selection or
1172    // PathList::Empty. PathList::Empty by itself is not a valid PathList.
1173    Empty,
1174}
1175
1176impl PathList {
1177    pub(crate) fn is_empty(&self) -> bool {
1178        matches!(self, PathList::Empty)
1179    }
1180
1181    pub(super) fn parse(input: Span) -> ParseResult<WithRange<Self>> {
1182        match Self::parse_with_depth(input.clone(), 0) {
1183            Ok((_, parsed)) if matches!(*parsed, Self::Empty) => Err(nom_error_message(
1184                input.clone(),
1185                // As a small technical note, you could consider
1186                // NamedGroupSelection (an Alias followed by a SubSelection) as
1187                // a kind of NamedPathSelection where the path is empty, but
1188                // it's still useful to distinguish groups in the grammar so we
1189                // can forbid empty paths in general. In fact, when parsing a
1190                // NamedGroupSelection, this error message is likely to be the
1191                // reason we abandon parsing NamedPathSelection and correctly
1192                // fall back to NamedGroupSelection.
1193                "Path selection cannot be empty",
1194            )),
1195            otherwise => otherwise,
1196        }
1197    }
1198
1199    #[cfg(test)]
1200    pub(super) fn into_with_range(self) -> WithRange<Self> {
1201        WithRange::new(self, None)
1202    }
1203
1204    pub(super) fn parse_with_depth(input: Span, depth: usize) -> ParseResult<WithRange<Self>> {
1205        let spec = get_connect_spec(&input);
1206
1207        // If the input is empty (i.e. this method will end up returning
1208        // PathList::Empty), we want the OffsetRange to be an empty range at the
1209        // end of the previously parsed PathList elements, not separated from
1210        // them by trailing spaces or comments, so we need to capture the empty
1211        // range before consuming leading spaces_or_comments.
1212        let offset_if_empty = input.location_offset();
1213        let range_if_empty: OffsetRange = Some(offset_if_empty..offset_if_empty);
1214
1215        // Consume leading spaces_or_comments for all cases below.
1216        let (input, _spaces) = spaces_or_comments(input)?;
1217
1218        // Variable references (including @ references), $(...) literals, and
1219        // key references without a leading . are accepted only at depth 0, or
1220        // at the beginning of the PathSelection.
1221        if depth == 0 {
1222            // The $(...) syntax allows embedding LitExpr values within
1223            // JSONSelection syntax (when not already parsing a LitExpr). This
1224            // case needs to come before the $ (and $var) case, because $( looks
1225            // like the $ variable followed by a parse error in the variable
1226            // case, unless we add some complicated lookahead logic there.
1227            match (
1228                spaces_or_comments,
1229                ranged_span("$("),
1230                LitExpr::parse,
1231                spaces_or_comments,
1232                ranged_span(")"),
1233            )
1234                .parse(input.clone())
1235            {
1236                Ok((suffix, (_, dollar_open_paren, expr, close_paren, _))) => {
1237                    let (remainder, rest) = Self::parse_with_depth(suffix, depth + 1)?;
1238                    let expr_range = merge_ranges(dollar_open_paren.range(), close_paren.range());
1239                    let full_range = merge_ranges(expr_range, rest.range());
1240                    return Ok((
1241                        remainder,
1242                        WithRange::new(Self::Expr(expr, rest), full_range),
1243                    ));
1244                }
1245                Err(nom::Err::Failure(err)) => {
1246                    return Err(nom::Err::Failure(err));
1247                }
1248                Err(_) => {
1249                    // We can otherwise continue for non-fatal errors
1250                }
1251            }
1252
1253            if let Ok((suffix, (dollar, opt_var))) =
1254                (ranged_span("$"), opt(parse_identifier_no_space)).parse(input.clone())
1255            {
1256                let dollar_range = dollar.range();
1257                let (remainder, rest) = Self::parse_with_depth(suffix, depth + 1)?;
1258                let full_range = merge_ranges(dollar_range.clone(), rest.range());
1259                return if let Some(var) = opt_var {
1260                    let full_name = format!("{}{}", dollar.as_ref(), var.as_str());
1261                    // This KnownVariable::External variant may get remapped to
1262                    // KnownVariable::Local if the variable was parsed as the
1263                    // first argument of an input->as($var) method call.
1264                    let known_var = if input.extra.is_local_var(&full_name) {
1265                        KnownVariable::Local(full_name)
1266                    } else {
1267                        KnownVariable::External(full_name)
1268                    };
1269                    let var_range = merge_ranges(dollar_range, var.range());
1270                    let ranged_known_var = WithRange::new(known_var, var_range);
1271                    Ok((
1272                        remainder,
1273                        WithRange::new(Self::Var(ranged_known_var, rest), full_range),
1274                    ))
1275                } else {
1276                    let ranged_dollar_var = WithRange::new(KnownVariable::Dollar, dollar_range);
1277                    Ok((
1278                        remainder,
1279                        WithRange::new(Self::Var(ranged_dollar_var, rest), full_range),
1280                    ))
1281                };
1282            }
1283
1284            if let Ok((suffix, at)) = ranged_span("@").parse(input.clone()) {
1285                let (remainder, rest) = Self::parse_with_depth(suffix, depth + 1)?;
1286                let full_range = merge_ranges(at.range(), rest.range());
1287                return Ok((
1288                    remainder,
1289                    WithRange::new(
1290                        Self::Var(WithRange::new(KnownVariable::AtSign, at.range()), rest),
1291                        full_range,
1292                    ),
1293                ));
1294            }
1295
1296            if let Ok((suffix, key)) = Key::parse(input.clone()) {
1297                let (remainder, rest) = Self::parse_with_depth(suffix, depth + 1)?;
1298
1299                return match spec {
1300                    ConnectSpec::V0_1 | ConnectSpec::V0_2 => match rest.as_ref() {
1301                        // We use nom_error_message rather than nom_fail_message
1302                        // here because the key might actually be a field selection,
1303                        // which means we want to unwind parsing the path and fall
1304                        // back to parsing other kinds of NamedSelection.
1305                        Self::Empty | Self::Selection(_) => Err(nom_error_message(
1306                            input.clone(),
1307                            // Another place where format! might be useful to
1308                            // suggest .{key}, which would require storing error
1309                            // messages as owned Strings.
1310                            "Single-key path must be prefixed with $. to avoid ambiguity with field name",
1311                        )),
1312                        _ => {
1313                            let full_range = merge_ranges(key.range(), rest.range());
1314                            Ok((remainder, WithRange::new(Self::Key(key, rest), full_range)))
1315                        }
1316                    },
1317
1318                    // With the unification of NamedSelection enum variants into
1319                    // a single struct in connect/v0.3, the ambiguity between
1320                    // single-key paths and field selections is no longer a
1321                    // problem, since they are now represented the same way.
1322                    ConnectSpec::V0_3 | ConnectSpec::V0_4 => {
1323                        let full_range = merge_ranges(key.range(), rest.range());
1324                        Ok((remainder, WithRange::new(Self::Key(key, rest), full_range)))
1325                    }
1326                };
1327            }
1328        }
1329
1330        if depth == 0 {
1331            // If the PathSelection does not start with a $var (or $ or @), a
1332            // key., or $(expr), it is not a valid PathSelection.
1333            if (ranged_span("."), Key::parse).parse(input.clone()).is_ok() {
1334                // Since we previously allowed starting key paths with .key but
1335                // now forbid that syntax (because it can be ambiguous), suggest
1336                // the unambiguous $.key syntax instead.
1337                return Err(nom_fail_message(
1338                    input.clone(),
1339                    "Key paths cannot start with just .key (use $.key instead)",
1340                ));
1341            }
1342            // This error technically covers the case above, but doesn't suggest
1343            // a helpful solution.
1344            return Err(nom_error_message(
1345                input.clone(),
1346                "Path selection must start with key, $variable, $, @, or $(expression)",
1347            ));
1348        }
1349
1350        // At any depth, if the next token is ? but not the PathList::Question
1351        // kind, we terminate path parsing so the hypothetical ?? or ?! tokens
1352        // have a chance to be parsed as infix operators. This is not
1353        // version-gated to connect/v0.3, because we want to begin forbidding
1354        // these tokens as continuations of a Path as early as we can.
1355        if input.fragment().starts_with("??") || input.fragment().starts_with("?!") {
1356            return Ok((input, WithRange::new(Self::Empty, range_if_empty)));
1357        }
1358
1359        match spec {
1360            ConnectSpec::V0_1 | ConnectSpec::V0_2 => {
1361                // The ? token was not introduced until connect/v0.3.
1362            }
1363            ConnectSpec::V0_3 | ConnectSpec::V0_4 => {
1364                if let Ok((suffix, question)) = ranged_span("?").parse(input.clone()) {
1365                    let (remainder, rest) = Self::parse_with_depth(suffix.clone(), depth + 1)?;
1366
1367                    return match rest.as_ref() {
1368                        // The ? cannot be repeated sequentially, so if rest starts with
1369                        // another PathList::Question, we terminate the current path,
1370                        // probably (but not necessarily) leading to a parse error for
1371                        // the upcoming ?.
1372                        PathList::Question(_) => {
1373                            let empty_range = question.range().map(|range| range.end..range.end);
1374                            let empty = WithRange::new(Self::Empty, empty_range);
1375                            Ok((
1376                                suffix,
1377                                WithRange::new(Self::Question(empty), question.range()),
1378                            ))
1379                        }
1380                        _ => {
1381                            let full_range = merge_ranges(question.range(), rest.range());
1382                            Ok((remainder, WithRange::new(Self::Question(rest), full_range)))
1383                        }
1384                    };
1385                }
1386            }
1387        };
1388
1389        // In previous versions of this code, a .key could appear at depth 0 (at
1390        // the beginning of a path), which was useful to disambiguate a KeyPath
1391        // consisting of a single key from a field selection.
1392        //
1393        // Now that key paths can appear alongside/after named selections within
1394        // a SubSelection, the .key syntax is potentially unsafe because it may
1395        // be parsed as a continuation of a previous field selection, since we
1396        // ignore spaces/newlines/comments between keys in a path.
1397        //
1398        // In order to prevent this ambiguity, we now require that a single .key
1399        // be written as a subproperty of the $ variable, e.g. $.key, which is
1400        // equivalent to the old behavior, but parses unambiguously. In terms of
1401        // this code, that means we allow a .key only at depths > 0.
1402        if let Ok((remainder, (dot, key))) = (ranged_span("."), Key::parse).parse(input.clone()) {
1403            let (remainder, rest) = Self::parse_with_depth(remainder, depth + 1)?;
1404            let dot_key_range = merge_ranges(dot.range(), key.range());
1405            let full_range = merge_ranges(dot_key_range, rest.range());
1406            return Ok((remainder, WithRange::new(Self::Key(key, rest), full_range)));
1407        }
1408
1409        // If we failed to parse "." Key above, but the input starts with a '.'
1410        // character, it's an error unless it's the beginning of a ... token.
1411        if input.fragment().starts_with('.') && !input.fragment().starts_with("...") {
1412            return Err(nom_fail_message(
1413                input.clone(),
1414                "Path selection . must be followed by key (identifier or quoted string literal)",
1415            ));
1416        }
1417
1418        // PathSelection can never start with a naked ->method (instead, use
1419        // $->method or @->method if you want to operate on the current value).
1420        if let Ok((suffix, arrow)) = ranged_span("->").parse(input.clone()) {
1421            // As soon as we see a -> token, we know what follows must be a
1422            // method name, so we can unconditionally return based on what
1423            // parse_identifier tells us. since MethodArgs::parse is optional,
1424            // the absence of args will never trigger the error case.
1425            return match (parse_identifier, opt(MethodArgs::parse)).parse(suffix) {
1426                Ok((suffix, (method, args_opt))) => {
1427                    let mut local_var_name = None;
1428
1429                    // Convert the first argument of input->as($var) from
1430                    // KnownVariable::External (the default for parsed named
1431                    // variable references) to KnownVariable::Local, when we know
1432                    // we're parsing an ->as($var) method invocation.
1433                    let args = if let Some(args) = args_opt.as_ref()
1434                        && ArrowMethod::lookup(method.as_ref()) == Some(ArrowMethod::As)
1435                    {
1436                        let new_args = if let Some(old_first_arg) = args.args.first()
1437                            && let LitExpr::Path(path_selection) = old_first_arg.as_ref()
1438                            && let PathList::Var(var_name, var_tail) = path_selection.path.as_ref()
1439                            && let KnownVariable::External(var_str) | KnownVariable::Local(var_str) =
1440                                var_name.as_ref()
1441                        {
1442                            let as_var = WithRange::new(
1443                                // This is the key change: remap to KnownVariable::Local.
1444                                KnownVariable::Local(var_str.clone()),
1445                                var_name.range(),
1446                            );
1447
1448                            local_var_name = Some(var_str.clone());
1449
1450                            let new_first_arg = WithRange::new(
1451                                LitExpr::Path(PathSelection {
1452                                    path: WithRange::new(
1453                                        PathList::Var(as_var, var_tail.clone()),
1454                                        path_selection.range(),
1455                                    ),
1456                                }),
1457                                old_first_arg.range(),
1458                            );
1459
1460                            let mut new_args = vec![new_first_arg];
1461                            new_args.extend(args.args.iter().skip(1).cloned());
1462                            new_args
1463                        } else {
1464                            args.args.clone()
1465                        };
1466
1467                        Some(MethodArgs {
1468                            args: new_args,
1469                            range: args.range(),
1470                        })
1471                    } else {
1472                        args_opt
1473                    };
1474
1475                    let suffix_with_local_var = if let Some(var_name) = local_var_name {
1476                        suffix.map_extra(|extra| extra.with_local_var(var_name))
1477                    } else {
1478                        suffix
1479                    };
1480
1481                    let (remainder, rest) =
1482                        Self::parse_with_depth(suffix_with_local_var, depth + 1)?;
1483                    let full_range = merge_ranges(arrow.range(), rest.range());
1484
1485                    Ok((
1486                        remainder,
1487                        WithRange::new(Self::Method(method, args, rest), full_range),
1488                    ))
1489                }
1490                Err(_) => Err(nom_fail_message(
1491                    input.clone(),
1492                    "Method name must follow ->",
1493                )),
1494            };
1495        }
1496
1497        // Likewise, if the PathSelection has a SubSelection, it must appear at
1498        // the end of a non-empty path. PathList::parse_with_depth is not
1499        // responsible for enforcing a trailing SubSelection in the
1500        // PathWithSubSelection case, since that requirement is checked by
1501        // NamedSelection::parse_path.
1502        if let Ok((suffix, selection)) = SubSelection::parse(input.clone()) {
1503            let selection_range = selection.range();
1504            return Ok((
1505                suffix,
1506                WithRange::new(Self::Selection(selection), selection_range),
1507            ));
1508        }
1509
1510        // The Self::Empty enum case is used to indicate the end of a
1511        // PathSelection that has no SubSelection.
1512        Ok((input.clone(), WithRange::new(Self::Empty, range_if_empty)))
1513    }
1514
1515    pub(super) fn is_anonymous(&self) -> bool {
1516        self.get_single_key().is_none()
1517    }
1518
1519    pub(super) fn is_single_key(&self) -> bool {
1520        self.get_single_key().is_some()
1521    }
1522
1523    pub(super) fn get_single_key(&self) -> Option<&WithRange<Key>> {
1524        fn rest_is_empty_or_selection(rest: &WithRange<PathList>) -> bool {
1525            match rest.as_ref() {
1526                PathList::Selection(_) | PathList::Empty => true,
1527                PathList::Question(tail) => rest_is_empty_or_selection(tail),
1528                // We could have a `_ => false` catch-all case here, but relying
1529                // on the exhaustiveness of this match ensures additions of new
1530                // PathList variants in the future (e.g. PathList::Question)
1531                // will be nudged to consider whether they should be compatible
1532                // with single-key field selections.
1533                PathList::Var(_, _)
1534                | PathList::Key(_, _)
1535                | PathList::Expr(_, _)
1536                | PathList::Method(_, _, _) => false,
1537            }
1538        }
1539
1540        match self {
1541            Self::Key(key, key_rest) => {
1542                if rest_is_empty_or_selection(key_rest) {
1543                    Some(key)
1544                } else {
1545                    None
1546                }
1547            }
1548            _ => None,
1549        }
1550    }
1551
1552    pub(super) fn is_question(&self) -> bool {
1553        matches!(self, Self::Question(_))
1554    }
1555
1556    #[allow(unused)]
1557    pub(super) fn from_slice(properties: &[Key], selection: Option<SubSelection>) -> Self {
1558        match properties {
1559            [] => selection.map_or(Self::Empty, Self::Selection),
1560            [head, tail @ ..] => Self::Key(
1561                WithRange::new(head.clone(), None),
1562                WithRange::new(Self::from_slice(tail, selection), None),
1563            ),
1564        }
1565    }
1566
1567    pub(super) fn has_subselection(&self) -> bool {
1568        self.next_subselection().is_some()
1569    }
1570
1571    /// Find the next subselection, traversing nested chains if needed
1572    pub(super) fn next_subselection(&self) -> Option<&SubSelection> {
1573        match self {
1574            Self::Var(_, tail) => tail.next_subselection(),
1575            Self::Key(_, tail) => tail.next_subselection(),
1576            Self::Expr(_, tail) => tail.next_subselection(),
1577            Self::Method(_, _, tail) => tail.next_subselection(),
1578            Self::Question(tail) => tail.next_subselection(),
1579            Self::Selection(sub) => Some(sub),
1580            Self::Empty => None,
1581        }
1582    }
1583
1584    #[allow(unused)]
1585    /// Find the next subselection, traversing nested chains if needed. Returns a mutable reference
1586    pub(super) fn next_mut_subselection(&mut self) -> Option<&mut SubSelection> {
1587        match self {
1588            Self::Var(_, tail) => tail.next_mut_subselection(),
1589            Self::Key(_, tail) => tail.next_mut_subselection(),
1590            Self::Expr(_, tail) => tail.next_mut_subselection(),
1591            Self::Method(_, _, tail) => tail.next_mut_subselection(),
1592            Self::Question(tail) => tail.next_mut_subselection(),
1593            Self::Selection(sub) => Some(sub),
1594            Self::Empty => None,
1595        }
1596    }
1597}
1598
1599impl VarPaths for PathList {
1600    fn var_paths(&self) -> Vec<&PathSelection> {
1601        let mut paths = Vec::new();
1602        match self {
1603            // PathSelection::var_paths is responsible for adding all
1604            // variable &PathSelection items to the set, since this
1605            // PathList::Var case cannot be sure it's looking at the beginning
1606            // of the path. However, we call rest.var_paths()
1607            // recursively because the tail of the list could contain other full
1608            // PathSelection variable references.
1609            PathList::Var(_, rest) | PathList::Key(_, rest) => {
1610                paths.extend(rest.var_paths());
1611            }
1612            PathList::Expr(expr, rest) => {
1613                paths.extend(expr.var_paths());
1614                paths.extend(rest.var_paths());
1615            }
1616            PathList::Method(_, opt_args, rest) => {
1617                if let Some(args) = opt_args {
1618                    for lit_arg in &args.args {
1619                        paths.extend(lit_arg.var_paths());
1620                    }
1621                }
1622                paths.extend(rest.var_paths());
1623            }
1624            PathList::Question(rest) => {
1625                paths.extend(rest.var_paths());
1626            }
1627            PathList::Selection(sub) => paths.extend(sub.var_paths()),
1628            PathList::Empty => {}
1629        }
1630        paths
1631    }
1632}
1633
1634// Sticky delimiter mode for `SubSelection::parse_naked_selections_v0_4`,
1635// set by the first inter-item separator and enforced for the rest of the
1636// list. See that function for the diagnostics emitted on mixed separators.
1637enum NamedSelectionSeparator {
1638    Undetermined,
1639    Comma,
1640    Space,
1641}
1642
1643// SubSelection ::= "{" NakedSubSelection "}"
1644
1645#[derive(Debug, PartialEq, Eq, Clone, Default)]
1646pub struct SubSelection {
1647    pub(super) selections: Vec<NamedSelection>,
1648    pub(super) range: OffsetRange,
1649}
1650
1651impl Ranged for SubSelection {
1652    // Since SubSelection is a struct, we can store its range directly as a
1653    // field of the struct, allowing SubSelection to implement the Ranged trait
1654    // without a WithRange<SubSelection> wrapper.
1655    fn range(&self) -> OffsetRange {
1656        self.range.clone()
1657    }
1658}
1659
1660impl SubSelection {
1661    pub(crate) fn parse(input: Span) -> ParseResult<Self> {
1662        match (
1663            spaces_or_comments,
1664            ranged_span("{"),
1665            Self::parse_naked,
1666            spaces_or_comments,
1667            ranged_span("}"),
1668        )
1669            .parse(input)
1670        {
1671            Ok((remainder, (_, open_brace, sub, _, close_brace))) => {
1672                let range = merge_ranges(open_brace.range(), close_brace.range());
1673                Ok((
1674                    remainder,
1675                    Self {
1676                        selections: sub.selections,
1677                        range,
1678                    },
1679                ))
1680            }
1681            Err(e) => Err(e),
1682        }
1683    }
1684
1685    fn parse_naked(input: Span) -> ParseResult<Self> {
1686        let selections_result = match get_connect_spec(&input) {
1687            ConnectSpec::V0_1 | ConnectSpec::V0_2 | ConnectSpec::V0_3 => {
1688                Self::parse_naked_selections_legacy(input.clone())
1689            }
1690            ConnectSpec::V0_4 => Self::parse_naked_selections_v0_4(input.clone()),
1691        };
1692
1693        match selections_result {
1694            Ok((remainder, selections)) => {
1695                // Enforce that if selections has any anonymous NamedSelection
1696                // elements, there is only one and it's the only NamedSelection in
1697                // the SubSelection.
1698                for sel in selections.iter() {
1699                    if sel.is_anonymous() && selections.len() > 1 {
1700                        return Err(nom_error_message(
1701                            input.clone(),
1702                            "SubSelection cannot contain multiple elements if it contains an anonymous NamedSelection",
1703                        ));
1704                    }
1705                }
1706
1707                let range = merge_ranges(
1708                    selections.first().and_then(|first| first.range()),
1709                    selections.last().and_then(|last| last.range()),
1710                );
1711
1712                Ok((remainder, Self { selections, range }))
1713            }
1714            Err(e) => Err(e),
1715        }
1716    }
1717
1718    fn parse_naked_selections_legacy(input: Span) -> ParseResult<Vec<NamedSelection>> {
1719        many0(NamedSelection::parse).parse(input)
1720    }
1721
1722    // In connect/v0.4, a SubSelection body separates its NamedSelection items
1723    // either entirely with commas (with an optional trailing comma) or entirely
1724    // with whitespace. Mixing the two styles is an error. This unifies the
1725    // shape of object literals (LitExpr::Object) with regular selection sets.
1726    //
1727    // Two mixed-separator cases are diagnosed with a targeted fatal error so
1728    // users get a clear message instead of a generic "Eof" / "trailing
1729    // characters" failure:
1730    //
1731    //   • whitespace-then-comma   `{ a b, c }`  — the comma is unexpected
1732    //     because the list committed to whitespace after `a b`.
1733    //   • comma-then-whitespace   `{ a, b c }`  — `c` is unexpected because
1734    //     the list committed to commas after `a, b`, and `c` follows without
1735    //     a preceding comma.
1736    //
1737    // Both errors point at the offending span (the stray comma, or the item
1738    // missing its preceding comma).
1739    fn parse_naked_selections_v0_4(input: Span) -> ParseResult<Vec<NamedSelection>> {
1740        let mut selections: Vec<NamedSelection> = Vec::new();
1741
1742        // Try parsing the first NamedSelection. If none, we're done (empty body).
1743        let mut rest = match NamedSelection::parse(input.clone()) {
1744            Ok((r, sel)) => {
1745                selections.push(sel);
1746                r
1747            }
1748            Err(nom::Err::Error(_)) => return Ok((input, selections)),
1749            Err(e) => return Err(e),
1750        };
1751
1752        let mut mode = NamedSelectionSeparator::Undetermined;
1753
1754        loop {
1755            let (after_ws, _) = spaces_or_comments(rest.clone())?;
1756            let comma_attempt: ParseResult<char> = char(',').parse(after_ws.clone());
1757
1758            if let Ok((after_comma, _)) = comma_attempt {
1759                if matches!(mode, NamedSelectionSeparator::Space) {
1760                    // The final phrase before the colon ("Unexpected comma")
1761                    // is rendered by `JSONSelectionParseError`'s Display impl
1762                    // immediately before the offending fragment, which is why
1763                    // the explanation goes first and the colon-led summary
1764                    // lands at the end.
1765                    return Err(nom_fail_message(
1766                        after_ws,
1767                        "Items in this selection list are separated by whitespace, so commas \
1768                         cannot be mixed in. Use commas or whitespace consistently throughout \
1769                         the list. Unexpected comma",
1770                    ));
1771                }
1772                mode = NamedSelectionSeparator::Comma;
1773                rest = after_comma;
1774
1775                match NamedSelection::parse(rest.clone()) {
1776                    Ok((r, sel)) => {
1777                        selections.push(sel);
1778                        rest = r;
1779                    }
1780                    Err(nom::Err::Error(_)) => break, // trailing comma
1781                    Err(e) => return Err(e),
1782                }
1783            } else {
1784                if matches!(mode, NamedSelectionSeparator::Comma) {
1785                    // A comma-separated list must stay comma-separated. If
1786                    // another NamedSelection follows without a preceding
1787                    // comma, that is an inconsistent-separator error; if no
1788                    // NamedSelection follows, the list is simply over.
1789                    if NamedSelection::parse(after_ws.clone()).is_ok() {
1790                        // Likewise, the colon-led summary ("Missing comma
1791                        // before item") lands at the end so the offending
1792                        // item follows it directly when the error is shown.
1793                        return Err(nom_fail_message(
1794                            after_ws,
1795                            "Items in this selection list are separated by commas, so each item \
1796                             after the first needs a preceding comma. Use commas or whitespace \
1797                             consistently throughout the list. Missing comma before item",
1798                        ));
1799                    }
1800                    break;
1801                }
1802                match NamedSelection::parse(after_ws.clone()) {
1803                    Ok((r, sel)) => {
1804                        selections.push(sel);
1805                        rest = r;
1806                        mode = NamedSelectionSeparator::Space;
1807                    }
1808                    Err(nom::Err::Error(_)) => break,
1809                    Err(e) => return Err(e),
1810                }
1811            }
1812        }
1813
1814        Ok((rest, selections))
1815    }
1816
1817    // Returns an Iterator over each &NamedSelection that contributes a single
1818    // name to the output object. This is more complicated than returning
1819    // self.selections.iter() because some NamedSelection::Path elements can
1820    // contribute multiple names if they do no have an Alias.
1821    pub fn selections_iter(&self) -> impl Iterator<Item = &NamedSelection> {
1822        // TODO Implement a NamedSelectionIterator to traverse nested selections
1823        // lazily, rather than using an intermediary vector.
1824        let mut selections = Vec::new();
1825        for selection in &self.selections {
1826            if selection.has_single_output_key() {
1827                // If the PathSelection has an Alias, then it has a singular
1828                // name and should be visited directly.
1829                selections.push(selection);
1830            } else if let Some(sub) = selection.next_subselection() {
1831                // If the PathSelection does not have an Alias but does have a
1832                // SubSelection, then it represents the PathWithSubSelection
1833                // non-terminal from the grammar (see README.md + PR #6076),
1834                // which produces multiple names derived from the SubSelection,
1835                // which need to be recursively collected.
1836                selections.extend(sub.selections_iter());
1837            } else {
1838                // This no-Alias, no-SubSelection case should be forbidden by
1839                // NamedSelection::parse_path.
1840                debug_assert!(false, "PathSelection without Alias or SubSelection");
1841            }
1842        }
1843        selections.into_iter()
1844    }
1845
1846    pub fn append_selection(&mut self, selection: NamedSelection) {
1847        self.selections.push(selection);
1848    }
1849
1850    pub fn last_selection_mut(&mut self) -> Option<&mut NamedSelection> {
1851        self.selections.last_mut()
1852    }
1853}
1854
1855impl VarPaths for SubSelection {
1856    fn var_paths(&self) -> Vec<&PathSelection> {
1857        let mut paths = Vec::new();
1858        for selection in &self.selections {
1859            paths.extend(selection.var_paths());
1860        }
1861        paths
1862    }
1863}
1864
1865// Alias ::= Key ":"
1866
1867#[derive(Debug, PartialEq, Eq, Clone)]
1868pub(crate) struct Alias {
1869    pub(super) name: WithRange<Key>,
1870    pub(super) range: OffsetRange,
1871}
1872
1873impl Ranged for Alias {
1874    fn range(&self) -> OffsetRange {
1875        self.range.clone()
1876    }
1877}
1878
1879impl Alias {
1880    pub(crate) fn new(name: &str) -> Self {
1881        if is_identifier(name) {
1882            Self::field(name)
1883        } else {
1884            Self::quoted(name)
1885        }
1886    }
1887
1888    pub(crate) fn field(name: &str) -> Self {
1889        Self {
1890            name: WithRange::new(Key::field(name), None),
1891            range: None,
1892        }
1893    }
1894
1895    pub(crate) fn quoted(name: &str) -> Self {
1896        Self {
1897            name: WithRange::new(Key::quoted(name), None),
1898            range: None,
1899        }
1900    }
1901
1902    pub(crate) fn parse(input: Span) -> ParseResult<Self> {
1903        (Key::parse, spaces_or_comments, ranged_span(":"))
1904            .parse(input)
1905            .map(|(input, (name, _, colon))| {
1906                let range = merge_ranges(name.range(), colon.range());
1907                (input, Self { name, range })
1908            })
1909    }
1910}
1911
1912// Key ::= Identifier | LitString
1913
1914#[derive(Debug, PartialEq, Eq, Clone, Hash)]
1915pub enum Key {
1916    Field(String),
1917    Quoted(String),
1918}
1919
1920impl Key {
1921    pub(crate) fn parse(input: Span) -> ParseResult<WithRange<Self>> {
1922        alt((
1923            map(parse_identifier, |id| id.take_as(Key::Field)),
1924            map(parse_string_literal, |s| s.take_as(Key::Quoted)),
1925        ))
1926        .parse(input)
1927    }
1928
1929    pub fn field(name: &str) -> Self {
1930        Self::Field(name.to_string())
1931    }
1932
1933    pub fn quoted(name: &str) -> Self {
1934        Self::Quoted(name.to_string())
1935    }
1936
1937    pub fn into_with_range(self) -> WithRange<Self> {
1938        WithRange::new(self, None)
1939    }
1940
1941    pub fn is_quoted(&self) -> bool {
1942        matches!(self, Self::Quoted(_))
1943    }
1944
1945    pub fn to_json(&self) -> JSON {
1946        match self {
1947            Key::Field(name) => JSON::String(name.clone().into()),
1948            Key::Quoted(name) => JSON::String(name.clone().into()),
1949        }
1950    }
1951
1952    // This method returns the field/property name as a String, and is
1953    // appropriate for accessing JSON properties, in contrast to the dotted
1954    // method below.
1955    pub fn as_string(&self) -> String {
1956        match self {
1957            Key::Field(name) => name.clone(),
1958            Key::Quoted(name) => name.clone(),
1959        }
1960    }
1961    // Like as_string, but without cloning a new String, for times when the Key
1962    // itself lives longer than the &str.
1963    pub fn as_str(&self) -> &str {
1964        match self {
1965            Key::Field(name) => name.as_str(),
1966            Key::Quoted(name) => name.as_str(),
1967        }
1968    }
1969
1970    // This method is used to implement the Display trait for Key, and includes
1971    // a leading '.' character for string keys, as well as proper quoting for
1972    // Key::Quoted values. However, these additions make key.dotted() unsafe to
1973    // use for accessing JSON properties.
1974    pub fn dotted(&self) -> String {
1975        match self {
1976            Key::Field(field) => format!(".{field}"),
1977            Key::Quoted(field) => {
1978                // JSON encoding is a reliable way to ensure a string that may
1979                // contain special characters (such as '"' characters) is
1980                // properly escaped and double-quoted.
1981                let quoted = serde_json_bytes::Value::String(field.clone().into()).to_string();
1982                format!(".{quoted}")
1983            }
1984        }
1985    }
1986}
1987
1988impl Display for Key {
1989    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1990        let dotted = self.dotted();
1991        write!(f, "{dotted}")
1992    }
1993}
1994
1995// Identifier ::= [a-zA-Z_] NO_SPACE [0-9a-zA-Z_]*
1996
1997pub(super) fn is_identifier(input: &str) -> bool {
1998    // TODO Don't use the whole parser for this?
1999    all_consuming(parse_identifier_no_space)
2000        .parse(new_span_with_spec(input, ConnectSpec::latest()))
2001        .is_ok()
2002}
2003
2004fn parse_identifier(input: Span) -> ParseResult<WithRange<String>> {
2005    preceded(spaces_or_comments, parse_identifier_no_space).parse(input)
2006}
2007
2008fn parse_identifier_no_space(input: Span) -> ParseResult<WithRange<String>> {
2009    recognize(pair(
2010        one_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"),
2011        many0(one_of(
2012            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789",
2013        )),
2014    ))
2015    .parse(input)
2016    .map(|(remainder, name)| {
2017        let range = Some(name.location_offset()..remainder.location_offset());
2018        (remainder, WithRange::new(name.to_string(), range))
2019    })
2020}
2021
2022// LitString ::=
2023//   | "'" ("\\'" | [^'])* "'"
2024//   | '"' ('\\"' | [^"])* '"'
2025
2026pub(crate) fn parse_string_literal(input: Span) -> ParseResult<WithRange<String>> {
2027    let input = spaces_or_comments(input)?.0;
2028    let start = input.location_offset();
2029    let mut input_char_indices = input.char_indices();
2030
2031    match input_char_indices.next() {
2032        Some((0, quote @ '\'')) | Some((0, quote @ '"')) => {
2033            let mut escape_next = false;
2034            let mut chars: Vec<char> = Vec::new();
2035            let mut remainder_opt: Option<Span> = None;
2036
2037            for (i, c) in input_char_indices {
2038                if escape_next {
2039                    match c {
2040                        'n' => chars.push('\n'),
2041                        _ => chars.push(c),
2042                    }
2043                    escape_next = false;
2044                    continue;
2045                }
2046                if c == '\\' {
2047                    escape_next = true;
2048                    continue;
2049                }
2050                if c == quote {
2051                    remainder_opt = Some(input.take_from(i + 1));
2052                    break;
2053                }
2054                chars.push(c);
2055            }
2056
2057            remainder_opt
2058                .ok_or_else(|| nom_fail_message(input, "Unterminated string literal"))
2059                .map(|remainder| {
2060                    let range = Some(start..remainder.location_offset());
2061                    (
2062                        remainder,
2063                        WithRange::new(chars.iter().collect::<String>(), range),
2064                    )
2065                })
2066        }
2067
2068        _ => Err(nom_error_message(input, "Not a string literal")),
2069    }
2070}
2071
2072#[derive(Debug, PartialEq, Eq, Clone, Default)]
2073pub(crate) struct MethodArgs {
2074    pub(super) args: Vec<WithRange<LitExpr>>,
2075    pub(super) range: OffsetRange,
2076}
2077
2078impl Ranged for MethodArgs {
2079    fn range(&self) -> OffsetRange {
2080        self.range.clone()
2081    }
2082}
2083
2084// Comma-separated positional arguments for a method, surrounded by parentheses.
2085// When an arrow method is used without arguments, the Option<MethodArgs> for
2086// the PathSelection::Method will be None, so we can safely define MethodArgs
2087// using a Vec<LitExpr> in all cases (possibly empty but never missing).
2088impl MethodArgs {
2089    fn parse(input: Span) -> ParseResult<Self> {
2090        let input = spaces_or_comments(input)?.0;
2091        let (mut input, open_paren) = ranged_span("(").parse(input)?;
2092        input = spaces_or_comments(input)?.0;
2093
2094        let mut args = Vec::new();
2095        if let Ok((remainder, first)) = LitExpr::parse(input.clone()) {
2096            args.push(first);
2097            input = remainder;
2098
2099            while let Ok((remainder, _)) = (spaces_or_comments, char(',')).parse(input.clone()) {
2100                input = spaces_or_comments(remainder)?.0;
2101                if let Ok((remainder, arg)) = LitExpr::parse(input.clone()) {
2102                    args.push(arg);
2103                    input = remainder;
2104                } else {
2105                    break;
2106                }
2107            }
2108        }
2109
2110        input = spaces_or_comments(input.clone())?.0;
2111        let (input, close_paren) = ranged_span(")").parse(input.clone())?;
2112
2113        let range = merge_ranges(open_paren.range(), close_paren.range());
2114        Ok((input, Self { args, range }))
2115    }
2116}
2117
2118#[cfg(test)]
2119mod tests {
2120    use apollo_compiler::collections::IndexMap;
2121    use rstest::rstest;
2122
2123    use super::super::location::strip_ranges::StripRanges;
2124    use super::*;
2125    use crate::assert_debug_snapshot;
2126    use crate::connectors::json_selection::PrettyPrintable;
2127    use crate::connectors::json_selection::SelectionTrie;
2128    use crate::connectors::json_selection::fixtures::Namespace;
2129    use crate::connectors::json_selection::helpers::span_is_all_spaces_or_comments;
2130    use crate::connectors::json_selection::location::new_span;
2131    use crate::selection;
2132
2133    #[test]
2134    fn test_identifier() {
2135        fn check(input: &str, expected_name: &str) {
2136            let (remainder, name) = parse_identifier(new_span(input)).unwrap();
2137            assert!(
2138                span_is_all_spaces_or_comments(remainder.clone()),
2139                "remainder is `{:?}`",
2140                remainder.clone(),
2141            );
2142            assert_eq!(name.as_ref(), expected_name);
2143        }
2144
2145        check("hello", "hello");
2146        check("hello_world", "hello_world");
2147        check("  hello_world ", "hello_world");
2148        check("hello_world_123", "hello_world_123");
2149        check(" hello ", "hello");
2150
2151        fn check_no_space(input: &str, expected_name: &str) {
2152            let name = parse_identifier_no_space(new_span(input)).unwrap().1;
2153            assert_eq!(name.as_ref(), expected_name);
2154        }
2155
2156        check_no_space("oyez", "oyez");
2157        check_no_space("oyez   ", "oyez");
2158
2159        {
2160            let identifier_with_leading_space = new_span("  oyez   ");
2161            assert_eq!(
2162                parse_identifier_no_space(identifier_with_leading_space.clone()),
2163                Err(nom::Err::Error(nom::error::Error::from_error_kind(
2164                    // The parse_identifier_no_space function does not provide a
2165                    // custom error message, since it's only used internally.
2166                    // Testing it directly here is somewhat contrived.
2167                    identifier_with_leading_space.clone(),
2168                    nom::error::ErrorKind::OneOf,
2169                ))),
2170            );
2171        }
2172    }
2173
2174    #[test]
2175    fn test_string_literal() {
2176        fn check(input: &str, expected: &str) {
2177            let (remainder, lit) = parse_string_literal(new_span(input)).unwrap();
2178            assert!(
2179                span_is_all_spaces_or_comments(remainder.clone()),
2180                "remainder is `{:?}`",
2181                remainder.clone(),
2182            );
2183            assert_eq!(lit.as_ref(), expected);
2184        }
2185        check("'hello world'", "hello world");
2186        check("\"hello world\"", "hello world");
2187        check("'hello \"world\"'", "hello \"world\"");
2188        check("\"hello \\\"world\\\"\"", "hello \"world\"");
2189        check("'hello \\'world\\''", "hello 'world'");
2190    }
2191
2192    #[test]
2193    fn test_key() {
2194        fn check(input: &str, expected: &Key) {
2195            let (remainder, key) = Key::parse(new_span(input)).unwrap();
2196            assert!(
2197                span_is_all_spaces_or_comments(remainder.clone()),
2198                "remainder is `{:?}`",
2199                remainder.clone(),
2200            );
2201            assert_eq!(key.as_ref(), expected);
2202        }
2203
2204        check("hello", &Key::field("hello"));
2205        check("'hello'", &Key::quoted("hello"));
2206        check("  hello ", &Key::field("hello"));
2207        check("\"hello\"", &Key::quoted("hello"));
2208        check("  \"hello\" ", &Key::quoted("hello"));
2209    }
2210
2211    #[test]
2212    fn test_alias() {
2213        fn check(input: &str, alias: &str) {
2214            let (remainder, parsed) = Alias::parse(new_span(input)).unwrap();
2215            assert!(
2216                span_is_all_spaces_or_comments(remainder.clone()),
2217                "remainder is `{:?}`",
2218                remainder.clone(),
2219            );
2220            assert_eq!(parsed.name.as_str(), alias);
2221        }
2222
2223        check("hello:", "hello");
2224        check("hello :", "hello");
2225        check("hello : ", "hello");
2226        check("  hello :", "hello");
2227        check("hello: ", "hello");
2228    }
2229
2230    #[test]
2231    fn test_named_selection() {
2232        #[track_caller]
2233        fn assert_result_and_names(input: &str, expected: NamedSelection, names: &[&str]) {
2234            let (remainder, selection) = NamedSelection::parse(new_span(input)).unwrap();
2235            assert!(
2236                span_is_all_spaces_or_comments(remainder.clone()),
2237                "remainder is `{:?}`",
2238                remainder.clone(),
2239            );
2240            let selection = selection.strip_ranges();
2241            assert_eq!(selection, expected);
2242            assert_eq!(selection.names(), names);
2243            assert_eq!(
2244                selection!(input).strip_ranges(),
2245                JSONSelection::named(SubSelection {
2246                    selections: vec![expected],
2247                    ..Default::default()
2248                },),
2249            );
2250        }
2251
2252        assert_result_and_names(
2253            "hello",
2254            NamedSelection::field(None, Key::field("hello").into_with_range(), None),
2255            &["hello"],
2256        );
2257
2258        assert_result_and_names(
2259            "hello { world }",
2260            NamedSelection::field(
2261                None,
2262                Key::field("hello").into_with_range(),
2263                Some(SubSelection {
2264                    selections: vec![NamedSelection::field(
2265                        None,
2266                        Key::field("world").into_with_range(),
2267                        None,
2268                    )],
2269                    ..Default::default()
2270                }),
2271            ),
2272            &["hello"],
2273        );
2274
2275        assert_result_and_names(
2276            "hi: hello",
2277            NamedSelection::field(
2278                Some(Alias::new("hi")),
2279                Key::field("hello").into_with_range(),
2280                None,
2281            ),
2282            &["hi"],
2283        );
2284
2285        assert_result_and_names(
2286            "hi: 'hello world'",
2287            NamedSelection::field(
2288                Some(Alias::new("hi")),
2289                Key::quoted("hello world").into_with_range(),
2290                None,
2291            ),
2292            &["hi"],
2293        );
2294
2295        assert_result_and_names(
2296            "hi: hello { world }",
2297            NamedSelection::field(
2298                Some(Alias::new("hi")),
2299                Key::field("hello").into_with_range(),
2300                Some(SubSelection {
2301                    selections: vec![NamedSelection::field(
2302                        None,
2303                        Key::field("world").into_with_range(),
2304                        None,
2305                    )],
2306                    ..Default::default()
2307                }),
2308            ),
2309            &["hi"],
2310        );
2311
2312        assert_result_and_names(
2313            "hey: hello { world again }",
2314            NamedSelection::field(
2315                Some(Alias::new("hey")),
2316                Key::field("hello").into_with_range(),
2317                Some(SubSelection {
2318                    selections: vec![
2319                        NamedSelection::field(None, Key::field("world").into_with_range(), None),
2320                        NamedSelection::field(None, Key::field("again").into_with_range(), None),
2321                    ],
2322                    ..Default::default()
2323                }),
2324            ),
2325            &["hey"],
2326        );
2327
2328        assert_result_and_names(
2329            "hey: 'hello world' { again }",
2330            NamedSelection::field(
2331                Some(Alias::new("hey")),
2332                Key::quoted("hello world").into_with_range(),
2333                Some(SubSelection {
2334                    selections: vec![NamedSelection::field(
2335                        None,
2336                        Key::field("again").into_with_range(),
2337                        None,
2338                    )],
2339                    ..Default::default()
2340                }),
2341            ),
2342            &["hey"],
2343        );
2344
2345        assert_result_and_names(
2346            "leggo: 'my ego'",
2347            NamedSelection::field(
2348                Some(Alias::new("leggo")),
2349                Key::quoted("my ego").into_with_range(),
2350                None,
2351            ),
2352            &["leggo"],
2353        );
2354
2355        assert_result_and_names(
2356            "'let go': 'my ego'",
2357            NamedSelection::field(
2358                Some(Alias::quoted("let go")),
2359                Key::quoted("my ego").into_with_range(),
2360                None,
2361            ),
2362            &["let go"],
2363        );
2364    }
2365
2366    #[test]
2367    fn test_selection() {
2368        assert_eq!(
2369            selection!("").strip_ranges(),
2370            JSONSelection::named(SubSelection {
2371                selections: vec![],
2372                ..Default::default()
2373            }),
2374        );
2375
2376        assert_eq!(
2377            selection!("   ").strip_ranges(),
2378            JSONSelection::named(SubSelection {
2379                selections: vec![],
2380                ..Default::default()
2381            }),
2382        );
2383
2384        assert_eq!(
2385            selection!("hello").strip_ranges(),
2386            JSONSelection::named(SubSelection {
2387                selections: vec![NamedSelection::field(
2388                    None,
2389                    Key::field("hello").into_with_range(),
2390                    None
2391                )],
2392                ..Default::default()
2393            }),
2394        );
2395
2396        assert_eq!(
2397            selection!("$.hello").strip_ranges(),
2398            JSONSelection::path(PathSelection {
2399                path: PathList::Var(
2400                    KnownVariable::Dollar.into_with_range(),
2401                    PathList::Key(
2402                        Key::field("hello").into_with_range(),
2403                        PathList::Empty.into_with_range()
2404                    )
2405                    .into_with_range(),
2406                )
2407                .into_with_range(),
2408            }),
2409        );
2410
2411        {
2412            let expected = JSONSelection::named(SubSelection {
2413                selections: vec![NamedSelection {
2414                    prefix: NamingPrefix::Alias(Alias::new("hi")),
2415                    path: path_value(PathSelection::from_slice(
2416                        &[
2417                            Key::Field("hello".to_string()),
2418                            Key::Field("world".to_string()),
2419                        ],
2420                        None,
2421                    )),
2422                }],
2423                ..Default::default()
2424            });
2425
2426            assert_eq!(selection!("hi: hello.world").strip_ranges(), expected);
2427            assert_eq!(selection!("hi: hello .world").strip_ranges(), expected);
2428            assert_eq!(selection!("hi:  hello. world").strip_ranges(), expected);
2429            assert_eq!(selection!("hi: hello . world").strip_ranges(), expected);
2430            assert_eq!(selection!("hi: hello.world").strip_ranges(), expected);
2431            assert_eq!(selection!("hi: hello. world").strip_ranges(), expected);
2432            assert_eq!(selection!("hi: hello .world").strip_ranges(), expected);
2433            assert_eq!(selection!("hi: hello . world ").strip_ranges(), expected);
2434        }
2435
2436        {
2437            let expected = JSONSelection::named(SubSelection {
2438                selections: vec![
2439                    NamedSelection::field(None, Key::field("before").into_with_range(), None),
2440                    NamedSelection {
2441                        prefix: NamingPrefix::Alias(Alias::new("hi")),
2442                        path: path_value(PathSelection::from_slice(
2443                            &[
2444                                Key::Field("hello".to_string()),
2445                                Key::Field("world".to_string()),
2446                            ],
2447                            None,
2448                        )),
2449                    },
2450                    NamedSelection::field(None, Key::field("after").into_with_range(), None),
2451                ],
2452                ..Default::default()
2453            });
2454
2455            assert_eq!(
2456                selection!("before hi: hello.world after").strip_ranges(),
2457                expected
2458            );
2459            assert_eq!(
2460                selection!("before hi: hello .world after").strip_ranges(),
2461                expected
2462            );
2463            assert_eq!(
2464                selection!("before hi: hello. world after").strip_ranges(),
2465                expected
2466            );
2467            assert_eq!(
2468                selection!("before hi: hello . world after").strip_ranges(),
2469                expected
2470            );
2471            assert_eq!(
2472                selection!("before hi:  hello.world after").strip_ranges(),
2473                expected
2474            );
2475            assert_eq!(
2476                selection!("before hi: hello .world after").strip_ranges(),
2477                expected
2478            );
2479            assert_eq!(
2480                selection!("before hi: hello. world after").strip_ranges(),
2481                expected
2482            );
2483            assert_eq!(
2484                selection!("before hi: hello . world after").strip_ranges(),
2485                expected
2486            );
2487        }
2488
2489        {
2490            let expected = JSONSelection::named(SubSelection {
2491                selections: vec![
2492                    NamedSelection::field(None, Key::field("before").into_with_range(), None),
2493                    NamedSelection {
2494                        prefix: NamingPrefix::Alias(Alias::new("hi")),
2495                        path: path_value(PathSelection::from_slice(
2496                            &[
2497                                Key::Field("hello".to_string()),
2498                                Key::Field("world".to_string()),
2499                            ],
2500                            Some(SubSelection {
2501                                selections: vec![
2502                                    NamedSelection::field(
2503                                        None,
2504                                        Key::field("nested").into_with_range(),
2505                                        None,
2506                                    ),
2507                                    NamedSelection::field(
2508                                        None,
2509                                        Key::field("names").into_with_range(),
2510                                        None,
2511                                    ),
2512                                ],
2513                                ..Default::default()
2514                            }),
2515                        )),
2516                    },
2517                    NamedSelection::field(None, Key::field("after").into_with_range(), None),
2518                ],
2519                ..Default::default()
2520            });
2521
2522            assert_eq!(
2523                selection!("before hi: hello.world { nested names } after").strip_ranges(),
2524                expected
2525            );
2526            assert_eq!(
2527                selection!("before hi:hello.world{nested names}after").strip_ranges(),
2528                expected
2529            );
2530            assert_eq!(
2531                selection!(" before hi : hello . world { nested names } after ").strip_ranges(),
2532                expected
2533            );
2534        }
2535
2536        assert_debug_snapshot!(selection!(
2537            "
2538            # Comments are supported because we parse them as whitespace
2539            topLevelAlias: topLevelField {
2540                identifier: 'property name with spaces'
2541                'unaliased non-identifier property'
2542                'non-identifier alias': identifier
2543
2544                # This extracts the value located at the given path and applies a
2545                # selection set to it before renaming the result to pathSelection
2546                pathSelection: some.nested.path {
2547                    still: yet
2548                    more
2549                    properties
2550                }
2551
2552                # An aliased SubSelection of fields nests the fields together
2553                # under the given alias
2554                siblingGroup: { brother sister }
2555            }"
2556        ));
2557    }
2558
2559    #[track_caller]
2560    fn check_path_selection(spec: ConnectSpec, input: &str, expected: PathSelection) {
2561        let (remainder, path_selection) =
2562            PathSelection::parse(new_span_with_spec(input, spec)).unwrap();
2563        assert!(
2564            span_is_all_spaces_or_comments(remainder.clone()),
2565            "remainder is `{:?}`",
2566            remainder.clone(),
2567        );
2568        let path_without_ranges = path_selection.strip_ranges();
2569        assert_eq!(&path_without_ranges, &expected);
2570        assert_eq!(
2571            selection!(input, spec).strip_ranges(),
2572            JSONSelection {
2573                inner: TopLevelSelection::Value(WithRange::new(
2574                    LitExpr::Path(path_without_ranges),
2575                    None,
2576                )),
2577                spec,
2578            },
2579        );
2580    }
2581
2582    #[rstest]
2583    #[case::v0_2(ConnectSpec::V0_2)]
2584    #[case::v0_3(ConnectSpec::V0_3)]
2585    #[case::v0_4(ConnectSpec::V0_4)]
2586    fn test_path_selection(#[case] spec: ConnectSpec) {
2587        check_path_selection(
2588            spec,
2589            "$.hello",
2590            PathSelection {
2591                path: PathList::Var(
2592                    KnownVariable::Dollar.into_with_range(),
2593                    PathList::Key(
2594                        Key::field("hello").into_with_range(),
2595                        PathList::Empty.into_with_range(),
2596                    )
2597                    .into_with_range(),
2598                )
2599                .into_with_range(),
2600            },
2601        );
2602
2603        {
2604            let expected = PathSelection {
2605                path: PathList::Var(
2606                    KnownVariable::Dollar.into_with_range(),
2607                    PathList::Key(
2608                        Key::field("hello").into_with_range(),
2609                        PathList::Key(
2610                            Key::field("world").into_with_range(),
2611                            PathList::Empty.into_with_range(),
2612                        )
2613                        .into_with_range(),
2614                    )
2615                    .into_with_range(),
2616                )
2617                .into_with_range(),
2618            };
2619            check_path_selection(spec, "$.hello.world", expected.clone());
2620            check_path_selection(spec, "$.hello .world", expected.clone());
2621            check_path_selection(spec, "$.hello. world", expected.clone());
2622            check_path_selection(spec, "$.hello . world", expected.clone());
2623            check_path_selection(spec, "$ . hello . world", expected.clone());
2624            check_path_selection(spec, " $ . hello . world ", expected);
2625        }
2626
2627        {
2628            let expected = PathSelection::from_slice(
2629                &[
2630                    Key::Field("hello".to_string()),
2631                    Key::Field("world".to_string()),
2632                ],
2633                None,
2634            );
2635            check_path_selection(spec, "hello.world", expected.clone());
2636            check_path_selection(spec, "hello .world", expected.clone());
2637            check_path_selection(spec, "hello. world", expected.clone());
2638            check_path_selection(spec, "hello . world", expected.clone());
2639            check_path_selection(spec, " hello . world ", expected);
2640        }
2641
2642        {
2643            let expected = PathSelection::from_slice(
2644                &[
2645                    Key::Field("hello".to_string()),
2646                    Key::Field("world".to_string()),
2647                ],
2648                Some(SubSelection {
2649                    selections: vec![NamedSelection::field(
2650                        None,
2651                        Key::field("hello").into_with_range(),
2652                        None,
2653                    )],
2654                    ..Default::default()
2655                }),
2656            );
2657            check_path_selection(spec, "hello.world{hello}", expected.clone());
2658            check_path_selection(spec, "hello.world { hello }", expected.clone());
2659            check_path_selection(spec, "hello .world { hello }", expected.clone());
2660            check_path_selection(spec, "hello. world { hello }", expected.clone());
2661            check_path_selection(spec, "hello . world { hello }", expected.clone());
2662            check_path_selection(spec, " hello . world { hello } ", expected);
2663        }
2664
2665        {
2666            let expected = PathSelection::from_slice(
2667                &[
2668                    Key::Field("nested".to_string()),
2669                    Key::Quoted("string literal".to_string()),
2670                    Key::Quoted("property".to_string()),
2671                    Key::Field("name".to_string()),
2672                ],
2673                None,
2674            );
2675            check_path_selection(
2676                spec,
2677                "nested.'string literal'.\"property\".name",
2678                expected.clone(),
2679            );
2680            check_path_selection(
2681                spec,
2682                "nested. 'string literal'.\"property\".name",
2683                expected.clone(),
2684            );
2685            check_path_selection(
2686                spec,
2687                "nested.'string literal'. \"property\".name",
2688                expected.clone(),
2689            );
2690            check_path_selection(
2691                spec,
2692                "nested.'string literal'.\"property\" .name",
2693                expected.clone(),
2694            );
2695            check_path_selection(
2696                spec,
2697                "nested.'string literal'.\"property\". name",
2698                expected.clone(),
2699            );
2700            check_path_selection(
2701                spec,
2702                " nested . 'string literal' . \"property\" . name ",
2703                expected,
2704            );
2705        }
2706
2707        {
2708            // In v0.4, `leggo: 'my ego'` binds `leggo` to the string literal
2709            // `"my ego"` (LitExpr::String), matching the fully unified
2710            // SubSelection/LitObject grammar where JSON literal heads always
2711            // win over field-key interpretation. The AST stores the bare
2712            // `LitExpr::String` directly — no `PathList::Expr` wrap, since
2713            // that node is reserved for source `$(...)`. To look up a field
2714            // called `my ego`, users write `leggo: $.'my ego'` instead.
2715            let leggo_selection = if matches!(spec, ConnectSpec::V0_4) {
2716                NamedSelection {
2717                    prefix: NamingPrefix::Alias(Alias::new("leggo")),
2718                    path: WithRange::new(LitExpr::String("my ego".to_string()), None),
2719                }
2720            } else {
2721                NamedSelection::field(
2722                    Some(Alias::new("leggo")),
2723                    Key::quoted("my ego").into_with_range(),
2724                    None,
2725                )
2726            };
2727            let expected = PathSelection::from_slice(
2728                &[
2729                    Key::Field("nested".to_string()),
2730                    Key::Quoted("string literal".to_string()),
2731                ],
2732                Some(SubSelection {
2733                    selections: vec![leggo_selection],
2734                    ..Default::default()
2735                }),
2736            );
2737
2738            check_path_selection(
2739                spec,
2740                "nested.'string literal' { leggo: 'my ego' }",
2741                expected.clone(),
2742            );
2743
2744            check_path_selection(
2745                spec,
2746                " nested . 'string literal' { leggo : 'my ego' } ",
2747                expected.clone(),
2748            );
2749
2750            check_path_selection(
2751                spec,
2752                "nested. 'string literal' { leggo: 'my ego' }",
2753                expected.clone(),
2754            );
2755
2756            check_path_selection(
2757                spec,
2758                "nested . 'string literal' { leggo: 'my ego' }",
2759                expected.clone(),
2760            );
2761            check_path_selection(
2762                spec,
2763                " nested . \"string literal\" { leggo: 'my ego' } ",
2764                expected,
2765            );
2766        }
2767
2768        {
2769            let expected = PathSelection {
2770                path: PathList::Var(
2771                    KnownVariable::Dollar.into_with_range(),
2772                    PathList::Key(
2773                        Key::field("results").into_with_range(),
2774                        PathList::Selection(SubSelection {
2775                            selections: vec![NamedSelection::field(
2776                                None,
2777                                Key::quoted("quoted without alias").into_with_range(),
2778                                Some(SubSelection {
2779                                    selections: vec![
2780                                        NamedSelection::field(
2781                                            None,
2782                                            Key::field("id").into_with_range(),
2783                                            None,
2784                                        ),
2785                                        NamedSelection::field(
2786                                            None,
2787                                            Key::quoted("n a m e").into_with_range(),
2788                                            None,
2789                                        ),
2790                                    ],
2791                                    ..Default::default()
2792                                }),
2793                            )],
2794                            ..Default::default()
2795                        })
2796                        .into_with_range(),
2797                    )
2798                    .into_with_range(),
2799                )
2800                .into_with_range(),
2801            };
2802            check_path_selection(
2803                spec,
2804                "$.results{'quoted without alias'{id'n a m e'}}",
2805                expected.clone(),
2806            );
2807            check_path_selection(
2808                spec,
2809                " $ . results { 'quoted without alias' { id 'n a m e' } } ",
2810                expected,
2811            );
2812        }
2813
2814        {
2815            // A quoted-string token immediately followed by `{ ... }` is
2816            // *not* a LitPath in v0.4: per the grammar (NonEmptyPathTail
2817            // excludes SubSelection), and per the explicit disambiguation
2818            // rule documented in the README's "Literals followed by a
2819            // SubSelection" section, such a token is reinterpreted as the
2820            // `Key` of a `KeyPath` whose trailing `SubSelection` is the
2821            // `{ ... }`. The resulting AST matches v0.3 verbatim.
2822            let quoted_with_alias_value: WithRange<LitExpr> = path_value(PathSelection {
2823                path: WithRange::new(
2824                    PathList::Key(
2825                        Key::quoted("quoted with alias").into_with_range(),
2826                        WithRange::new(
2827                            PathList::Selection(SubSelection {
2828                                selections: vec![
2829                                    NamedSelection::field(
2830                                        None,
2831                                        Key::field("id").into_with_range(),
2832                                        None,
2833                                    ),
2834                                    NamedSelection::field(
2835                                        Some(Alias::quoted("n a m e")),
2836                                        Key::field("name").into_with_range(),
2837                                        None,
2838                                    ),
2839                                ],
2840                                ..Default::default()
2841                            }),
2842                            None,
2843                        ),
2844                    ),
2845                    None,
2846                ),
2847            });
2848            let expected = PathSelection {
2849                path: PathList::Var(
2850                    KnownVariable::Dollar.into_with_range(),
2851                    PathList::Key(
2852                        Key::field("results").into_with_range(),
2853                        PathList::Selection(SubSelection {
2854                            selections: vec![NamedSelection {
2855                                prefix: NamingPrefix::Alias(Alias::quoted("non-identifier alias")),
2856                                path: quoted_with_alias_value,
2857                            }],
2858                            ..Default::default()
2859                        })
2860                        .into_with_range(),
2861                    )
2862                    .into_with_range(),
2863                )
2864                .into_with_range(),
2865            };
2866            check_path_selection(
2867                spec,
2868                "$.results{'non-identifier alias':'quoted with alias'{id'n a m e':name}}",
2869                expected.clone(),
2870            );
2871            check_path_selection(
2872                spec,
2873                " $ . results { 'non-identifier alias' : 'quoted with alias' { id 'n a m e': name } } ",
2874                expected,
2875            );
2876        }
2877    }
2878
2879    #[rstest]
2880    #[case::v0_2(ConnectSpec::V0_2)]
2881    #[case::v0_3(ConnectSpec::V0_3)]
2882    #[case::v0_4(ConnectSpec::V0_4)]
2883    fn test_path_selection_vars(#[case] spec: ConnectSpec) {
2884        check_path_selection(
2885            spec,
2886            "$this",
2887            PathSelection {
2888                path: PathList::Var(
2889                    KnownVariable::External(Namespace::This.to_string()).into_with_range(),
2890                    PathList::Empty.into_with_range(),
2891                )
2892                .into_with_range(),
2893            },
2894        );
2895
2896        check_path_selection(
2897            spec,
2898            "$",
2899            PathSelection {
2900                path: PathList::Var(
2901                    KnownVariable::Dollar.into_with_range(),
2902                    PathList::Empty.into_with_range(),
2903                )
2904                .into_with_range(),
2905            },
2906        );
2907
2908        check_path_selection(
2909            spec,
2910            "$this { hello }",
2911            PathSelection {
2912                path: PathList::Var(
2913                    KnownVariable::External(Namespace::This.to_string()).into_with_range(),
2914                    PathList::Selection(SubSelection {
2915                        selections: vec![NamedSelection::field(
2916                            None,
2917                            Key::field("hello").into_with_range(),
2918                            None,
2919                        )],
2920                        ..Default::default()
2921                    })
2922                    .into_with_range(),
2923                )
2924                .into_with_range(),
2925            },
2926        );
2927
2928        check_path_selection(
2929            spec,
2930            "$ { hello }",
2931            PathSelection {
2932                path: PathList::Var(
2933                    KnownVariable::Dollar.into_with_range(),
2934                    PathList::Selection(SubSelection {
2935                        selections: vec![NamedSelection::field(
2936                            None,
2937                            Key::field("hello").into_with_range(),
2938                            None,
2939                        )],
2940                        ..Default::default()
2941                    })
2942                    .into_with_range(),
2943                )
2944                .into_with_range(),
2945            },
2946        );
2947
2948        check_path_selection(
2949            spec,
2950            "$this { before alias: $args.arg after }",
2951            PathList::Var(
2952                KnownVariable::External(Namespace::This.to_string()).into_with_range(),
2953                PathList::Selection(SubSelection {
2954                    selections: vec![
2955                        NamedSelection::field(None, Key::field("before").into_with_range(), None),
2956                        NamedSelection {
2957                            prefix: NamingPrefix::Alias(Alias::new("alias")),
2958                            path: path_value(PathSelection {
2959                                path: PathList::Var(
2960                                    KnownVariable::External(Namespace::Args.to_string())
2961                                        .into_with_range(),
2962                                    PathList::Key(
2963                                        Key::field("arg").into_with_range(),
2964                                        PathList::Empty.into_with_range(),
2965                                    )
2966                                    .into_with_range(),
2967                                )
2968                                .into_with_range(),
2969                            }),
2970                        },
2971                        NamedSelection::field(None, Key::field("after").into_with_range(), None),
2972                    ],
2973                    ..Default::default()
2974                })
2975                .into_with_range(),
2976            )
2977            .into(),
2978        );
2979
2980        check_path_selection(
2981            spec,
2982            "$.nested { key injected: $args.arg }",
2983            PathSelection {
2984                path: PathList::Var(
2985                    KnownVariable::Dollar.into_with_range(),
2986                    PathList::Key(
2987                        Key::field("nested").into_with_range(),
2988                        PathList::Selection(SubSelection {
2989                            selections: vec![
2990                                NamedSelection::field(
2991                                    None,
2992                                    Key::field("key").into_with_range(),
2993                                    None,
2994                                ),
2995                                NamedSelection {
2996                                    prefix: NamingPrefix::Alias(Alias::new("injected")),
2997                                    path: path_value(PathSelection {
2998                                        path: PathList::Var(
2999                                            KnownVariable::External(Namespace::Args.to_string())
3000                                                .into_with_range(),
3001                                            PathList::Key(
3002                                                Key::field("arg").into_with_range(),
3003                                                PathList::Empty.into_with_range(),
3004                                            )
3005                                            .into_with_range(),
3006                                        )
3007                                        .into_with_range(),
3008                                    }),
3009                                },
3010                            ],
3011                            ..Default::default()
3012                        })
3013                        .into_with_range(),
3014                    )
3015                    .into_with_range(),
3016                )
3017                .into_with_range(),
3018            },
3019        );
3020
3021        check_path_selection(
3022            spec,
3023            "$args.a.b.c",
3024            PathSelection {
3025                path: PathList::Var(
3026                    KnownVariable::External(Namespace::Args.to_string()).into_with_range(),
3027                    PathList::from_slice(
3028                        &[
3029                            Key::Field("a".to_string()),
3030                            Key::Field("b".to_string()),
3031                            Key::Field("c".to_string()),
3032                        ],
3033                        None,
3034                    )
3035                    .into_with_range(),
3036                )
3037                .into_with_range(),
3038            },
3039        );
3040
3041        check_path_selection(
3042            spec,
3043            "root.x.y.z",
3044            PathSelection::from_slice(
3045                &[
3046                    Key::Field("root".to_string()),
3047                    Key::Field("x".to_string()),
3048                    Key::Field("y".to_string()),
3049                    Key::Field("z".to_string()),
3050                ],
3051                None,
3052            ),
3053        );
3054
3055        check_path_selection(
3056            spec,
3057            "$.data",
3058            PathSelection {
3059                path: PathList::Var(
3060                    KnownVariable::Dollar.into_with_range(),
3061                    PathList::Key(
3062                        Key::field("data").into_with_range(),
3063                        PathList::Empty.into_with_range(),
3064                    )
3065                    .into_with_range(),
3066                )
3067                .into_with_range(),
3068            },
3069        );
3070
3071        check_path_selection(
3072            spec,
3073            "$.data.'quoted property'.nested",
3074            PathSelection {
3075                path: PathList::Var(
3076                    KnownVariable::Dollar.into_with_range(),
3077                    PathList::Key(
3078                        Key::field("data").into_with_range(),
3079                        PathList::Key(
3080                            Key::quoted("quoted property").into_with_range(),
3081                            PathList::Key(
3082                                Key::field("nested").into_with_range(),
3083                                PathList::Empty.into_with_range(),
3084                            )
3085                            .into_with_range(),
3086                        )
3087                        .into_with_range(),
3088                    )
3089                    .into_with_range(),
3090                )
3091                .into_with_range(),
3092            },
3093        );
3094
3095        #[track_caller]
3096        fn check_path_parse_error(
3097            spec: ConnectSpec,
3098            input: &str,
3099            expected_offset: usize,
3100            expected_message: impl Into<String>,
3101        ) {
3102            let expected_message: String = expected_message.into();
3103            match PathSelection::parse(new_span_with_spec(input, spec)) {
3104                Ok((remainder, path)) => {
3105                    panic!(
3106                        "Expected error at offset {expected_offset} with message '{expected_message}', but got path {path:?} and remainder {remainder:?}",
3107                    );
3108                }
3109                Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
3110                    assert_eq!(&input[expected_offset..], *e.input.fragment());
3111                    // The PartialEq implementation for LocatedSpan
3112                    // unfortunately ignores span.extra, so we have to check
3113                    // e.input.extra manually.
3114                    assert_eq!(
3115                        e.input.extra,
3116                        SpanExtra {
3117                            spec,
3118                            errors: vec![(expected_message, expected_offset)],
3119                            local_vars: Vec::new(),
3120                        }
3121                    );
3122                }
3123                Err(e) => {
3124                    panic!("Unexpected error {e:?}");
3125                }
3126            }
3127        }
3128
3129        // Single-key path ambiguity is only enforced in V0_1/V0_2; in V0_3+
3130        // the ambiguity was resolved by unifying NamedSelection variants.
3131        let single_key_path_error_message =
3132            "Single-key path must be prefixed with $. to avoid ambiguity with field name";
3133        check_path_parse_error(
3134            ConnectSpec::V0_2,
3135            new_span("naked").fragment(),
3136            0,
3137            single_key_path_error_message,
3138        );
3139        check_path_parse_error(
3140            ConnectSpec::V0_2,
3141            new_span("naked { hi }").fragment(),
3142            0,
3143            single_key_path_error_message,
3144        );
3145        check_path_parse_error(
3146            ConnectSpec::V0_2,
3147            new_span("  naked { hi }").fragment(),
3148            2,
3149            single_key_path_error_message,
3150        );
3151
3152        let path_key_ambiguity_error_message =
3153            "Path selection . must be followed by key (identifier or quoted string literal)";
3154        check_path_parse_error(
3155            ConnectSpec::latest(),
3156            new_span("valid.$invalid").fragment(),
3157            5,
3158            path_key_ambiguity_error_message,
3159        );
3160        check_path_parse_error(
3161            ConnectSpec::latest(),
3162            new_span("  valid.$invalid").fragment(),
3163            7,
3164            path_key_ambiguity_error_message,
3165        );
3166        check_path_parse_error(
3167            ConnectSpec::latest(),
3168            new_span("  valid . $invalid").fragment(),
3169            8,
3170            path_key_ambiguity_error_message,
3171        );
3172
3173        assert_eq!(
3174            selection!("$").strip_ranges(),
3175            JSONSelection::path(PathSelection {
3176                path: PathList::Var(
3177                    KnownVariable::Dollar.into_with_range(),
3178                    PathList::Empty.into_with_range()
3179                )
3180                .into_with_range(),
3181            }),
3182        );
3183
3184        assert_eq!(
3185            selection!("$this").strip_ranges(),
3186            JSONSelection::path(PathSelection {
3187                path: PathList::Var(
3188                    KnownVariable::External(Namespace::This.to_string()).into_with_range(),
3189                    PathList::Empty.into_with_range()
3190                )
3191                .into_with_range(),
3192            }),
3193        );
3194
3195        assert_eq!(
3196            selection!("value: $ a { b c }").strip_ranges(),
3197            JSONSelection::named(SubSelection {
3198                selections: vec![
3199                    NamedSelection {
3200                        prefix: NamingPrefix::Alias(Alias::new("value")),
3201                        path: path_value(PathSelection {
3202                            path: PathList::Var(
3203                                KnownVariable::Dollar.into_with_range(),
3204                                PathList::Empty.into_with_range()
3205                            )
3206                            .into_with_range(),
3207                        }),
3208                    },
3209                    NamedSelection::field(
3210                        None,
3211                        Key::field("a").into_with_range(),
3212                        Some(SubSelection {
3213                            selections: vec![
3214                                NamedSelection::field(
3215                                    None,
3216                                    Key::field("b").into_with_range(),
3217                                    None
3218                                ),
3219                                NamedSelection::field(
3220                                    None,
3221                                    Key::field("c").into_with_range(),
3222                                    None
3223                                ),
3224                            ],
3225                            ..Default::default()
3226                        }),
3227                    ),
3228                ],
3229                ..Default::default()
3230            }),
3231        );
3232        assert_eq!(
3233            selection!("value: $this { b c }").strip_ranges(),
3234            JSONSelection::named(SubSelection {
3235                selections: vec![NamedSelection {
3236                    prefix: NamingPrefix::Alias(Alias::new("value")),
3237                    path: path_value(PathSelection {
3238                        path: PathList::Var(
3239                            KnownVariable::External(Namespace::This.to_string()).into_with_range(),
3240                            PathList::Selection(SubSelection {
3241                                selections: vec![
3242                                    NamedSelection::field(
3243                                        None,
3244                                        Key::field("b").into_with_range(),
3245                                        None
3246                                    ),
3247                                    NamedSelection::field(
3248                                        None,
3249                                        Key::field("c").into_with_range(),
3250                                        None
3251                                    ),
3252                                ],
3253                                ..Default::default()
3254                            })
3255                            .into_with_range(),
3256                        )
3257                        .into_with_range(),
3258                    }),
3259                }],
3260                ..Default::default()
3261            }),
3262        );
3263    }
3264
3265    #[test]
3266    fn test_error_snapshots_v0_2() {
3267        let spec = ConnectSpec::V0_2;
3268
3269        // The .data shorthand is no longer allowed, since it can be mistakenly
3270        // parsed as a continuation of a previous selection. Instead, use $.data
3271        // to achieve the same effect without ambiguity.
3272        assert_debug_snapshot!(JSONSelection::parse_with_spec(".data", spec));
3273
3274        // If you want to mix a path selection with other named selections, the
3275        // path selection must have a trailing subselection, to enforce that it
3276        // returns an object with statically known keys, or be inlined/spread
3277        // with a ... token.
3278        assert_debug_snapshot!(JSONSelection::parse_with_spec("id $.object", spec));
3279    }
3280
3281    #[test]
3282    fn test_error_snapshots_v0_3() {
3283        let spec = ConnectSpec::V0_3;
3284
3285        // The .data shorthand is no longer allowed, since it can be mistakenly
3286        // parsed as a continuation of a previous selection. Instead, use $.data
3287        // to achieve the same effect without ambiguity.
3288        assert_debug_snapshot!(JSONSelection::parse_with_spec(".data", spec));
3289
3290        // If you want to mix a path selection with other named selections, the
3291        // path selection must have a trailing subselection, to enforce that it
3292        // returns an object with statically known keys, or be inlined/spread
3293        // with a ... token.
3294        assert_debug_snapshot!(JSONSelection::parse_with_spec("id $.object", spec));
3295    }
3296
3297    #[test]
3298    fn test_error_snapshots_v0_4() {
3299        let spec = ConnectSpec::V0_4;
3300
3301        // When this assertion fails, don't panic, but it's time to decide how
3302        // the next-next version should behave in these error cases (possibly
3303        // exactly the same).
3304        assert_eq!(spec, ConnectSpec::next());
3305
3306        // The .data shorthand is no longer allowed, since it can be mistakenly
3307        // parsed as a continuation of a previous selection. Instead, use $.data
3308        // to achieve the same effect without ambiguity.
3309        assert_debug_snapshot!(JSONSelection::parse_with_spec(".data", spec));
3310
3311        // If you want to mix a path selection with other named selections, the
3312        // path selection must have a trailing subselection, to enforce that it
3313        // returns an object with statically known keys, or be inlined/spread
3314        // with a ... token.
3315        assert_debug_snapshot!(JSONSelection::parse_with_spec("id $.object", spec));
3316    }
3317
3318    #[rstest]
3319    #[case::v0_2(ConnectSpec::V0_2)]
3320    #[case::v0_3(ConnectSpec::V0_3)]
3321    #[case::v0_4(ConnectSpec::V0_4)]
3322    fn test_path_selection_at(#[case] spec: ConnectSpec) {
3323        check_path_selection(
3324            spec,
3325            "@",
3326            PathSelection {
3327                path: PathList::Var(
3328                    KnownVariable::AtSign.into_with_range(),
3329                    PathList::Empty.into_with_range(),
3330                )
3331                .into_with_range(),
3332            },
3333        );
3334
3335        check_path_selection(
3336            spec,
3337            "@.a.b.c",
3338            PathSelection {
3339                path: PathList::Var(
3340                    KnownVariable::AtSign.into_with_range(),
3341                    PathList::from_slice(
3342                        &[
3343                            Key::Field("a".to_string()),
3344                            Key::Field("b".to_string()),
3345                            Key::Field("c".to_string()),
3346                        ],
3347                        None,
3348                    )
3349                    .into_with_range(),
3350                )
3351                .into_with_range(),
3352            },
3353        );
3354
3355        check_path_selection(
3356            spec,
3357            "@.items->first",
3358            PathSelection {
3359                path: PathList::Var(
3360                    KnownVariable::AtSign.into_with_range(),
3361                    PathList::Key(
3362                        Key::field("items").into_with_range(),
3363                        PathList::Method(
3364                            WithRange::new("first".to_string(), None),
3365                            None,
3366                            PathList::Empty.into_with_range(),
3367                        )
3368                        .into_with_range(),
3369                    )
3370                    .into_with_range(),
3371                )
3372                .into_with_range(),
3373            },
3374        );
3375    }
3376
3377    #[rstest]
3378    #[case::v0_2(ConnectSpec::V0_2)]
3379    #[case::v0_3(ConnectSpec::V0_3)]
3380    #[case::v0_4(ConnectSpec::V0_4)]
3381    fn test_expr_path_selections(#[case] spec: ConnectSpec) {
3382        fn check_simple_lit_expr(spec: ConnectSpec, input: &str, expected: LitExpr) {
3383            check_path_selection(
3384                spec,
3385                input,
3386                PathSelection {
3387                    path: PathList::Expr(
3388                        expected.into_with_range(),
3389                        PathList::Empty.into_with_range(),
3390                    )
3391                    .into_with_range(),
3392                },
3393            );
3394        }
3395
3396        check_simple_lit_expr(spec, "$(null)", LitExpr::Null);
3397
3398        check_simple_lit_expr(spec, "$(true)", LitExpr::Bool(true));
3399        check_simple_lit_expr(spec, "$(false)", LitExpr::Bool(false));
3400
3401        check_simple_lit_expr(
3402            spec,
3403            "$(1234)",
3404            LitExpr::Number("1234".parse().expect("serde_json::Number parse error")),
3405        );
3406        check_simple_lit_expr(
3407            spec,
3408            "$(1234.5678)",
3409            LitExpr::Number("1234.5678".parse().expect("serde_json::Number parse error")),
3410        );
3411
3412        check_simple_lit_expr(
3413            spec,
3414            "$('hello world')",
3415            LitExpr::String("hello world".to_string()),
3416        );
3417        check_simple_lit_expr(
3418            spec,
3419            "$(\"hello world\")",
3420            LitExpr::String("hello world".to_string()),
3421        );
3422        check_simple_lit_expr(
3423            spec,
3424            "$(\"hello \\\"world\\\"\")",
3425            LitExpr::String("hello \"world\"".to_string()),
3426        );
3427
3428        check_simple_lit_expr(
3429            spec,
3430            "$([1, 2, 3])",
3431            LitExpr::Array(
3432                vec!["1".parse(), "2".parse(), "3".parse()]
3433                    .into_iter()
3434                    .map(|n| {
3435                        LitExpr::Number(n.expect("serde_json::Number parse error"))
3436                            .into_with_range()
3437                    })
3438                    .collect(),
3439            ),
3440        );
3441
3442        // v0.1–v0.3 use the legacy object literal grammar (Key: LitExpr).
3443        // v0.4 unifies the body with SubSelection, where values must be
3444        // PathSelections — literal values like `1` require `$(1)` wrapping,
3445        // which is covered by dedicated v0.4 tests (see test_shorthand_object_property).
3446        if matches!(
3447            spec,
3448            ConnectSpec::V0_1 | ConnectSpec::V0_2 | ConnectSpec::V0_3
3449        ) {
3450            check_simple_lit_expr(spec, "$({})", LitExpr::LegacyObject(IndexMap::default()));
3451            check_simple_lit_expr(
3452                spec,
3453                "$({ a: 1, b: 2, c: 3 })",
3454                LitExpr::LegacyObject({
3455                    let mut map = IndexMap::default();
3456                    for (key, value) in &[("a", "1"), ("b", "2"), ("c", "3")] {
3457                        map.insert(
3458                            Key::field(key).into_with_range(),
3459                            LitExpr::Number(value.parse().expect("serde_json::Number parse error"))
3460                                .into_with_range(),
3461                        );
3462                    }
3463                    map
3464                }),
3465            );
3466        }
3467    }
3468
3469    #[test]
3470    fn test_path_expr_with_spaces_v0_2() {
3471        assert_debug_snapshot!(selection!(
3472            " suffix : results -> slice ( $( - 1 ) -> mul ( $args . suffixLength ) ) ",
3473            // Snapshot tests can be brittle when used with (multiple) #[rstest]
3474            // cases, since the filenames of the snapshots do not always take
3475            // into account the differences between the cases, so we hard-code
3476            // the ConnectSpec in tests like this.
3477            ConnectSpec::V0_2
3478        ));
3479    }
3480
3481    #[test]
3482    fn test_path_expr_with_spaces_v0_3() {
3483        assert_debug_snapshot!(selection!(
3484            " suffix : results -> slice ( $( - 1 ) -> mul ( $args . suffixLength ) ) ",
3485            ConnectSpec::V0_3
3486        ));
3487    }
3488
3489    #[rstest]
3490    #[case::v0_2(ConnectSpec::V0_2)]
3491    #[case::v0_3(ConnectSpec::V0_3)]
3492    #[case::v0_4(ConnectSpec::V0_4)]
3493    fn test_path_methods(#[case] spec: ConnectSpec) {
3494        check_path_selection(
3495            spec,
3496            "data.x->or(data.y)",
3497            PathSelection {
3498                path: PathList::Key(
3499                    Key::field("data").into_with_range(),
3500                    PathList::Key(
3501                        Key::field("x").into_with_range(),
3502                        PathList::Method(
3503                            WithRange::new("or".to_string(), None),
3504                            Some(MethodArgs {
3505                                args: vec![
3506                                    LitExpr::Path(PathSelection::from_slice(
3507                                        &[Key::field("data"), Key::field("y")],
3508                                        None,
3509                                    ))
3510                                    .into_with_range(),
3511                                ],
3512                                ..Default::default()
3513                            }),
3514                            PathList::Empty.into_with_range(),
3515                        )
3516                        .into_with_range(),
3517                    )
3518                    .into_with_range(),
3519                )
3520                .into_with_range(),
3521            },
3522        );
3523
3524        {
3525            fn make_dollar_key_expr(key: &str) -> WithRange<LitExpr> {
3526                WithRange::new(
3527                    LitExpr::Path(PathSelection {
3528                        path: PathList::Var(
3529                            KnownVariable::Dollar.into_with_range(),
3530                            PathList::Key(
3531                                Key::field(key).into_with_range(),
3532                                PathList::Empty.into_with_range(),
3533                            )
3534                            .into_with_range(),
3535                        )
3536                        .into_with_range(),
3537                    }),
3538                    None,
3539                )
3540            }
3541
3542            let expected = PathSelection {
3543                path: PathList::Key(
3544                    Key::field("data").into_with_range(),
3545                    PathList::Method(
3546                        WithRange::new("query".to_string(), None),
3547                        Some(MethodArgs {
3548                            args: vec![
3549                                make_dollar_key_expr("a"),
3550                                make_dollar_key_expr("b"),
3551                                make_dollar_key_expr("c"),
3552                            ],
3553                            ..Default::default()
3554                        }),
3555                        PathList::Empty.into_with_range(),
3556                    )
3557                    .into_with_range(),
3558                )
3559                .into_with_range(),
3560            };
3561            check_path_selection(spec, "data->query($.a, $.b, $.c)", expected.clone());
3562            check_path_selection(spec, "data->query($.a, $.b, $.c )", expected.clone());
3563            check_path_selection(spec, "data->query($.a, $.b, $.c,)", expected.clone());
3564            check_path_selection(spec, "data->query($.a, $.b, $.c ,)", expected.clone());
3565            check_path_selection(spec, "data->query($.a, $.b, $.c , )", expected);
3566        }
3567
3568        {
3569            let expected = PathSelection {
3570                path: PathList::Key(
3571                    Key::field("data").into_with_range(),
3572                    PathList::Key(
3573                        Key::field("x").into_with_range(),
3574                        PathList::Method(
3575                            WithRange::new("concat".to_string(), None),
3576                            Some(MethodArgs {
3577                                args: vec![
3578                                    LitExpr::Array(vec![
3579                                        LitExpr::Path(PathSelection::from_slice(
3580                                            &[Key::field("data"), Key::field("y")],
3581                                            None,
3582                                        ))
3583                                        .into_with_range(),
3584                                        LitExpr::Path(PathSelection::from_slice(
3585                                            &[Key::field("data"), Key::field("z")],
3586                                            None,
3587                                        ))
3588                                        .into_with_range(),
3589                                    ])
3590                                    .into_with_range(),
3591                                ],
3592                                ..Default::default()
3593                            }),
3594                            PathList::Empty.into_with_range(),
3595                        )
3596                        .into_with_range(),
3597                    )
3598                    .into_with_range(),
3599                )
3600                .into_with_range(),
3601            };
3602            check_path_selection(spec, "data.x->concat([data.y, data.z])", expected.clone());
3603            check_path_selection(spec, "data.x->concat([ data.y, data.z ])", expected.clone());
3604            check_path_selection(spec, "data.x->concat([data.y, data.z,])", expected.clone());
3605            check_path_selection(
3606                spec,
3607                "data.x->concat([data.y, data.z , ])",
3608                expected.clone(),
3609            );
3610            check_path_selection(spec, "data.x->concat([data.y, data.z,],)", expected.clone());
3611            check_path_selection(spec, "data.x->concat([data.y, data.z , ] , )", expected);
3612        }
3613
3614        check_path_selection(
3615            spec,
3616            "data->method([$ { x2: x->times(2) }, $ { y2: y->times(2) }])",
3617            PathSelection {
3618                path: PathList::Key(
3619                    Key::field("data").into_with_range(),
3620                    PathList::Method(
3621                        WithRange::new("method".to_string(), None),
3622                        Some(MethodArgs {
3623                                args: vec![LitExpr::Array(vec![
3624                                LitExpr::Path(PathSelection {
3625                                    path: PathList::Var(
3626                                        KnownVariable::Dollar.into_with_range(),
3627                                        PathList::Selection(
3628                                            SubSelection {
3629                                                selections: vec![NamedSelection {
3630                                                    prefix: NamingPrefix::Alias(Alias::new("x2")),
3631                                                    path: path_value(PathSelection {
3632                                                        path: PathList::Key(
3633                                                            Key::field("x").into_with_range(),
3634                                                            PathList::Method(
3635                                                                WithRange::new(
3636                                                                    "times".to_string(),
3637                                                                    None,
3638                                                                ),
3639                                                                Some(MethodArgs {
3640                                                                    args: vec![LitExpr::Number(
3641                                                                        "2".parse().expect(
3642                                                                            "serde_json::Number parse error",
3643                                                                        ),
3644                                                                    ).into_with_range()],
3645                                                                    ..Default::default()
3646                                                                }),
3647                                                                PathList::Empty.into_with_range(),
3648                                                            )
3649                                                            .into_with_range(),
3650                                                        )
3651                                                        .into_with_range(),
3652                                                    }),
3653                                                }],
3654                                                ..Default::default()
3655                                            },
3656                                        )
3657                                        .into_with_range(),
3658                                    )
3659                                    .into_with_range(),
3660                                })
3661                                .into_with_range(),
3662                                LitExpr::Path(PathSelection {
3663                                    path: PathList::Var(
3664                                        KnownVariable::Dollar.into_with_range(),
3665                                        PathList::Selection(
3666                                            SubSelection {
3667                                                selections: vec![NamedSelection {
3668                                                    prefix: NamingPrefix::Alias(Alias::new("y2")),
3669                                                    path: path_value(PathSelection {
3670                                                        path: PathList::Key(
3671                                                            Key::field("y").into_with_range(),
3672                                                            PathList::Method(
3673                                                                WithRange::new(
3674                                                                    "times".to_string(),
3675                                                                    None,
3676                                                                ),
3677                                                                Some(
3678                                                                    MethodArgs {
3679                                                                        args: vec![LitExpr::Number(
3680                                                                            "2".parse().expect(
3681                                                                                "serde_json::Number parse error",
3682                                                                            ),
3683                                                                        ).into_with_range()],
3684                                                                        ..Default::default()
3685                                                                    },
3686                                                                ),
3687                                                                PathList::Empty.into_with_range(),
3688                                                            )
3689                                                            .into_with_range(),
3690                                                        )
3691                                                        .into_with_range(),
3692                                                    }),
3693                                                }],
3694                                                ..Default::default()
3695                                            },
3696                                        )
3697                                        .into_with_range(),
3698                                    )
3699                                    .into_with_range(),
3700                                })
3701                                .into_with_range(),
3702                            ])
3703                            .into_with_range()],
3704                            ..Default::default()
3705                        }),
3706                        PathList::Empty.into_with_range(),
3707                    )
3708                    .into_with_range(),
3709                )
3710                .into_with_range(),
3711            },
3712        );
3713    }
3714
3715    #[test]
3716    fn test_path_with_subselection() {
3717        assert_debug_snapshot!(selection!(
3718            r#"
3719            choices->first.message { content role }
3720        "#
3721        ));
3722
3723        assert_debug_snapshot!(selection!(
3724            r#"
3725            id
3726            created
3727            choices->first.message { content role }
3728            model
3729        "#
3730        ));
3731
3732        assert_debug_snapshot!(selection!(
3733            r#"
3734            id
3735            created
3736            choices->first.message { content role }
3737            model
3738            choices->last.message { lastContent: content }
3739        "#
3740        ));
3741
3742        assert_debug_snapshot!(JSONSelection::parse(
3743            r#"
3744            id
3745            created
3746            choices->first.message
3747            model
3748        "#
3749        ));
3750
3751        assert_debug_snapshot!(JSONSelection::parse(
3752            r#"
3753            id: $this.id
3754            $args.input {
3755                title
3756                body
3757            }
3758        "#
3759        ));
3760
3761        // Like the selection above, this selection produces an output shape
3762        // with id, title, and body all flattened in a top-level object.
3763        assert_debug_snapshot!(JSONSelection::parse(
3764            r#"
3765            $this { id }
3766            $args { $.input { title body } }
3767        "#
3768        ));
3769
3770        assert_debug_snapshot!(JSONSelection::parse(
3771            r#"
3772            # Equivalent to id: $this.id
3773            $this { id }
3774
3775            $args {
3776                __typename: $("Args")
3777
3778                # Using $. instead of just . prevents .input from
3779                # parsing as a key applied to the $("Args") string.
3780                $.input { title body }
3781
3782                extra
3783            }
3784
3785            from: $.from
3786        "#
3787        ));
3788    }
3789
3790    #[test]
3791    fn test_subselection() {
3792        fn check_parsed(input: &str, expected: SubSelection) {
3793            let (remainder, parsed) = SubSelection::parse(new_span(input)).unwrap();
3794            assert!(
3795                span_is_all_spaces_or_comments(remainder.clone()),
3796                "remainder is `{:?}`",
3797                remainder.clone(),
3798            );
3799            assert_eq!(parsed.strip_ranges(), expected);
3800        }
3801
3802        check_parsed(
3803            " { \n } ",
3804            SubSelection {
3805                selections: vec![],
3806                ..Default::default()
3807            },
3808        );
3809
3810        check_parsed(
3811            "{hello}",
3812            SubSelection {
3813                selections: vec![NamedSelection::field(
3814                    None,
3815                    Key::field("hello").into_with_range(),
3816                    None,
3817                )],
3818                ..Default::default()
3819            },
3820        );
3821
3822        check_parsed(
3823            "{ hello }",
3824            SubSelection {
3825                selections: vec![NamedSelection::field(
3826                    None,
3827                    Key::field("hello").into_with_range(),
3828                    None,
3829                )],
3830                ..Default::default()
3831            },
3832        );
3833
3834        check_parsed(
3835            "  { padded  } ",
3836            SubSelection {
3837                selections: vec![NamedSelection::field(
3838                    None,
3839                    Key::field("padded").into_with_range(),
3840                    None,
3841                )],
3842                ..Default::default()
3843            },
3844        );
3845
3846        check_parsed(
3847            "{ hello world }",
3848            SubSelection {
3849                selections: vec![
3850                    NamedSelection::field(None, Key::field("hello").into_with_range(), None),
3851                    NamedSelection::field(None, Key::field("world").into_with_range(), None),
3852                ],
3853                ..Default::default()
3854            },
3855        );
3856
3857        check_parsed(
3858            "{ hello { world } }",
3859            SubSelection {
3860                selections: vec![NamedSelection::field(
3861                    None,
3862                    Key::field("hello").into_with_range(),
3863                    Some(SubSelection {
3864                        selections: vec![NamedSelection::field(
3865                            None,
3866                            Key::field("world").into_with_range(),
3867                            None,
3868                        )],
3869                        ..Default::default()
3870                    }),
3871                )],
3872                ..Default::default()
3873            },
3874        );
3875    }
3876
3877    #[test]
3878    fn test_external_var_paths() {
3879        fn parse(input: &str) -> PathSelection {
3880            PathSelection::parse(new_span(input))
3881                .unwrap()
3882                .1
3883                .strip_ranges()
3884        }
3885
3886        {
3887            let sel = selection!(
3888                r#"
3889                $->echo([$args.arg1, $args.arg2, @.items->first])
3890            "#
3891            )
3892            .strip_ranges();
3893            let args_arg1_path = parse("$args.arg1");
3894            let args_arg2_path = parse("$args.arg2");
3895            assert_eq!(
3896                sel.external_var_paths(),
3897                vec![&args_arg1_path, &args_arg2_path]
3898            );
3899        }
3900        {
3901            let sel = selection!(
3902                r#"
3903                $this.kind->match(
3904                    ["A", $this.a],
3905                    ["B", $this.b],
3906                    ["C", $this.c],
3907                    [@, @->to_lower_case],
3908                )
3909            "#
3910            )
3911            .strip_ranges();
3912            let this_kind_path = match &sel.inner {
3913                TopLevelSelection::Value(lit) => match lit.as_ref() {
3914                    LitExpr::Path(path) => path,
3915                    _ => panic!("Expected PathSelection"),
3916                },
3917                _ => panic!("Expected PathSelection"),
3918            };
3919            let this_a_path = parse("$this.a");
3920            let this_b_path = parse("$this.b");
3921            let this_c_path = parse("$this.c");
3922            assert_eq!(
3923                sel.external_var_paths(),
3924                vec![this_kind_path, &this_a_path, &this_b_path, &this_c_path,]
3925            );
3926        }
3927        {
3928            let sel = selection!(
3929                r#"
3930                data.results->slice($args.start, $args.end) {
3931                    id
3932                    __typename: $args.type
3933                }
3934            "#
3935            )
3936            .strip_ranges();
3937            let start_path = parse("$args.start");
3938            let end_path = parse("$args.end");
3939            let args_type_path = parse("$args.type");
3940            assert_eq!(
3941                sel.external_var_paths(),
3942                vec![&start_path, &end_path, &args_type_path]
3943            );
3944        }
3945    }
3946
3947    #[test]
3948    fn test_local_var_paths() {
3949        let spec = ConnectSpec::V0_3;
3950        let name_selection = selection!(
3951            "person->as($name, @.name)->as($stray, 123)->echo({ hello: $name })",
3952            spec
3953        );
3954        let local_var_names = name_selection.local_var_names();
3955        assert_eq!(local_var_names.len(), 2);
3956        assert!(local_var_names.contains("$name"));
3957        assert!(local_var_names.contains("$stray"));
3958    }
3959
3960    #[test]
3961    fn test_ranged_locations() {
3962        fn check(input: &str, expected: JSONSelection) {
3963            let parsed = JSONSelection::parse(input).unwrap();
3964            assert_eq!(parsed, expected);
3965        }
3966
3967        check(
3968            "hello",
3969            JSONSelection::named(SubSelection {
3970                selections: vec![NamedSelection::field(
3971                    None,
3972                    WithRange::new(Key::field("hello"), Some(0..5)),
3973                    None,
3974                )],
3975                range: Some(0..5),
3976            }),
3977        );
3978
3979        check(
3980            "  hello ",
3981            JSONSelection::named(SubSelection {
3982                selections: vec![NamedSelection::field(
3983                    None,
3984                    WithRange::new(Key::field("hello"), Some(2..7)),
3985                    None,
3986                )],
3987                range: Some(2..7),
3988            }),
3989        );
3990
3991        check(
3992            "  hello  { hi name }",
3993            JSONSelection::named(SubSelection {
3994                selections: vec![NamedSelection::field(
3995                    None,
3996                    WithRange::new(Key::field("hello"), Some(2..7)),
3997                    Some(SubSelection {
3998                        selections: vec![
3999                            NamedSelection::field(
4000                                None,
4001                                WithRange::new(Key::field("hi"), Some(11..13)),
4002                                None,
4003                            ),
4004                            NamedSelection::field(
4005                                None,
4006                                WithRange::new(Key::field("name"), Some(14..18)),
4007                                None,
4008                            ),
4009                        ],
4010                        range: Some(9..20),
4011                    }),
4012                )],
4013                range: Some(2..20),
4014            }),
4015        );
4016
4017        check(
4018            "$args.product.id",
4019            JSONSelection::path(PathSelection {
4020                path: WithRange::new(
4021                    PathList::Var(
4022                        WithRange::new(
4023                            KnownVariable::External(Namespace::Args.to_string()),
4024                            Some(0..5),
4025                        ),
4026                        WithRange::new(
4027                            PathList::Key(
4028                                WithRange::new(Key::field("product"), Some(6..13)),
4029                                WithRange::new(
4030                                    PathList::Key(
4031                                        WithRange::new(Key::field("id"), Some(14..16)),
4032                                        WithRange::new(PathList::Empty, Some(16..16)),
4033                                    ),
4034                                    Some(13..16),
4035                                ),
4036                            ),
4037                            Some(5..16),
4038                        ),
4039                    ),
4040                    Some(0..16),
4041                ),
4042            }),
4043        );
4044
4045        check(
4046            " $args . product . id ",
4047            JSONSelection::path(PathSelection {
4048                path: WithRange::new(
4049                    PathList::Var(
4050                        WithRange::new(
4051                            KnownVariable::External(Namespace::Args.to_string()),
4052                            Some(1..6),
4053                        ),
4054                        WithRange::new(
4055                            PathList::Key(
4056                                WithRange::new(Key::field("product"), Some(9..16)),
4057                                WithRange::new(
4058                                    PathList::Key(
4059                                        WithRange::new(Key::field("id"), Some(19..21)),
4060                                        WithRange::new(PathList::Empty, Some(21..21)),
4061                                    ),
4062                                    Some(17..21),
4063                                ),
4064                            ),
4065                            Some(7..21),
4066                        ),
4067                    ),
4068                    Some(1..21),
4069                ),
4070            }),
4071        );
4072
4073        check(
4074            "before product:$args.product{id name}after",
4075            JSONSelection::named(SubSelection {
4076                selections: vec![
4077                    NamedSelection::field(
4078                        None,
4079                        WithRange::new(Key::field("before"), Some(0..6)),
4080                        None,
4081                    ),
4082                    NamedSelection {
4083                        prefix: NamingPrefix::Alias(Alias {
4084                            name: WithRange::new(Key::field("product"), Some(7..14)),
4085                            range: Some(7..15),
4086                        }),
4087                        path: path_value(PathSelection {
4088                            path: WithRange::new(
4089                                PathList::Var(
4090                                    WithRange::new(
4091                                        KnownVariable::External(Namespace::Args.to_string()),
4092                                        Some(15..20),
4093                                    ),
4094                                    WithRange::new(
4095                                        PathList::Key(
4096                                            WithRange::new(Key::field("product"), Some(21..28)),
4097                                            WithRange::new(
4098                                                PathList::Selection(SubSelection {
4099                                                    selections: vec![
4100                                                        NamedSelection::field(
4101                                                            None,
4102                                                            WithRange::new(
4103                                                                Key::field("id"),
4104                                                                Some(29..31),
4105                                                            ),
4106                                                            None,
4107                                                        ),
4108                                                        NamedSelection::field(
4109                                                            None,
4110                                                            WithRange::new(
4111                                                                Key::field("name"),
4112                                                                Some(32..36),
4113                                                            ),
4114                                                            None,
4115                                                        ),
4116                                                    ],
4117                                                    range: Some(28..37),
4118                                                }),
4119                                                Some(28..37),
4120                                            ),
4121                                        ),
4122                                        Some(20..37),
4123                                    ),
4124                                ),
4125                                Some(15..37),
4126                            ),
4127                        }),
4128                    },
4129                    NamedSelection::field(
4130                        None,
4131                        WithRange::new(Key::field("after"), Some(37..42)),
4132                        None,
4133                    ),
4134                ],
4135                range: Some(0..42),
4136            }),
4137        );
4138    }
4139
4140    #[test]
4141    fn test_variable_reference_no_path() {
4142        let selection = JSONSelection::parse("$this").unwrap();
4143        let var_paths = selection.external_var_paths();
4144        assert_eq!(var_paths.len(), 1);
4145        assert_eq!(
4146            var_paths[0].variable_reference(),
4147            Some(VariableReference {
4148                namespace: VariableNamespace {
4149                    namespace: Namespace::This,
4150                    location: Some(0..5),
4151                },
4152                selection: {
4153                    let mut selection = SelectionTrie::new();
4154                    selection.add_str_path([]);
4155                    selection
4156                },
4157                location: Some(0..5),
4158            })
4159        );
4160    }
4161
4162    #[test]
4163    fn test_variable_reference_with_path() {
4164        let selection = JSONSelection::parse("$this.a.b.c").unwrap();
4165        let var_paths = selection.external_var_paths();
4166        assert_eq!(var_paths.len(), 1);
4167
4168        let var_ref = var_paths[0].variable_reference().unwrap();
4169        assert_eq!(
4170            var_ref.namespace,
4171            VariableNamespace {
4172                namespace: Namespace::This,
4173                location: Some(0..5)
4174            }
4175        );
4176        assert_eq!(var_ref.selection.to_string(), "a { b { c } }");
4177        assert_eq!(var_ref.location, Some(0..11));
4178
4179        assert_eq!(
4180            var_ref.selection.key_ranges("a").collect::<Vec<_>>(),
4181            vec![6..7]
4182        );
4183        let a_trie = var_ref.selection.get("a").unwrap();
4184        assert_eq!(a_trie.key_ranges("b").collect::<Vec<_>>(), vec![8..9]);
4185        let b_trie = a_trie.get("b").unwrap();
4186        assert_eq!(b_trie.key_ranges("c").collect::<Vec<_>>(), vec![10..11]);
4187    }
4188
4189    #[test]
4190    fn test_variable_reference_nested() {
4191        let selection = JSONSelection::parse("a b { c: $this.x.y.z { d } }").unwrap();
4192        let var_paths = selection.external_var_paths();
4193        assert_eq!(var_paths.len(), 1);
4194
4195        let var_ref = var_paths[0].variable_reference().unwrap();
4196        assert_eq!(
4197            var_ref.namespace,
4198            VariableNamespace {
4199                namespace: Namespace::This,
4200                location: Some(9..14),
4201            }
4202        );
4203        assert_eq!(var_ref.selection.to_string(), "x { y { z { d } } }");
4204        assert_eq!(var_ref.location, Some(9..26));
4205
4206        assert_eq!(
4207            var_ref.selection.key_ranges("x").collect::<Vec<_>>(),
4208            vec![15..16]
4209        );
4210        let x_trie = var_ref.selection.get("x").unwrap();
4211        assert_eq!(x_trie.key_ranges("y").collect::<Vec<_>>(), vec![17..18]);
4212        let y_trie = x_trie.get("y").unwrap();
4213        assert_eq!(y_trie.key_ranges("z").collect::<Vec<_>>(), vec![19..20]);
4214        let z_trie = y_trie.get("z").unwrap();
4215        assert_eq!(z_trie.key_ranges("d").collect::<Vec<_>>(), vec![23..24]);
4216    }
4217
4218    /// Regression test for RH-1345 / CNN-1093 —
4219    /// `CONNECTORS_CANNOT_RESOLVE_KEY` composition error from `->filter()` in
4220    /// a `@connect(http: { body: })` mapping. The legacy walker treated
4221    /// `PathList::Method` as an opaque leaf and stopped at it, so the
4222    /// trailing subselection `{ id name }` was dropped from the variable's
4223    /// consumption trie — and the synthesized `@key(fields: "items")` failed
4224    /// validation because `items` looks scalar but isn't.
4225    ///
4226    /// With the fused-trie implementation of `variable_reference`, the
4227    /// recursion continues through the `->filter(...)` *and* records the
4228    /// predicate's input consumption — `@.product` inside the filter reads
4229    /// `product` from each item of `$this.items`, so `product` joins
4230    /// `id` / `name` in the trie. The legacy walker captured none of this.
4231    #[test]
4232    fn test_variable_reference_through_filter_subselection() {
4233        let selection = JSONSelection::parse_with_spec(
4234            "$this.items->filter(@.product) { id name }",
4235            ConnectSpec::V0_4,
4236        )
4237        .unwrap();
4238        let var_paths = selection.external_var_paths();
4239        assert_eq!(var_paths.len(), 1);
4240
4241        let var_ref = var_paths[0]
4242            .variable_reference::<crate::connectors::Namespace>()
4243            .unwrap();
4244        assert_eq!(
4245            var_ref.namespace.namespace,
4246            crate::connectors::Namespace::This
4247        );
4248        assert_eq!(var_ref.selection.to_string(), "items { id name product }");
4249    }
4250
4251    #[test]
4252    fn test_external_var_paths_no_variable() {
4253        let selection = JSONSelection::parse("a.b.c").unwrap();
4254        let var_paths = selection.external_var_paths();
4255        assert_eq!(var_paths.len(), 0);
4256    }
4257
4258    #[test]
4259    fn test_naked_literal_path_for_connect_v0_2() {
4260        let spec = ConnectSpec::V0_2;
4261
4262        let selection_null_stringify_v0_2 = selection!("$(null->jsonStringify)", spec);
4263        assert_eq!(
4264            selection_null_stringify_v0_2.pretty_print(),
4265            "$(null->jsonStringify)"
4266        );
4267
4268        let selection_hello_slice_v0_2 = selection!("sliced: $('hello'->slice(1, 3))", spec);
4269        assert_eq!(
4270            selection_hello_slice_v0_2.pretty_print(),
4271            "sliced: $(\"hello\"->slice(1, 3))"
4272        );
4273
4274        let selection_true_not_v0_2 = selection!("true->not", spec);
4275        assert_eq!(selection_true_not_v0_2.pretty_print(), "true->not");
4276
4277        let selection_false_not_v0_2 = selection!("false->not", spec);
4278        assert_eq!(selection_false_not_v0_2.pretty_print(), "false->not");
4279
4280        let selection_object_path_v0_2 = selection!("$({ a: 123 }.a)", spec);
4281        assert_eq!(
4282            selection_object_path_v0_2.pretty_print_with_indentation(true, 0),
4283            "$({ a: 123 }.a)"
4284        );
4285
4286        let selection_array_path_v0_2 = selection!("$([1, 2, 3]->get(1))", spec);
4287        assert_eq!(
4288            selection_array_path_v0_2.pretty_print(),
4289            "$([1, 2, 3]->get(1))"
4290        );
4291
4292        assert_debug_snapshot!(selection_null_stringify_v0_2);
4293        assert_debug_snapshot!(selection_hello_slice_v0_2);
4294        assert_debug_snapshot!(selection_true_not_v0_2);
4295        assert_debug_snapshot!(selection_false_not_v0_2);
4296        assert_debug_snapshot!(selection_object_path_v0_2);
4297        assert_debug_snapshot!(selection_array_path_v0_2);
4298    }
4299
4300    #[test]
4301    fn test_optional_key_access() {
4302        let spec = ConnectSpec::V0_3;
4303
4304        check_path_selection(
4305            spec,
4306            "$.foo?.bar",
4307            PathSelection {
4308                path: PathList::Var(
4309                    KnownVariable::Dollar.into_with_range(),
4310                    PathList::Key(
4311                        Key::field("foo").into_with_range(),
4312                        PathList::Question(
4313                            PathList::Key(
4314                                Key::field("bar").into_with_range(),
4315                                PathList::Empty.into_with_range(),
4316                            )
4317                            .into_with_range(),
4318                        )
4319                        .into_with_range(),
4320                    )
4321                    .into_with_range(),
4322                )
4323                .into_with_range(),
4324            },
4325        );
4326    }
4327
4328    #[test]
4329    fn test_unambiguous_single_key_paths_v0_2() {
4330        let spec = ConnectSpec::V0_2;
4331
4332        let mul_with_dollars = selection!("a->mul($.b, $.c)", spec);
4333        match mul_with_dollars.top_level() {
4334            TopLevelSelection::Value(lit) => match lit.as_ref() {
4335                LitExpr::Path(path) => {
4336                    assert_eq!(path.get_single_key(), None);
4337                    assert_eq!(path.pretty_print(), "a->mul($.b, $.c)");
4338                }
4339                other => panic!("Expected a path selection, got {other:?}"),
4340            },
4341            other => panic!("Expected a path selection, got {other:?}"),
4342        }
4343
4344        assert_debug_snapshot!(mul_with_dollars);
4345    }
4346
4347    #[test]
4348    fn test_invalid_single_key_paths_v0_2() {
4349        let spec = ConnectSpec::V0_2;
4350
4351        let a_plus_b_plus_c = JSONSelection::parse_with_spec("a->add(b, c)", spec);
4352        assert_eq!(a_plus_b_plus_c, Err(JSONSelectionParseError {
4353            message: "Named path selection must either begin with alias or ..., or end with subselection".to_string(),
4354            fragment: "a->add(b, c)".to_string(),
4355            offset: 0,
4356            spec: ConnectSpec::V0_2,
4357        }));
4358
4359        let sum_a_plus_b_plus_c = JSONSelection::parse_with_spec("sum: a->add(b, c)", spec);
4360        assert_eq!(
4361            sum_a_plus_b_plus_c,
4362            Err(JSONSelectionParseError {
4363                message: "nom::error::ErrorKind::Eof".to_string(),
4364                fragment: "(b, c)".to_string(),
4365                offset: 11,
4366                spec: ConnectSpec::V0_2,
4367            })
4368        );
4369    }
4370
4371    #[test]
4372    fn test_optional_method_call() {
4373        let spec = ConnectSpec::V0_3;
4374
4375        check_path_selection(
4376            spec,
4377            "$.foo?->method",
4378            PathSelection {
4379                path: PathList::Var(
4380                    KnownVariable::Dollar.into_with_range(),
4381                    PathList::Key(
4382                        Key::field("foo").into_with_range(),
4383                        PathList::Question(
4384                            PathList::Method(
4385                                WithRange::new("method".to_string(), None),
4386                                None,
4387                                PathList::Empty.into_with_range(),
4388                            )
4389                            .into_with_range(),
4390                        )
4391                        .into_with_range(),
4392                    )
4393                    .into_with_range(),
4394                )
4395                .into_with_range(),
4396            },
4397        );
4398    }
4399
4400    #[test]
4401    fn test_chained_optional_accesses() {
4402        let spec = ConnectSpec::V0_3;
4403
4404        check_path_selection(
4405            spec,
4406            "$.foo?.bar?.baz",
4407            PathSelection {
4408                path: PathList::Var(
4409                    KnownVariable::Dollar.into_with_range(),
4410                    PathList::Key(
4411                        Key::field("foo").into_with_range(),
4412                        PathList::Question(
4413                            PathList::Key(
4414                                Key::field("bar").into_with_range(),
4415                                PathList::Question(
4416                                    PathList::Key(
4417                                        Key::field("baz").into_with_range(),
4418                                        PathList::Empty.into_with_range(),
4419                                    )
4420                                    .into_with_range(),
4421                                )
4422                                .into_with_range(),
4423                            )
4424                            .into_with_range(),
4425                        )
4426                        .into_with_range(),
4427                    )
4428                    .into_with_range(),
4429                )
4430                .into_with_range(),
4431            },
4432        );
4433    }
4434
4435    #[test]
4436    fn test_mixed_regular_and_optional_access() {
4437        let spec = ConnectSpec::V0_3;
4438
4439        check_path_selection(
4440            spec,
4441            "$.foo.bar?.baz",
4442            PathSelection {
4443                path: PathList::Var(
4444                    KnownVariable::Dollar.into_with_range(),
4445                    PathList::Key(
4446                        Key::field("foo").into_with_range(),
4447                        PathList::Key(
4448                            Key::field("bar").into_with_range(),
4449                            PathList::Question(
4450                                PathList::Key(
4451                                    Key::field("baz").into_with_range(),
4452                                    PathList::Empty.into_with_range(),
4453                                )
4454                                .into_with_range(),
4455                            )
4456                            .into_with_range(),
4457                        )
4458                        .into_with_range(),
4459                    )
4460                    .into_with_range(),
4461                )
4462                .into_with_range(),
4463            },
4464        );
4465    }
4466
4467    #[test]
4468    fn test_invalid_sequential_question_marks() {
4469        let spec = ConnectSpec::V0_3;
4470
4471        assert_eq!(
4472            JSONSelection::parse_with_spec("baz: $.foo??.bar", spec),
4473            Err(JSONSelectionParseError {
4474                message: "nom::error::ErrorKind::Eof".to_string(),
4475                fragment: "??.bar".to_string(),
4476                offset: 10,
4477                spec,
4478            }),
4479        );
4480
4481        assert_eq!(
4482            JSONSelection::parse_with_spec("baz: $.foo?->echo(null)??.bar", spec),
4483            Err(JSONSelectionParseError {
4484                message: "nom::error::ErrorKind::Eof".to_string(),
4485                fragment: "??.bar".to_string(),
4486                offset: 23,
4487                spec,
4488            }),
4489        );
4490    }
4491
4492    #[test]
4493    fn test_invalid_infix_operator_parsing() {
4494        let spec = ConnectSpec::V0_2;
4495
4496        assert_eq!(
4497            JSONSelection::parse_with_spec("aOrB: $($.a ?? $.b)", spec),
4498            Err(JSONSelectionParseError {
4499                message: "nom::error::ErrorKind::Eof".to_string(),
4500                fragment: "($.a ?? $.b)".to_string(),
4501                offset: 7,
4502                spec,
4503            }),
4504        );
4505
4506        assert_eq!(
4507            JSONSelection::parse_with_spec("aOrB: $($.a ?! $.b)", spec),
4508            Err(JSONSelectionParseError {
4509                message: "nom::error::ErrorKind::Eof".to_string(),
4510                fragment: "($.a ?! $.b)".to_string(),
4511                offset: 7,
4512                spec,
4513            }),
4514        );
4515    }
4516
4517    #[test]
4518    fn test_optional_chaining_with_subselection() {
4519        let spec = ConnectSpec::V0_3;
4520
4521        check_path_selection(
4522            spec,
4523            "$.foo?.bar { id name }",
4524            PathSelection {
4525                path: PathList::Var(
4526                    KnownVariable::Dollar.into_with_range(),
4527                    PathList::Key(
4528                        Key::field("foo").into_with_range(),
4529                        PathList::Question(
4530                            PathList::Key(
4531                                Key::field("bar").into_with_range(),
4532                                PathList::Selection(SubSelection {
4533                                    selections: vec![
4534                                        NamedSelection::field(
4535                                            None,
4536                                            Key::field("id").into_with_range(),
4537                                            None,
4538                                        ),
4539                                        NamedSelection::field(
4540                                            None,
4541                                            Key::field("name").into_with_range(),
4542                                            None,
4543                                        ),
4544                                    ],
4545                                    ..Default::default()
4546                                })
4547                                .into_with_range(),
4548                            )
4549                            .into_with_range(),
4550                        )
4551                        .into_with_range(),
4552                    )
4553                    .into_with_range(),
4554                )
4555                .into_with_range(),
4556            },
4557        );
4558    }
4559
4560    #[test]
4561    fn test_optional_method_with_arguments() {
4562        let spec = ConnectSpec::V0_3;
4563
4564        check_path_selection(
4565            spec,
4566            "$.foo?->filter('active')",
4567            PathSelection {
4568                path: PathList::Var(
4569                    KnownVariable::Dollar.into_with_range(),
4570                    PathList::Key(
4571                        Key::field("foo").into_with_range(),
4572                        PathList::Question(
4573                            PathList::Method(
4574                                WithRange::new("filter".to_string(), None),
4575                                Some(MethodArgs {
4576                                    args: vec![
4577                                        LitExpr::String("active".to_string()).into_with_range(),
4578                                    ],
4579                                    ..Default::default()
4580                                }),
4581                                PathList::Empty.into_with_range(),
4582                            )
4583                            .into_with_range(),
4584                        )
4585                        .into_with_range(),
4586                    )
4587                    .into_with_range(),
4588                )
4589                .into_with_range(),
4590            },
4591        );
4592    }
4593
4594    #[test]
4595    fn test_unambiguous_single_key_paths_v0_3() {
4596        let spec = ConnectSpec::V0_3;
4597
4598        let mul_with_dollars = selection!("a->mul($.b, $.c)", spec);
4599        match mul_with_dollars.top_level() {
4600            TopLevelSelection::Value(lit) => match lit.as_ref() {
4601                LitExpr::Path(path) => {
4602                    assert_eq!(path.get_single_key(), None);
4603                    assert_eq!(path.pretty_print(), "a->mul($.b, $.c)");
4604                }
4605                other => panic!("Expected a path selection, got {other:?}"),
4606            },
4607            other => panic!("Expected a path selection, got {other:?}"),
4608        }
4609
4610        assert_debug_snapshot!(mul_with_dollars);
4611    }
4612
4613    #[test]
4614    fn test_valid_single_key_path_v0_3() {
4615        let spec = ConnectSpec::V0_3;
4616
4617        let a_plus_b_plus_c = JSONSelection::parse_with_spec("a->add(b, c)", spec);
4618        if let Ok(selection) = a_plus_b_plus_c {
4619            match selection.top_level() {
4620                TopLevelSelection::Value(lit) => match lit.as_ref() {
4621                    LitExpr::Path(path) => {
4622                        assert_eq!(path.pretty_print(), "a->add(b, c)");
4623                        assert_eq!(path.get_single_key(), None);
4624                    }
4625                    other => panic!("Expected a path selection, got {other:?}"),
4626                },
4627                other => panic!("Expected a path selection, got {other:?}"),
4628            }
4629            assert_debug_snapshot!(selection);
4630        } else {
4631            panic!("Expected a valid selection, got error: {a_plus_b_plus_c:?}");
4632        }
4633    }
4634
4635    #[test]
4636    fn test_valid_single_key_path_with_alias_v0_3() {
4637        let spec = ConnectSpec::V0_3;
4638
4639        let sum_a_plus_b_plus_c = JSONSelection::parse_with_spec("sum: a->add(b, c)", spec);
4640        if let Ok(selection) = sum_a_plus_b_plus_c {
4641            match selection.top_level() {
4642                TopLevelSelection::Named(named) => {
4643                    for selection in named.selections_iter() {
4644                        assert_eq!(selection.pretty_print(), "sum: a->add(b, c)");
4645                        assert_eq!(
4646                            selection.get_single_key().map(|key| key.as_str()),
4647                            Some("sum")
4648                        );
4649                    }
4650                }
4651                other => panic!("Expected any number of named selections, got {other:?}"),
4652            }
4653            assert_debug_snapshot!(selection);
4654        } else {
4655            panic!("Expected a valid selection, got error: {sum_a_plus_b_plus_c:?}");
4656        }
4657    }
4658
4659    #[test]
4660    fn test_disallowed_spread_syntax_error() {
4661        assert_eq!(
4662            JSONSelection::parse_with_spec("id ...names", ConnectSpec::V0_2),
4663            Err(JSONSelectionParseError {
4664                message: "nom::error::ErrorKind::Eof".to_string(),
4665                fragment: "...names".to_string(),
4666                offset: 3,
4667                spec: ConnectSpec::V0_2,
4668            }),
4669        );
4670
4671        assert_eq!(
4672            JSONSelection::parse_with_spec("id ...names", ConnectSpec::V0_3),
4673            Err(JSONSelectionParseError {
4674                message: "Spread syntax (...) is not supported in connect/v0.3 (use connect/v0.4)"
4675                    .to_string(),
4676                // This is the fragment and offset we should get, but we need to
4677                // store error offsets in SpanExtra::errors to provide that
4678                // information.
4679                fragment: "...names".to_string(),
4680                offset: 3,
4681                spec: ConnectSpec::V0_3,
4682            }),
4683        );
4684
4685        // Guard test disabled - spread syntax tests have been enabled for V0_4.
4686        // assert_eq!(ConnectSpec::V0_3, ConnectSpec::next());
4687    }
4688
4689    // Spread syntax tests for ConnectSpec::V0_4 (now enabled!)
4690    #[cfg(test)]
4691    mod spread_parsing {
4692        use crate::connectors::ConnectSpec;
4693        use crate::connectors::json_selection::PrettyPrintable;
4694        use crate::selection;
4695
4696        #[track_caller]
4697        pub(super) fn check(spec: ConnectSpec, input: &str, expected_pretty: &str) {
4698            let selection = selection!(input, spec);
4699            assert_eq!(selection.pretty_print(), expected_pretty);
4700        }
4701    }
4702
4703    #[test]
4704    fn test_basic_spread_parsing_one_field() {
4705        let spec = ConnectSpec::V0_4;
4706        let expected = "... a";
4707        spread_parsing::check(spec, "...a", expected);
4708        spread_parsing::check(spec, "... a", expected);
4709        spread_parsing::check(spec, "...a ", expected);
4710        spread_parsing::check(spec, "... a ", expected);
4711        spread_parsing::check(spec, " ... a ", expected);
4712        spread_parsing::check(spec, "...\na", expected);
4713        assert_debug_snapshot!(selection!("...a", spec));
4714    }
4715
4716    #[test]
4717    fn test_spread_parsing_spread_a_spread_b() {
4718        let spec = ConnectSpec::V0_4;
4719        let expected = "... a\n... b";
4720        spread_parsing::check(spec, "...a...b", expected);
4721        spread_parsing::check(spec, "... a ... b", expected);
4722        spread_parsing::check(spec, "... a ...b", expected);
4723        spread_parsing::check(spec, "... a ... b ", expected);
4724        spread_parsing::check(spec, " ... a ... b ", expected);
4725        assert_debug_snapshot!(selection!("...a...b", spec));
4726    }
4727
4728    #[test]
4729    fn test_spread_parsing_a_spread_b() {
4730        let spec = ConnectSpec::V0_4;
4731        let expected = "a\n... b";
4732        spread_parsing::check(spec, "a...b", expected);
4733        spread_parsing::check(spec, "a ... b", expected);
4734        spread_parsing::check(spec, "a\n...b", expected);
4735        spread_parsing::check(spec, "a\n...\nb", expected);
4736        spread_parsing::check(spec, "a...\nb", expected);
4737        spread_parsing::check(spec, " a ... b", expected);
4738        spread_parsing::check(spec, " a ...b", expected);
4739        spread_parsing::check(spec, " a ... b ", expected);
4740        assert_debug_snapshot!(selection!("a...b", spec));
4741    }
4742
4743    #[test]
4744    fn test_spread_parsing_spread_a_b() {
4745        let spec = ConnectSpec::V0_4;
4746        let expected = "... a\nb";
4747        spread_parsing::check(spec, "...a b", expected);
4748        spread_parsing::check(spec, "... a b", expected);
4749        spread_parsing::check(spec, "... a b ", expected);
4750        spread_parsing::check(spec, "... a\nb", expected);
4751        spread_parsing::check(spec, "... a\n b", expected);
4752        spread_parsing::check(spec, " ... a b ", expected);
4753        assert_debug_snapshot!(selection!("...a b", spec));
4754    }
4755
4756    #[test]
4757    fn test_spread_parsing_spread_a_b_c() {
4758        let spec = ConnectSpec::V0_4;
4759        let expected = "... a\nb\nc";
4760        spread_parsing::check(spec, "...a b c", expected);
4761        spread_parsing::check(spec, "... a b c", expected);
4762        spread_parsing::check(spec, "... a b c ", expected);
4763        spread_parsing::check(spec, "... a\nb\nc", expected);
4764        spread_parsing::check(spec, "... a\nb\n c", expected);
4765        spread_parsing::check(spec, " ... a b c ", expected);
4766        spread_parsing::check(spec, "...\na b c", expected);
4767        assert_debug_snapshot!(selection!("...a b c", spec));
4768    }
4769
4770    #[test]
4771    fn test_spread_parsing_spread_spread_a_sub_b() {
4772        let spec = ConnectSpec::V0_4;
4773        let expected = "... a {\n  b\n}";
4774        spread_parsing::check(spec, "...a{b}", expected);
4775        spread_parsing::check(spec, "... a { b }", expected);
4776        spread_parsing::check(spec, "...a { b }", expected);
4777        spread_parsing::check(spec, "... a { b } ", expected);
4778        spread_parsing::check(spec, "... a\n{ b }", expected);
4779        spread_parsing::check(spec, "... a\n{b}", expected);
4780        spread_parsing::check(spec, " ... a { b } ", expected);
4781        spread_parsing::check(spec, "...\na { b }", expected);
4782        assert_debug_snapshot!(selection!("...a{b}", spec));
4783    }
4784
4785    #[test]
4786    fn test_spread_parsing_spread_a_sub_b_c() {
4787        let spec = ConnectSpec::V0_4;
4788        let expected = "... a {\n  b\n  c\n}";
4789        spread_parsing::check(spec, "...a{b c}", expected);
4790        spread_parsing::check(spec, "... a { b c }", expected);
4791        spread_parsing::check(spec, "...a { b c }", expected);
4792        spread_parsing::check(spec, "... a { b c } ", expected);
4793        spread_parsing::check(spec, "... a\n{ b c }", expected);
4794        spread_parsing::check(spec, "... a\n{b c}", expected);
4795        spread_parsing::check(spec, " ... a { b c } ", expected);
4796        spread_parsing::check(spec, "...\na { b c }", expected);
4797        spread_parsing::check(spec, "...\na { b\nc }", expected);
4798        assert_debug_snapshot!(selection!("...a{b c}", spec));
4799    }
4800
4801    #[test]
4802    fn test_spread_parsing_spread_a_sub_b_spread_c() {
4803        let spec = ConnectSpec::V0_4;
4804        let expected = "... a {\n  b\n  ... c\n}";
4805        spread_parsing::check(spec, "...a{b...c}", expected);
4806        spread_parsing::check(spec, "... a { b ... c }", expected);
4807        spread_parsing::check(spec, "...a { b ... c }", expected);
4808        spread_parsing::check(spec, "... a { b ... c } ", expected);
4809        spread_parsing::check(spec, "... a\n{ b ... c }", expected);
4810        spread_parsing::check(spec, "... a\n{b ... c}", expected);
4811        spread_parsing::check(spec, " ... a { b ... c } ", expected);
4812        spread_parsing::check(spec, "...\na { b ... c }", expected);
4813        spread_parsing::check(spec, "...\na {b ...\nc }", expected);
4814        assert_debug_snapshot!(selection!("...a{b...c}", spec));
4815    }
4816
4817    #[test]
4818    fn test_spread_parsing_spread_a_sub_b_spread_c_d() {
4819        let spec = ConnectSpec::V0_4;
4820        let expected = "... a {\n  b\n  ... c\n  d\n}";
4821        spread_parsing::check(spec, "...a{b...c d}", expected);
4822        spread_parsing::check(spec, "... a { b ... c d }", expected);
4823        spread_parsing::check(spec, "...a { b ... c d }", expected);
4824        spread_parsing::check(spec, "... a { b ... c d } ", expected);
4825        spread_parsing::check(spec, "... a\n{ b ... c d }", expected);
4826        spread_parsing::check(spec, "... a\n{b ... c d}", expected);
4827        spread_parsing::check(spec, " ... a { b ... c d } ", expected);
4828        spread_parsing::check(spec, "...\na { b ... c d }", expected);
4829        spread_parsing::check(spec, "...\na {b ...\nc d }", expected);
4830        assert_debug_snapshot!(selection!("...a{b...c d}", spec));
4831    }
4832
4833    #[test]
4834    fn test_spread_parsing_spread_a_sub_spread_b_c_d_spread_e() {
4835        let spec = ConnectSpec::V0_4;
4836        let expected = "... a {\n  ... b\n  c\n  d\n  ... e\n}";
4837        spread_parsing::check(spec, "...a{...b c d...e}", expected);
4838        spread_parsing::check(spec, "... a { ... b c d ... e }", expected);
4839        spread_parsing::check(spec, "...a { ... b c d ... e }", expected);
4840        spread_parsing::check(spec, "... a { ... b c d ... e } ", expected);
4841        spread_parsing::check(spec, "... a\n{ ... b c d ... e }", expected);
4842        spread_parsing::check(spec, "... a\n{... b c d ... e}", expected);
4843        spread_parsing::check(spec, " ... a { ... b c d ... e } ", expected);
4844        spread_parsing::check(spec, "...\na { ... b c d ... e }", expected);
4845        spread_parsing::check(spec, "...\na {...\nb\nc d ...\ne }", expected);
4846        assert_debug_snapshot!(selection!("...a{...b c d...e}", spec));
4847    }
4848
4849    #[test]
4850    fn should_parse_null_coalescing_in_connect_0_3() {
4851        assert!(JSONSelection::parse_with_spec("sum: $(a ?? b)", ConnectSpec::V0_3).is_ok());
4852        assert!(JSONSelection::parse_with_spec("sum: $(a ?! b)", ConnectSpec::V0_3).is_ok());
4853    }
4854
4855    #[test]
4856    fn should_not_parse_null_coalescing_in_connect_0_2() {
4857        assert!(JSONSelection::parse_with_spec("sum: $(a ?? b)", ConnectSpec::V0_2).is_err());
4858        assert!(JSONSelection::parse_with_spec("sum: $(a ?! b)", ConnectSpec::V0_2).is_err());
4859    }
4860
4861    #[test]
4862    fn should_not_parse_mixed_operators_in_same_expression() {
4863        let result = JSONSelection::parse_with_spec("sum: $(a ?? b ?! c)", ConnectSpec::V0_3);
4864
4865        let err = result.expect_err("Expected parse error for mixed operators ?? and ?!");
4866        assert_eq!(
4867            err.message,
4868            "Found mixed operators ?? and ?!. You can only chain operators of the same kind."
4869        );
4870
4871        // Also test the reverse order
4872        let result2 = JSONSelection::parse_with_spec("sum: $(a ?! b ?? c)", ConnectSpec::V0_3);
4873        let err2 = result2.expect_err("Expected parse error for mixed operators ?! and ??");
4874        assert_eq!(
4875            err2.message,
4876            "Found mixed operators ?! and ??. You can only chain operators of the same kind."
4877        );
4878    }
4879
4880    #[test]
4881    fn should_parse_mixed_operators_in_nested_expression() {
4882        let result = JSONSelection::parse_with_spec("sum: $(a ?? $(b ?! c))", ConnectSpec::V0_3);
4883
4884        assert!(result.is_ok());
4885    }
4886
4887    #[test]
4888    fn should_parse_local_vars_as_such() {
4889        let spec = ConnectSpec::V0_3;
4890        // No external variable references because $ and @ are internal, and
4891        // $root is locally bound by the ->as method everywhere it's used.
4892        let all_local = selection!("$->as($root, @.data)->echo([$root, $root])", spec);
4893        assert!(all_local.external_var_paths().is_empty());
4894        assert_debug_snapshot!(all_local);
4895
4896        // Introducing one external variable reference: $ext.
4897        let ext = selection!("$->as($root, @.data)->echo([$root, $ext])", spec);
4898        let external_vars = ext.external_var_paths();
4899        assert_eq!(external_vars.len(), 1);
4900
4901        for ext_var in &external_vars {
4902            match ext_var.path.as_ref() {
4903                PathList::Var(var, _) => match var.as_ref() {
4904                    KnownVariable::External(var_name) => {
4905                        assert_eq!(var_name, "$ext");
4906                    }
4907                    _ => panic!("Expected external variable, got: {var:?}"),
4908                },
4909                _ => panic!(
4910                    "Expected variable at start of path, got: {:?}",
4911                    &ext_var.path
4912                ),
4913            };
4914        }
4915
4916        assert_debug_snapshot!(ext);
4917    }
4918
4919    #[test]
4920    fn top_level_braced_subselection_v0_4() {
4921        // `{ id name }` is the LitObject alternative of the unified v0.4
4922        // grammar — it lands as a `Value(LitExpr::Object(sub))`, not as a
4923        // `Named(sub)`, because the braces are a real token the user wrote.
4924        // Bare `id name` remains the `NamedSelectionList` alternative and
4925        // lands as `Named(sub)`.
4926        let braced = JSONSelection::parse_with_spec("{ id name }", ConnectSpec::V0_4).unwrap();
4927        let naked = JSONSelection::parse_with_spec("id name", ConnectSpec::V0_4).unwrap();
4928        assert!(
4929            matches!(
4930                &braced.inner,
4931                TopLevelSelection::Value(lit) if matches!(lit.as_ref(), LitExpr::Object(_)),
4932            ),
4933            "expected Value(LitExpr::Object(_)), got {:?}",
4934            braced.inner,
4935        );
4936        assert!(matches!(naked.inner, TopLevelSelection::Named(_)));
4937
4938        // Empty `{}` is accepted as an empty object literal.
4939        let empty_braced = JSONSelection::parse_with_spec("{}", ConnectSpec::V0_4).unwrap();
4940        assert!(matches!(
4941            &empty_braced.inner,
4942            TopLevelSelection::Value(lit) if matches!(lit.as_ref(), LitExpr::Object(_)),
4943        ));
4944    }
4945
4946    #[test]
4947    fn top_level_literal_array_v0_4() {
4948        // `[1, 2, 3]` at the top level is an array literal, stored verbatim
4949        // in `Value(LitExpr::Array(_))`. No synthetic `PathList::Expr`
4950        // wrapping — that node is reserved for explicit `$(...)` in source.
4951        let array = JSONSelection::parse_with_spec("[1, 2, 3]", ConnectSpec::V0_4).unwrap();
4952        match &array.inner {
4953            TopLevelSelection::Value(lit) => {
4954                assert!(matches!(lit.as_ref(), LitExpr::Array(_)));
4955            }
4956            other => panic!("Expected TopLevelSelection::Value, got: {other:?}"),
4957        }
4958
4959        // Empty `[]` is accepted.
4960        let empty = JSONSelection::parse_with_spec("[]", ConnectSpec::V0_4).unwrap();
4961        assert!(matches!(
4962            &empty.inner,
4963            TopLevelSelection::Value(lit) if matches!(lit.as_ref(), LitExpr::Array(_)),
4964        ));
4965
4966        // Mixed element kinds: path expression, number, string.
4967        let mixed =
4968            JSONSelection::parse_with_spec(r#"[$.foo, 42, "x"]"#, ConnectSpec::V0_4).unwrap();
4969        assert!(matches!(
4970            &mixed.inner,
4971            TopLevelSelection::Value(lit) if matches!(lit.as_ref(), LitExpr::Array(_)),
4972        ));
4973    }
4974
4975    #[test]
4976    fn top_level_braces_and_brackets_rejected_in_v0_3() {
4977        // The braces/brackets-at-top-level grammar is v0.4-only; v0.3 must
4978        // still reject `{...}` and `[...]` as the whole JSONSelection.
4979        assert!(JSONSelection::parse_with_spec("{ id name }", ConnectSpec::V0_3).is_err());
4980        assert!(JSONSelection::parse_with_spec("[1, 2, 3]", ConnectSpec::V0_3).is_err());
4981    }
4982}