Skip to main content

apollo_federation/connectors/json_selection/
parser.rs

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