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