Skip to main content

apollo_federation/connectors/json_selection/
apply_to.rs

1/// ApplyTo is a trait for applying a JSONSelection to a JSON value, collecting
2/// any/all errors encountered in the process.
3use std::cell::RefCell;
4use std::hash::Hash;
5
6use apollo_compiler::collections::IndexMap;
7use apollo_compiler::collections::IndexSet;
8use serde_json_bytes::Map as JSONMap;
9use serde_json_bytes::Value as JSON;
10use serde_json_bytes::json;
11use shape::Shape;
12use shape::ShapeCase;
13use shape::location::Location;
14use shape::location::SourceId;
15
16use super::Ref;
17use super::helpers::json_merge;
18use super::helpers::json_type_name;
19use super::immutable::InputPath;
20use super::known_var::KnownVariable;
21use super::lit_expr::LitExpr;
22use super::lit_expr::LitOp;
23use super::location::OffsetRange;
24use super::location::Ranged;
25use super::location::WithRange;
26use super::methods::ArrowMethod;
27use super::parser::*;
28use super::selection_trie::SelectionTrie;
29use crate::connectors::spec::ConnectSpec;
30
31pub(super) type VarsWithPathsMap<'a> = IndexMap<KnownVariable, (&'a JSON, InputPath<JSON>)>;
32
33impl JSONSelection {
34    // Applying a selection to a JSON value produces a new JSON value, along
35    // with any/all errors encountered in the process. The value is represented
36    // as an Option to allow for undefined/missing values (which JSON does not
37    // explicitly support), which are distinct from null values (which it does
38    // support).
39    pub fn apply_to(&self, data: &JSON) -> (Option<JSON>, Vec<ApplyToError>) {
40        self.apply_with_vars(data, &IndexMap::default())
41    }
42
43    pub fn apply_with_vars(
44        &self,
45        data: &JSON,
46        vars: &IndexMap<String, JSON>,
47    ) -> (Option<JSON>, Vec<ApplyToError>) {
48        // Using IndexSet over HashSet to preserve the order of the errors.
49        let mut errors = IndexSet::default();
50
51        let mut vars_with_paths: VarsWithPathsMap = IndexMap::default();
52        for (var_name, var_data) in vars {
53            vars_with_paths.insert(
54                KnownVariable::External(var_name.as_str().to_string()),
55                (var_data, InputPath::empty().append(json!(var_name))),
56            );
57        }
58        // The $ variable initially refers to the root data value, but is
59        // rebound by nested selection sets to refer to the root value the
60        // selection set was applied to.
61        vars_with_paths.insert(KnownVariable::Dollar, (data, InputPath::empty()));
62
63        let spec = self.spec();
64        let (value, apply_errors) =
65            self.apply_to_path(data, &vars_with_paths, &InputPath::empty(), spec);
66
67        // Since errors is an IndexSet, this line effectively deduplicates the
68        // errors, in an attempt to make them less verbose. However, now that we
69        // include both path and range information in the errors, there's an
70        // argument to be made that errors can no longer be meaningfully
71        // deduplicated, so we might consider sticking with a Vec<ApplyToError>.
72        errors.extend(apply_errors);
73
74        (value, errors.into_iter().collect())
75    }
76
77    pub fn shape(&self) -> Shape {
78        let context =
79            ShapeContext::new(SourceId::Other("JSONSelection".into())).with_spec(self.spec());
80
81        self.compute_output_shape(
82            // Relatively static/unchanging inputs to compute_output_shape,
83            // passed down by immutable shared reference.
84            &context,
85            // If we don't know anything about the shape of the input data, we
86            // can represent the data symbolically using the $root variable
87            // shape. Subproperties needed from this shape will show up as
88            // subpaths like $root.books.4.isbn in the output shape.
89            //
90            // While we do not currently have a $root variable available as a
91            // KnownVariable during apply_to_path execution, we might consider
92            // adding it, since it would align with the way we process other
93            // variable shapes. For now, $root exists only as a shape name that
94            // we are inventing right here.
95            Shape::name("$root", Vec::new()),
96        )
97    }
98
99    pub(crate) fn compute_output_shape(&self, context: &ShapeContext, input_shape: Shape) -> Shape {
100        debug_assert_eq!(context.spec(), self.spec());
101
102        let computable: &dyn ApplyToInternal = match &self.inner {
103            TopLevelSelection::Named(selection) => selection,
104            TopLevelSelection::Value(lit) => lit,
105        };
106
107        let dollar_shape = input_shape.clone();
108
109        if Some(&input_shape) == context.named_shapes().get("$root") {
110            // If the $root variable happens to be bound to the input shape,
111            // context does not need to be cloned or modified.
112            computable.compute_output_shape(context, input_shape, dollar_shape)
113        } else {
114            // Otherwise, we'll want to register the input_shape as $root in a
115            // cloned_context, so $root is reliably defined either way.
116            let cloned_context = context
117                .clone()
118                .with_named_shapes([("$root".to_string(), input_shape.clone())]);
119            computable.compute_output_shape(&cloned_context, input_shape, dollar_shape)
120        }
121    }
122}
123
124fn lookup_variable<'a>(
125    vars: &'a VarsWithPathsMap,
126    var_name: &str,
127) -> Option<(&'a JSON, &'a InputPath<JSON>)> {
128    let entry = if var_name == "$" {
129        vars.get(&KnownVariable::Dollar)
130    } else {
131        // Variables longer than $ may be stored either as
132        // KnownVariable::External(String) or KnownVariable::Local(String), so
133        // we need to check both variants, local first.
134        vars.get(&KnownVariable::Local(var_name.to_string()))
135            .or_else(|| vars.get(&KnownVariable::External(var_name.to_string())))
136    };
137    entry.map(|(data, path)| (*data, path))
138}
139
140impl Ranged for JSONSelection {
141    fn range(&self) -> OffsetRange {
142        match &self.inner {
143            TopLevelSelection::Named(selection) => selection.range(),
144            TopLevelSelection::Value(lit) => lit.range(),
145        }
146    }
147
148    fn shape_location(&self, source_id: &SourceId) -> Option<Location> {
149        self.range().map(|range| source_id.location(range))
150    }
151}
152
153pub(super) trait ApplyToInternal {
154    // This is the trait method that should be implemented and called
155    // recursively by the various JSONSelection types.
156    fn apply_to_path(
157        &self,
158        data: &JSON,
159        vars: &VarsWithPathsMap,
160        input_path: &InputPath<JSON>,
161        spec: ConnectSpec,
162    ) -> (Option<JSON>, Vec<ApplyToError>);
163
164    // When array is encountered, the Self selection will be applied to each
165    // element of the array, producing a new array.
166    fn apply_to_array(
167        &self,
168        data_array: &[JSON],
169        vars: &VarsWithPathsMap,
170        input_path: &InputPath<JSON>,
171        spec: ConnectSpec,
172    ) -> (Option<JSON>, Vec<ApplyToError>) {
173        let mut output = Vec::with_capacity(data_array.len());
174        let mut errors = Vec::new();
175
176        for (i, element) in data_array.iter().enumerate() {
177            let input_path_with_index = input_path.append(json!(i));
178            let (applied, apply_errors) =
179                self.apply_to_path(element, vars, &input_path_with_index, spec);
180            errors.extend(apply_errors);
181            // When building an Object, we can simply omit missing properties
182            // and report an error, but when building an Array, we need to
183            // insert null values to preserve the original array indices/length.
184            output.push(applied.unwrap_or(JSON::Null));
185        }
186
187        (Some(JSON::Array(output)), errors)
188    }
189
190    /// Computes the static output shape produced by a JSONSelection, by
191    /// traversing the selection AST, recursively calling `compute_output_shape`
192    /// on the current data/variable shapes at each level.
193    fn compute_output_shape(
194        &self,
195        context: &ShapeContext,
196        // Shape of the `@` variable, which typically changes with each
197        // recursive call to compute_output_shape.
198        input_shape: Shape,
199        // Shape of the `$` variable, which is bound to the closest enclosing
200        // subselection object, or the root data object if there is no enclosing
201        // subselection.
202        dollar_shape: Shape,
203    ) -> Shape;
204}
205
206#[derive(Debug, Clone)]
207pub(crate) struct ShapeContext {
208    /// [`ConnectSpec`] version derived from the [`JSONSelection`] that created
209    /// this [`ShapeContext`].
210    #[allow(dead_code)]
211    spec: ConnectSpec,
212
213    /// Shapes of other named variables, with the variable name `String`
214    /// including the initial `$` character. This map typically does not change
215    /// during the compute_output_shape recursion, and so can be passed down by
216    /// immutable reference.
217    named_shapes: IndexMap<String, Shape>,
218
219    /// A shared source name to use for all locations originating from this
220    /// `JSONSelection`.
221    source_id: SourceId,
222
223    /// Consumption trie accumulated as a byproduct of `compute_output_shape`.
224    /// Wrapped in `Ref<RefCell<…>>` so all clones produced by
225    /// [`ShapeContext::with_named_shapes`] (or any other builder cloning) share
226    /// the *same* trie — each step of the recursion appends into one place.
227    /// Inspectable with [`ShapeContext::consumption`] after recursion ends.
228    consumption: Ref<RefCell<SelectionTrie>>,
229}
230
231impl ShapeContext {
232    pub(crate) fn new(source_id: SourceId) -> Self {
233        // RefCell is not Sync, so Clippy flags this Arc as `arc_with_non_send_sync`.
234        // The context is constructed and consumed within a single static-analysis
235        // traversal (no thread crossings), so Arc is the wrong tool but Rc would
236        // diverge from the module-wide `Ref<T> = Arc<T>` shape-rs convention.
237        #[allow(clippy::arc_with_non_send_sync)]
238        let consumption = Ref::new(RefCell::new(SelectionTrie::new()));
239        Self {
240            spec: ConnectSpec::latest(),
241            named_shapes: IndexMap::default(),
242            source_id,
243            consumption,
244        }
245    }
246
247    #[allow(dead_code)]
248    pub(crate) fn spec(&self) -> ConnectSpec {
249        self.spec
250    }
251
252    pub(crate) fn with_spec(mut self, spec: ConnectSpec) -> Self {
253        self.spec = spec;
254        self
255    }
256
257    pub(crate) fn named_shapes(&self) -> &IndexMap<String, Shape> {
258        &self.named_shapes
259    }
260
261    pub(crate) fn with_named_shapes(
262        mut self,
263        named_shapes: impl IntoIterator<Item = (String, Shape)>,
264    ) -> Self {
265        for (name, shape) in named_shapes {
266            self.named_shapes.insert(name.clone(), shape.clone());
267        }
268        self
269    }
270
271    pub(crate) fn source_id(&self) -> &SourceId {
272        &self.source_id
273    }
274
275    /// Shared handle to the consumption trie that this context's
276    /// `compute_output_shape` invocations contribute to. Cloning this handle
277    /// is cheap; mutations via `borrow_mut()` are visible to every other
278    /// holder, including child contexts produced via `with_named_shapes`.
279    #[allow(dead_code)]
280    pub(crate) fn consumption(&self) -> &Ref<RefCell<SelectionTrie>> {
281        &self.consumption
282    }
283
284    /// Record the names attached to a shape produced during
285    /// `compute_output_shape` recursion. Each name describes the input path
286    /// that the shape was derived from; walking the name chain into the
287    /// consumption trie accumulates the union of all consumed paths.
288    ///
289    /// A shape's identity-bearing names live in two places:
290    ///
291    ///  1. `shape.case() == ShapeCase::Name(name, _)` — the case itself is a
292    ///     symbolic reference, with the `Name` chain as its only payload.
293    ///  2. `shape.names()` — name-metadata accumulated via shape-rs's
294    ///     `with_base_name` / `with_name` / per-step name propagation.
295    ///
296    /// Both sources are recorded.
297    ///
298    /// The `terminal` flag controls whether the deepest node of each
299    /// recorded name chain is marked as a [`SelectionTrie::is_leaf`].
300    /// Pass `true` from terminal recording sites (`PathList::Empty`,
301    /// `PathList::Method` call boundary) where the input value is
302    /// "explicitly consumed"; pass `false` for navigation-only sites
303    /// (`PathList::Key`) where we only want the trie to *contain* the
304    /// path without claiming it terminates a selection.
305    ///
306    /// Only the [`ShapeCase::Name`] case-name (the shape's "primary"
307    /// identity) is eligible to be marked as a leaf. Metadata names
308    /// from `shape.names()` accumulate the path structure as Names
309    /// propagate through field/item operations; marking those as
310    /// leaves too would cause the terminal `$root.a.b.c` recording to
311    /// incorrectly mark its ancestors `$root`, `$root.a`, and
312    /// `$root.a.b` as leaves as well.
313    #[allow(dead_code)]
314    pub(crate) fn record_consumption(&self, shape: &Shape, terminal: bool) {
315        let mut trie = self.consumption.borrow_mut();
316        if let ShapeCase::Name(name, _weak) = shape.case() {
317            let leaf = trie.add_name(name);
318            if terminal {
319                leaf.set_leaf();
320            }
321        }
322        for name in shape.names() {
323            // Metadata names contribute path structure, not leaf identity.
324            trie.add_name(name);
325        }
326    }
327}
328
329#[derive(Debug, Eq, PartialEq, Clone, Hash)]
330pub struct ApplyToError {
331    message: String,
332    path: Vec<JSON>,
333    range: OffsetRange,
334    spec: ConnectSpec,
335}
336
337impl ApplyToError {
338    pub(crate) const fn new(
339        message: String,
340        path: Vec<JSON>,
341        range: OffsetRange,
342        spec: ConnectSpec,
343    ) -> Self {
344        Self {
345            message,
346            path,
347            range,
348            spec,
349        }
350    }
351
352    // This macro is useful for tests, but it absolutely should never be used with
353    // dynamic input at runtime, since it panics for any input that's not JSON.
354    #[cfg(test)]
355    pub(crate) fn from_json(json: &JSON) -> Self {
356        use crate::link::spec::Version;
357
358        let error = json.as_object().unwrap();
359        let message = error.get("message").unwrap().as_str().unwrap().to_string();
360        let path = error.get("path").unwrap().as_array().unwrap().clone();
361        let range = error.get("range").unwrap().as_array().unwrap();
362        let spec = error
363            .get("spec")
364            .and_then(|s| s.as_str())
365            .and_then(|s| match s.parse::<Version>() {
366                Ok(version) => ConnectSpec::try_from(&version).ok(),
367                Err(_) => None,
368            })
369            .unwrap_or_else(ConnectSpec::latest);
370
371        Self {
372            message,
373            path,
374            range: if range.len() == 2 {
375                let start = range[0].as_u64().unwrap() as usize;
376                let end = range[1].as_u64().unwrap() as usize;
377                Some(start..end)
378            } else {
379                None
380            },
381            spec,
382        }
383    }
384
385    pub fn message(&self) -> &str {
386        self.message.as_str()
387    }
388
389    pub fn path(&self) -> &[JSON] {
390        self.path.as_slice()
391    }
392
393    pub fn range(&self) -> OffsetRange {
394        self.range.clone()
395    }
396
397    pub fn spec(&self) -> ConnectSpec {
398        self.spec
399    }
400}
401
402// Rust doesn't allow implementing methods directly on tuples like
403// (Option<JSON>, Vec<ApplyToError>), so we define a trait to provide the
404// methods we need, and implement the trait for the tuple in question.
405pub(super) trait ApplyToResultMethods {
406    fn prepend_errors(self, errors: Vec<ApplyToError>) -> Self;
407
408    fn and_then_collecting_errors(
409        self,
410        f: impl FnOnce(&JSON) -> (Option<JSON>, Vec<ApplyToError>),
411    ) -> (Option<JSON>, Vec<ApplyToError>);
412}
413
414impl ApplyToResultMethods for (Option<JSON>, Vec<ApplyToError>) {
415    // Intentionally taking ownership of self to avoid cloning, since we pretty
416    // much always use this method to replace the previous (value, errors) tuple
417    // before returning.
418    fn prepend_errors(self, mut errors: Vec<ApplyToError>) -> Self {
419        if errors.is_empty() {
420            self
421        } else {
422            let (value_opt, apply_errors) = self;
423            errors.extend(apply_errors);
424            (value_opt, errors)
425        }
426    }
427
428    // A substitute for Option<_>::and_then that accumulates errors behind the
429    // scenes. I'm no Haskell programmer, but this feels monadic? ¯\_(ツ)_/¯
430    fn and_then_collecting_errors(
431        self,
432        f: impl FnOnce(&JSON) -> (Option<JSON>, Vec<ApplyToError>),
433    ) -> (Option<JSON>, Vec<ApplyToError>) {
434        match self {
435            (Some(data), errors) => f(&data).prepend_errors(errors),
436            (None, errors) => (None, errors),
437        }
438    }
439}
440
441impl ApplyToInternal for JSONSelection {
442    fn apply_to_path(
443        &self,
444        data: &JSON,
445        vars: &VarsWithPathsMap,
446        input_path: &InputPath<JSON>,
447        _spec: ConnectSpec,
448    ) -> (Option<JSON>, Vec<ApplyToError>) {
449        match &self.inner {
450            // Because we represent a JSONSelection::Named as a SubSelection, we
451            // can fully delegate apply_to_path to SubSelection::apply_to_path.
452            // Even if we represented Self::Named as a Vec<NamedSelection>, we
453            // could still delegate to SubSelection::apply_to_path, but we would
454            // need to create a temporary SubSelection to wrap the selections
455            // Vec.
456            TopLevelSelection::Named(named_selections) => {
457                named_selections.apply_to_path(data, vars, input_path, self.spec)
458            }
459            TopLevelSelection::Value(lit) => lit.apply_to_path(data, vars, input_path, self.spec),
460        }
461    }
462
463    fn compute_output_shape(
464        &self,
465        context: &ShapeContext,
466        input_shape: Shape,
467        dollar_shape: Shape,
468    ) -> Shape {
469        debug_assert_eq!(context.spec(), self.spec());
470
471        match &self.inner {
472            TopLevelSelection::Named(selection) => {
473                selection.compute_output_shape(context, input_shape, dollar_shape)
474            }
475            TopLevelSelection::Value(lit) => {
476                lit.compute_output_shape(context, input_shape, dollar_shape)
477            }
478        }
479    }
480}
481
482impl ApplyToInternal for NamedSelection {
483    fn apply_to_path(
484        &self,
485        data: &JSON,
486        vars: &VarsWithPathsMap,
487        input_path: &InputPath<JSON>,
488        spec: ConnectSpec,
489    ) -> (Option<JSON>, Vec<ApplyToError>) {
490        let mut output: Option<JSON> = None;
491        let mut errors = Vec::new();
492
493        let (value_opt, apply_errors) = self.path.apply_to_path(data, vars, input_path, spec);
494        errors.extend(apply_errors);
495
496        match &self.prefix {
497            NamingPrefix::Alias(alias) => {
498                if let Some(value) = value_opt {
499                    output = Some(json!({ alias.name.as_str(): value }));
500                }
501            }
502
503            NamingPrefix::Spread(_spread_range) => {
504                match value_opt {
505                    Some(JSON::Object(_) | JSON::Null) => {
506                        // Objects and null are valid outputs for an
507                        // inline/spread NamedSelection.
508                        output = value_opt;
509                    }
510                    Some(value) => {
511                        errors.push(ApplyToError::new(
512                            format!("Expected object or null, not {}", json_type_name(&value)),
513                            input_path.to_vec(),
514                            self.path.range(),
515                            spec,
516                        ));
517                    }
518                    None => {
519                        errors.push(ApplyToError::new(
520                            "Inlined path produced no value".to_string(),
521                            input_path.to_vec(),
522                            self.path.range(),
523                            spec,
524                        ));
525                    }
526                };
527            }
528
529            NamingPrefix::None => {
530                // Since there is no prefix (NamingPrefix::None), value_opt is
531                // usable as the output of NamedSelection::apply_to_path only if
532                // the NamedSelection has an implied single key, or by having a
533                // trailing SubSelection that guarantees object/null output.
534                let single_key = if let LitExpr::Path(path) = self.path.as_ref() {
535                    path.get_single_key()
536                } else {
537                    None
538                };
539                if let Some(single_key) = single_key {
540                    if let Some(value) = value_opt {
541                        output = Some(json!({ single_key.as_str(): value }));
542                    }
543                } else {
544                    output = value_opt;
545                }
546            }
547        }
548
549        (output, errors)
550    }
551
552    fn compute_output_shape(
553        &self,
554        context: &ShapeContext,
555        input_shape: Shape,
556        dollar_shape: Shape,
557    ) -> Shape {
558        let path_shape = self
559            .path
560            .compute_output_shape(context, input_shape, dollar_shape);
561
562        if let Some(single_output_key) = self.get_single_key() {
563            let mut map = Shape::empty_map();
564            map.insert(single_output_key.as_string(), path_shape);
565            Shape::record(map, self.shape_location(context.source_id()))
566        } else {
567            path_shape
568        }
569    }
570}
571
572impl ApplyToInternal for PathSelection {
573    fn apply_to_path(
574        &self,
575        data: &JSON,
576        vars: &VarsWithPathsMap,
577        input_path: &InputPath<JSON>,
578        spec: ConnectSpec,
579    ) -> (Option<JSON>, Vec<ApplyToError>) {
580        match (self.path.as_ref(), vars.get(&KnownVariable::Dollar)) {
581            // If this is a KeyPath, instead of using data as given, we need to
582            // evaluate the path starting from the current value of $. To evaluate
583            // the KeyPath against data, prefix it with @. This logic supports
584            // method chaining like obj->has('a')->and(obj->has('b')), where both
585            // obj references are interpreted as $.obj.
586            (PathList::Key(_, _), Some((dollar_data, dollar_path))) => {
587                self.path
588                    .apply_to_path(dollar_data, vars, dollar_path, spec)
589            }
590
591            // If $ is undefined for some reason, fall back to using data...
592            // TODO: Since $ should never be undefined, we might want to
593            // guarantee its existence at compile time, somehow.
594            // (PathList::Key(_, _), None) => todo!(),
595            _ => self.path.apply_to_path(data, vars, input_path, spec),
596        }
597    }
598
599    fn compute_output_shape(
600        &self,
601        context: &ShapeContext,
602        input_shape: Shape,
603        dollar_shape: Shape,
604    ) -> Shape {
605        match self.path.as_ref() {
606            PathList::Key(_, _) => {
607                // If this is a KeyPath, we need to evaluate the path starting
608                // from the current $ shape, so we pass dollar_shape as the data
609                // *and* dollar_shape to self.path.compute_output_shape.
610                self.path
611                    .compute_output_shape(context, dollar_shape.clone(), dollar_shape)
612            }
613            // If this is not a KeyPath, keep evaluating against input_shape.
614            // This logic parallels PathSelection::apply_to_path (above).
615            _ => self
616                .path
617                .compute_output_shape(context, input_shape, dollar_shape),
618        }
619    }
620}
621
622impl ApplyToInternal for WithRange<PathList> {
623    fn apply_to_path(
624        &self,
625        data: &JSON,
626        vars: &VarsWithPathsMap,
627        input_path: &InputPath<JSON>,
628        spec: ConnectSpec,
629    ) -> (Option<JSON>, Vec<ApplyToError>) {
630        match self.as_ref() {
631            PathList::Var(ranged_var_name, tail) => {
632                let var_name = ranged_var_name.as_ref();
633                if var_name == &KnownVariable::AtSign {
634                    // We represent @ as a variable name in PathList::Var, but
635                    // it is never stored in the vars map, because it is always
636                    // shorthand for the current data value.
637                    tail.apply_to_path(data, vars, input_path, spec)
638                } else if let Some((var_data, var_path)) = lookup_variable(vars, var_name.as_str())
639                {
640                    // Variables are associated with a path, which is always
641                    // just the variable name for named $variables other than $.
642                    // For the special variable $, the path represents the
643                    // sequence of keys from the root input data to the $ data.
644                    tail.apply_to_path(var_data, vars, var_path, spec)
645                } else {
646                    (
647                        None,
648                        vec![ApplyToError::new(
649                            format!("Variable {} not found", var_name.as_str()),
650                            input_path.to_vec(),
651                            ranged_var_name.range(),
652                            spec,
653                        )],
654                    )
655                }
656            }
657            PathList::Key(key, tail) => {
658                let input_path_with_key = input_path.append(key.to_json());
659
660                if let JSON::Array(array) = data {
661                    // If we recursively call self.apply_to_array, it will end
662                    // up invoking the tail of the key recursively, whereas we
663                    // want to apply the tail once to the entire output array of
664                    // shallow key lookups. To keep the recursion shallow, we
665                    // need a version of self that has the same key but no tail.
666                    let empty_tail = WithRange::new(PathList::Empty, tail.range());
667                    let self_with_empty_tail =
668                        WithRange::new(PathList::Key(key.clone(), empty_tail), key.range());
669
670                    self_with_empty_tail
671                        .apply_to_array(array, vars, input_path, spec)
672                        .and_then_collecting_errors(|shallow_mapped_array| {
673                            // This tail.apply_to_path call happens only once,
674                            // passing to the original/top-level tail the entire
675                            // array produced by key-related recursion/mapping.
676                            tail.apply_to_path(
677                                shallow_mapped_array,
678                                vars,
679                                &input_path_with_key,
680                                spec,
681                            )
682                        })
683                } else {
684                    let not_found = || {
685                        (
686                            None,
687                            vec![ApplyToError::new(
688                                format!(
689                                    "Property {} not found in {}",
690                                    key.dotted(),
691                                    json_type_name(data),
692                                ),
693                                input_path_with_key.to_vec(),
694                                key.range(),
695                                spec,
696                            )],
697                        )
698                    };
699
700                    if !matches!(data, JSON::Object(_)) {
701                        return not_found();
702                    }
703
704                    if let Some(child) = data.get(key.as_str()) {
705                        tail.apply_to_path(child, vars, &input_path_with_key, spec)
706                    } else if tail.is_question() {
707                        (None, vec![])
708                    } else {
709                        not_found()
710                    }
711                }
712            }
713            PathList::Expr(expr, tail) => expr
714                .apply_to_path(data, vars, input_path, spec)
715                .and_then_collecting_errors(|value| {
716                    tail.apply_to_path(value, vars, input_path, spec)
717                }),
718            PathList::Method(method_name, method_args, tail) => {
719                let method_path =
720                    input_path.append(JSON::String(format!("->{}", method_name.as_ref()).into()));
721
722                ArrowMethod::lookup(method_name).map_or_else(
723                    || {
724                        (
725                            None,
726                            vec![ApplyToError::new(
727                                format!("Method ->{} not found", method_name.as_ref()),
728                                method_path.to_vec(),
729                                method_name.range(),
730                                spec,
731                            )],
732                        )
733                    },
734                    |method| {
735                        let (result_opt, errors) = method.apply(
736                            method_name,
737                            method_args.as_ref(),
738                            data,
739                            vars,
740                            &method_path,
741                            spec,
742                        );
743
744                        // We special-case the ->as method here to avoid having
745                        // to give every -> method the ability to update
746                        // variables. The method.apply implementation for
747                        // ArrowMethod::As returns Some(json_object) where the
748                        // keys of json_object are variable names to update, and
749                        // the values are the values of those named variables.
750                        if let (ArrowMethod::As, Some(JSON::Object(bindings))) =
751                            (method, result_opt.as_ref())
752                        {
753                            let mut updated_vars = vars.clone();
754
755                            for (var_name, var_value) in bindings {
756                                updated_vars.insert(
757                                    KnownVariable::Local(var_name.as_str().to_string()),
758                                    // Should this InputPath include prior path information?
759                                    (var_value, InputPath::empty().append(json!(var_name))),
760                                );
761                            }
762
763                            return tail
764                                // We always pass the original input data to the
765                                // tail, no matter what ->as returned.
766                                .apply_to_path(data, &updated_vars, &method_path, spec)
767                                .prepend_errors(errors);
768                        }
769
770                        if let Some(result) = result_opt {
771                            tail.apply_to_path(&result, vars, &method_path, spec)
772                                .prepend_errors(errors)
773                        } else {
774                            // If the method produced no output, assume the errors
775                            // explain the None. Methods can legitimately produce
776                            // None without errors (like ->first or ->last on an
777                            // empty array), so we do not report any blanket error
778                            // here when errors.is_empty().
779                            (None, errors)
780                        }
781                    },
782                )
783            }
784            PathList::Selection(selection) => selection.apply_to_path(data, vars, input_path, spec),
785            PathList::Question(tail) => {
786                // Universal null check for any operation after ?
787                if data.is_null() {
788                    (None, vec![])
789                } else {
790                    tail.apply_to_path(data, vars, input_path, spec)
791                }
792            }
793            PathList::Empty => {
794                // If data is not an object here, we want to preserve its value
795                // without an error.
796                (Some(data.clone()), vec![])
797            }
798        }
799    }
800
801    fn compute_output_shape(
802        &self,
803        context: &ShapeContext,
804        input_shape: Shape,
805        dollar_shape: Shape,
806    ) -> Shape {
807        match input_shape.case() {
808            ShapeCase::One(shapes) => {
809                return Shape::one(
810                    shapes.iter().map(|shape| {
811                        self.compute_output_shape(context, shape.clone(), dollar_shape.clone())
812                    }),
813                    input_shape.locations().cloned(),
814                );
815            }
816            ShapeCase::All(shapes) => {
817                return Shape::all(
818                    shapes.iter().map(|shape| {
819                        self.compute_output_shape(context, shape.clone(), dollar_shape.clone())
820                    }),
821                    input_shape.locations().cloned(),
822                );
823            }
824            ShapeCase::Error(error) => {
825                return match error.partial.as_ref() {
826                    Some(partial) => Shape::error_with_partial(
827                        error.message.clone(),
828                        self.compute_output_shape(context, partial.clone(), dollar_shape),
829                        input_shape.locations().cloned(),
830                    ),
831                    None => input_shape.clone(),
832                };
833            }
834            _ => {}
835        };
836
837        // Given the base cases above, we can assume below that input_shape is
838        // neither ::One, ::All, nor ::Error.
839
840        let mut extra_vars_opt: Option<Shape> = None;
841        let (current_shape, tail_opt) = match self.as_ref() {
842            PathList::Var(ranged_var_name, tail) => {
843                let var_name = ranged_var_name.as_ref();
844                let var_shape = if var_name == &KnownVariable::AtSign {
845                    input_shape
846                } else if var_name == &KnownVariable::Dollar {
847                    dollar_shape.clone()
848                } else if let Some(shape) = context.named_shapes().get(var_name.as_str()) {
849                    shape.clone()
850                } else {
851                    Shape::name(
852                        var_name.as_str(),
853                        ranged_var_name.shape_location(context.source_id()),
854                    )
855                };
856                (var_shape, Some(tail))
857            }
858
859            // For the first key in a path, PathSelection::compute_output_shape
860            // will have set our input_shape equal to its dollar_shape, thereby
861            // ensuring that some.nested.path is equivalent to
862            // $.some.nested.path.
863            PathList::Key(key, tail) => {
864                if input_shape.is_none() {
865                    // If the previous path prefix evaluated to None, path
866                    // evaluation must terminate because we cannot select a key
867                    // from a missing input value.
868                    //
869                    // Any errors that might explain an unexpected None value
870                    // should have been reported as Shape::error_with_partial
871                    // errors at a higher level.
872                    //
873                    // Although PathList::Key selections always refer to $.key,
874                    // the input_shape here has already been set to $ in
875                    // PathSelection::compute_output_shape.
876                    return input_shape;
877                }
878
879                let child_shape = field(&input_shape, key, context.source_id());
880
881                // Here input_shape was not None, but input_shape.field(key) was
882                // None, so it's the responsibility of this PathList::Key node
883                // to report the missing property error. Elsewhere None may
884                // terminate path evaluation, but it does not necessarily
885                // trigger a Shape::error. Here, the shape system is telling us
886                // the key will never be found, so an error is warranted.
887                //
888                // In the future, we might allow tail to be a PathList::Question
889                // supporting optional ? chaining syntax, which would be a way
890                // of silencing this error when the key's absence is acceptable.
891                if child_shape.is_none() {
892                    return Shape::error(
893                        format!(
894                            "Property {} not found in {}",
895                            key.dotted(),
896                            input_shape.pretty_print()
897                        ),
898                        key.shape_location(context.source_id()),
899                    );
900                }
901
902                // Record the navigation path through this key step with
903                // `terminal = false`, so the structurally-traversed key
904                // appears in the consumption trie *as a non-leaf* — the
905                // path's actual leaf is recorded separately at the next
906                // `Empty` / `Method` boundary. This lets aliased
907                // selections like `users { name: $args.name }` capture
908                // `$root.users` as an internal node (necessary for
909                // upstream-existence guarantees) without marking it as a
910                // terminal selection.
911                context.record_consumption(&child_shape, false);
912
913                (child_shape, Some(tail))
914            }
915
916            PathList::Expr(expr, tail) => (
917                expr.compute_output_shape(context, input_shape, dollar_shape.clone()),
918                Some(tail),
919            ),
920
921            PathList::Method(method_name, method_args, tail) => {
922                if input_shape.is_none() {
923                    // If the previous path prefix evaluated to None, path
924                    // evaluation must terminate because -> methods never
925                    // execute against a missing/None input value.
926                    //
927                    // Any errors that might explain an unexpected None value
928                    // should have been reported as Shape::error_with_partial
929                    // errors at a higher level.
930                    return input_shape;
931                }
932
933                // The method consumes whatever the input path resolved to;
934                // record the input's accumulated name(s) into the trie before
935                // invoking the method, since `method.shape(...)` typically
936                // does not propagate the original name through to its result.
937                context.record_consumption(&input_shape, true);
938
939                if let Some(method) = ArrowMethod::lookup(method_name) {
940                    // Before connect/v0.3, we did not consult method.shape at
941                    // all, and instead returned Unknown. Since this behavior
942                    // has consequences for URI validation, the older behavior
943                    // is preserved/retrievable given ConnectSpec::V0_2/earlier.
944                    if context.spec() < ConnectSpec::V0_3 {
945                        (
946                            Shape::unknown(method_name.shape_location(context.source_id())),
947                            None,
948                        )
949                    } else {
950                        let result_shape = method.shape(
951                            context,
952                            method_name,
953                            method_args.as_ref(),
954                            input_shape.clone(),
955                            dollar_shape.clone(),
956                        );
957
958                        // We special-case ArrowMethod::As in apply_to_path, so
959                        // it makes sense to do so here as well.
960                        if method == ArrowMethod::As {
961                            // This is the only place we set extra_vars_opt to a
962                            // non-None value, which allows compute_tail_shape
963                            // to call context.with_named_shapes to make sure
964                            // $var gets defined in input->as($var)->echo($var).
965                            extra_vars_opt = Some(result_shape);
966                            (
967                                // We always apply the tail of an input->as(...)
968                                // method to the input shape, regardless of what
969                                // method.shape returned.
970                                input_shape,
971                                Some(tail),
972                            )
973                        } else {
974                            (result_shape, Some(tail))
975                        }
976                    }
977                } else {
978                    (
979                        Shape::error(
980                            format!("Method ->{} not found", method_name.as_str()),
981                            method_name.shape_location(context.source_id()),
982                        ),
983                        None,
984                    )
985                }
986            }
987
988            PathList::Question(tail) => {
989                let q_shape = input_shape.question(self.shape_location(context.source_id()));
990                (
991                    if tail.is_empty() {
992                        // If there is no tail, we do not need to account for
993                        // the possibility that the whole path might evaluate to
994                        // None, as that possibility will be encoded in the
995                        // computed shape for this terminal/leaf ::Question.
996                        q_shape
997                    } else {
998                        // Using the ? operator with a non-empty tail suggests
999                        // the input shape could evaluate to None, so we always
1000                        // include None as a possible shape here. If we don't
1001                        // entertain this possibility, we might compute a
1002                        // non-optional object shape with missing fields instead
1003                        // of correctly computing One<{...}, None>.
1004                        Shape::one([q_shape, Shape::none()], [])
1005                    },
1006                    Some(tail),
1007                )
1008            }
1009
1010            PathList::Selection(selection) => {
1011                if input_shape.is_none() {
1012                    // We do not require that the input shape for a SubSelection
1013                    // must be an object (as it will be bound to $ and @
1014                    // regardless), but we skip executing the SubSelection
1015                    // if/when the input shape is None, since we can't bind None
1016                    // to $ or @.
1017                    return input_shape;
1018                }
1019
1020                // The path terminates here — `Selection` is a leaf-position
1021                // for the outer path (no `tail` after it). Record the input
1022                // path as terminal so the trie has a leaf at the
1023                // navigation's endpoint, matching the walker's behavior of
1024                // marking a key followed by a subselection as leaf-with-
1025                // children. Subsequent recursion into `selection` records
1026                // additional consumption (e.g. the subselection's own
1027                // fields) on top.
1028                context.record_consumption(&input_shape, true);
1029
1030                (
1031                    selection.compute_output_shape(context, input_shape, dollar_shape.clone()),
1032                    None,
1033                )
1034            }
1035
1036            PathList::Empty => {
1037                // Terminal step — the accumulated name(s) on `input_shape`
1038                // describe the deepest input path the surrounding path
1039                // expression consumed. Record them into the trie.
1040                context.record_consumption(&input_shape, true);
1041                (input_shape, None)
1042            }
1043        };
1044
1045        if let Some(tail) = tail_opt {
1046            // Recurses over extra_vars_opt, which is usually None, but could be
1047            // Some(object_shape) (when handling ArrowMethod::As), and might
1048            // sometimes be Some(error_shape) with an object partial shape.
1049            fn compute_tail_shape(
1050                tail: &WithRange<PathList>,
1051                extra_vars_opt: &Option<Shape>,
1052                context: &ShapeContext,
1053                input_shape: Shape,
1054                dollar_shape: Shape,
1055            ) -> Shape {
1056                match extra_vars_opt.as_ref().map(|s| s.case()) {
1057                    Some(ShapeCase::Object { fields, .. }) => {
1058                        // TODO Refactor the internal ShapeContext
1059                        // representation to make this cloning
1060                        // unnecessary/cheaper.
1061                        let new_context = context.clone().with_named_shapes(
1062                            fields
1063                                .iter()
1064                                .map(|(name, shape)| (name.clone(), shape.clone())),
1065                        );
1066                        tail.compute_output_shape(&new_context, input_shape, dollar_shape)
1067                    }
1068
1069                    Some(ShapeCase::Error(shape::Error { message, partial })) => {
1070                        if partial.is_some() {
1071                            let tail_shape = compute_tail_shape(
1072                                tail,
1073                                partial,
1074                                context,
1075                                input_shape,
1076                                dollar_shape,
1077                            );
1078
1079                            Shape::error_with_partial(
1080                                message.clone(),
1081                                tail_shape,
1082                                tail.shape_location(context.source_id()),
1083                            )
1084                        } else {
1085                            Shape::error(message.clone(), tail.shape_location(context.source_id()))
1086                        }
1087                    }
1088
1089                    _ => tail.compute_output_shape(context, input_shape, dollar_shape),
1090                }
1091            }
1092
1093            compute_tail_shape(
1094                tail,
1095                &extra_vars_opt,
1096                context,
1097                // Note: current_shape gets renamed to input_shape within the
1098                // compute_tail_shape function defined above.
1099                current_shape,
1100                dollar_shape,
1101            )
1102        } else {
1103            current_shape
1104        }
1105    }
1106}
1107
1108impl ApplyToInternal for WithRange<LitExpr> {
1109    fn apply_to_path(
1110        &self,
1111        data: &JSON,
1112        vars: &VarsWithPathsMap,
1113        input_path: &InputPath<JSON>,
1114        spec: ConnectSpec,
1115    ) -> (Option<JSON>, Vec<ApplyToError>) {
1116        match self.as_ref() {
1117            LitExpr::String(s) => (Some(JSON::String(s.clone().into())), vec![]),
1118            LitExpr::Number(n) => (Some(JSON::Number(n.clone())), vec![]),
1119            LitExpr::Bool(b) => (Some(JSON::Bool(*b)), vec![]),
1120            LitExpr::Null => (Some(JSON::Null), vec![]),
1121            LitExpr::LegacyObject(map) => {
1122                let mut output = JSONMap::with_capacity(map.len());
1123                let mut errors = Vec::new();
1124                for (key, value) in map {
1125                    let (value_opt, apply_errors) =
1126                        value.apply_to_path(data, vars, input_path, spec);
1127                    errors.extend(apply_errors);
1128                    if let Some(value_json) = value_opt {
1129                        output.insert(key.as_str(), value_json);
1130                    }
1131                }
1132                (Some(JSON::Object(output)), errors)
1133            }
1134            LitExpr::Object(sub) => {
1135                // LitExpr::Object shares grammar with SubSelection but NOT its
1136                // `$` rebinding: `$` inside a literal object stays bound to
1137                // the surrounding data. Nested `Key SubSelection` syntax
1138                // (e.g. `{ foo { bar } }`) still rebinds `$` via the inner
1139                // SubSelection, which is handled by NamedSelection itself.
1140                let (output, errors) = sub.apply_selections_no_rebind(data, vars, input_path, spec);
1141                (Some(output), errors)
1142            }
1143            LitExpr::Array(vec) => {
1144                let mut output = Vec::with_capacity(vec.len());
1145                let mut errors = Vec::new();
1146                for value in vec {
1147                    let (value_opt, apply_errors) =
1148                        value.apply_to_path(data, vars, input_path, spec);
1149                    errors.extend(apply_errors);
1150                    output.push(value_opt.unwrap_or(JSON::Null));
1151                }
1152                (Some(JSON::Array(output)), errors)
1153            }
1154            LitExpr::Path(path) => path.apply_to_path(data, vars, input_path, spec),
1155            LitExpr::LitPath(literal, subpath) => literal
1156                .apply_to_path(data, vars, input_path, spec)
1157                .and_then_collecting_errors(|value| {
1158                    subpath.apply_to_path(value, vars, input_path, spec)
1159                }),
1160            LitExpr::OpChain(op, operands) => {
1161                match op.as_ref() {
1162                    LitOp::NullishCoalescing => {
1163                        // Null coalescing: A ?? B ?? C
1164                        // Returns B if A is null OR None, otherwise A. If B is also null/None, returns C, etc.
1165                        let mut accumulated_errors = Vec::new();
1166                        let mut last_value: Option<JSON> = None;
1167
1168                        for operand in operands {
1169                            let (value, errors) =
1170                                operand.apply_to_path(data, vars, input_path, spec);
1171
1172                            match value {
1173                                // If we get a non-null, non-None value, return it
1174                                Some(JSON::Null) | None => {
1175                                    // Accumulate errors but continue to next operand
1176                                    accumulated_errors.extend(errors);
1177                                    last_value = value;
1178                                    continue;
1179                                }
1180                                Some(value) => {
1181                                    // Found a non-null/non-None value, return it (ignoring accumulated errors)
1182                                    return (Some(value), errors);
1183                                }
1184                            }
1185                        }
1186
1187                        // If the last value was Some(JSON::Null), we return
1188                        // that null, since there is no ?? after it. Otherwise,
1189                        // last_value will be None at this point, because we
1190                        // return Some(value) above as soon as we find a
1191                        // non-null/non-None value.
1192                        if last_value.is_none() {
1193                            // If we never found a non-null value, return None
1194                            // with all accumulated errors.
1195                            (None, accumulated_errors)
1196                        } else {
1197                            // If the last operand evaluated to null (or
1198                            // anything else except None), that counts as a
1199                            // successful evaluation, so we do not return any
1200                            // earlier accumulated_errors.
1201                            (last_value, Vec::new())
1202                        }
1203                    }
1204
1205                    LitOp::NoneCoalescing => {
1206                        // None coalescing: A ?! B ?! C
1207                        // Returns B if A is None (preserves null), otherwise A. If B is also None, returns C, etc.
1208                        let mut accumulated_errors = Vec::new();
1209
1210                        for operand in operands {
1211                            let (value, errors) =
1212                                operand.apply_to_path(data, vars, input_path, spec);
1213
1214                            match value {
1215                                // If we get None, continue to next operand
1216                                None => {
1217                                    accumulated_errors.extend(errors);
1218                                    continue;
1219                                }
1220                                // If we get any value (including null), return it
1221                                Some(value) => {
1222                                    return (Some(value), errors);
1223                                }
1224                            }
1225                        }
1226
1227                        // All operands were None, return None with all accumulated errors
1228                        (None, accumulated_errors)
1229                    }
1230                }
1231            }
1232        }
1233    }
1234
1235    fn compute_output_shape(
1236        &self,
1237        context: &ShapeContext,
1238        input_shape: Shape,
1239        dollar_shape: Shape,
1240    ) -> Shape {
1241        let locations = self.shape_location(context.source_id());
1242
1243        match self.as_ref() {
1244            LitExpr::Null => Shape::null(locations),
1245            LitExpr::Bool(value) => Shape::bool_value(*value, locations),
1246            LitExpr::String(value) => Shape::string_value(value.as_str(), locations),
1247
1248            LitExpr::Number(value) => {
1249                if let Some(n) = value.as_i64() {
1250                    Shape::int_value(n, locations)
1251                } else if value.is_f64() {
1252                    Shape::float(locations)
1253                } else {
1254                    Shape::error("Number neither Int nor Float", locations)
1255                }
1256            }
1257
1258            LitExpr::LegacyObject(map) => {
1259                let mut fields = Shape::empty_map();
1260                for (key, value) in map {
1261                    fields.insert(
1262                        key.as_string(),
1263                        value.compute_output_shape(
1264                            context,
1265                            input_shape.clone(),
1266                            dollar_shape.clone(),
1267                        ),
1268                    );
1269                }
1270                Shape::object(fields, Shape::none(), locations)
1271            }
1272
1273            LitExpr::Object(sub) => {
1274                // Mirror the LitExpr::Object apply_to_path semantics: iterate
1275                // NamedSelection shapes against the outer input/dollar shapes
1276                // rather than rebinding $ (which SubSelection would do).
1277                sub.compute_selections_shape_no_rebind(context, input_shape, dollar_shape)
1278            }
1279
1280            LitExpr::Array(vec) => {
1281                let mut shapes = Vec::with_capacity(vec.len());
1282                for value in vec {
1283                    shapes.push(value.compute_output_shape(
1284                        context,
1285                        input_shape.clone(),
1286                        dollar_shape.clone(),
1287                    ));
1288                }
1289                Shape::array(shapes, Shape::none(), locations)
1290            }
1291
1292            LitExpr::Path(path) => path.compute_output_shape(context, input_shape, dollar_shape),
1293
1294            LitExpr::LitPath(literal, subpath) => {
1295                let literal_shape =
1296                    literal.compute_output_shape(context, input_shape, dollar_shape.clone());
1297                subpath.compute_output_shape(context, literal_shape, dollar_shape)
1298            }
1299
1300            LitExpr::OpChain(op, operands) => {
1301                let mut shapes: Vec<Shape> = operands
1302                    .iter()
1303                    .map(|operand| {
1304                        operand.compute_output_shape(
1305                            context,
1306                            input_shape.clone(),
1307                            dollar_shape.clone(),
1308                        )
1309                    })
1310                    .collect();
1311
1312                match op.as_ref() {
1313                    LitOp::NullishCoalescing => {
1314                        if let Some(last_shape) = shapes.pop() {
1315                            let mut new_shapes = shapes
1316                                .iter()
1317                                .map(|shape| {
1318                                    shape
1319                                        .question(locations.clone())
1320                                        .not_none(locations.clone())
1321                                })
1322                                .collect::<Vec<_>>();
1323                            new_shapes.push(last_shape);
1324                            Shape::one(new_shapes, locations)
1325                        } else {
1326                            Shape::one(shapes, locations)
1327                        }
1328                    }
1329
1330                    // Just like NullishCoalescing except null is not excluded.
1331                    LitOp::NoneCoalescing => {
1332                        if let Some(last_shape) = shapes.pop() {
1333                            let mut new_shapes = shapes
1334                                .iter()
1335                                .map(|shape| shape.not_none(locations.clone()))
1336                                .collect::<Vec<_>>();
1337                            new_shapes.push(last_shape);
1338                            Shape::one(new_shapes, locations)
1339                        } else {
1340                            Shape::one(shapes, locations)
1341                        }
1342                    }
1343                }
1344            }
1345        }
1346    }
1347}
1348
1349impl SubSelection {
1350    /// Apply each [`NamedSelection`] in `self.selections` to `data`, merging
1351    /// outputs into an accumulating object. Unlike
1352    /// [`ApplyToInternal::apply_to_path`] on `SubSelection`, this does **not**
1353    /// rebind `$` — `vars` is threaded to each inner selection as-is. Callers
1354    /// that want `SubSelection`'s `$`-to-`data` binding must prepare `vars`
1355    /// before calling.
1356    fn apply_selections_no_rebind(
1357        &self,
1358        data: &JSON,
1359        vars: &VarsWithPathsMap,
1360        input_path: &InputPath<JSON>,
1361        spec: ConnectSpec,
1362    ) -> (JSON, Vec<ApplyToError>) {
1363        let mut output = JSON::Object(JSONMap::new());
1364        let mut errors = Vec::new();
1365
1366        for named_selection in self.selections.iter() {
1367            let (named_output_opt, apply_errors) =
1368                named_selection.apply_to_path(data, vars, input_path, spec);
1369            errors.extend(apply_errors);
1370
1371            let (merged, merge_errors) = json_merge(Some(&output), named_output_opt.as_ref());
1372
1373            errors.extend(merge_errors.into_iter().map(|message| {
1374                ApplyToError::new(message, input_path.to_vec(), self.range(), spec)
1375            }));
1376
1377            if let Some(merged) = merged {
1378                output = merged;
1379            }
1380        }
1381
1382        (output, errors)
1383    }
1384
1385    /// Shape counterpart of [`Self::apply_selections_no_rebind`]: merge the
1386    /// output shapes of each [`NamedSelection`] with [`Shape::all`] without
1387    /// rebinding `$`. Callers pass the `dollar_shape` they want threaded.
1388    fn compute_selections_shape_no_rebind(
1389        &self,
1390        context: &ShapeContext,
1391        input_shape: Shape,
1392        dollar_shape: Shape,
1393    ) -> Shape {
1394        let locations = self.shape_location(context.source_id());
1395        let mut all_shape = Shape::unknown([]);
1396
1397        for named_selection in self.selections.iter() {
1398            // Simplifying as we go with Shape::all keeps all_shape relatively
1399            // small in the common case when all named_selection items return an
1400            // object shape, since those object shapes can all be merged
1401            // together into one object.
1402            all_shape = Shape::all(
1403                [
1404                    all_shape,
1405                    named_selection.compute_output_shape(
1406                        context,
1407                        input_shape.clone(),
1408                        dollar_shape.clone(),
1409                    ),
1410                ],
1411                locations.clone(),
1412            );
1413
1414            // If any named_selection item returns null instead of an object,
1415            // that nullifies the whole object and allows shape computation to
1416            // bail out early.
1417            if all_shape.is_null() {
1418                break;
1419            }
1420        }
1421
1422        if all_shape.is_unknown() {
1423            Shape::empty_object(locations)
1424        } else {
1425            all_shape
1426        }
1427    }
1428}
1429
1430impl ApplyToInternal for SubSelection {
1431    fn apply_to_path(
1432        &self,
1433        data: &JSON,
1434        vars: &VarsWithPathsMap,
1435        input_path: &InputPath<JSON>,
1436        spec: ConnectSpec,
1437    ) -> (Option<JSON>, Vec<ApplyToError>) {
1438        if let JSON::Array(array) = data {
1439            return self.apply_to_array(array, vars, input_path, spec);
1440        }
1441
1442        let vars: VarsWithPathsMap = {
1443            let mut vars = vars.clone();
1444            vars.insert(KnownVariable::Dollar, (data, input_path.clone()));
1445            vars
1446        };
1447
1448        let (output, errors) = self.apply_selections_no_rebind(data, &vars, input_path, spec);
1449
1450        if !matches!(data, JSON::Object(_)) {
1451            let output_is_empty = match &output {
1452                JSON::Object(map) => map.is_empty(),
1453                _ => false,
1454            };
1455            if output_is_empty {
1456                // If data was a primitive value (neither array nor object), and
1457                // no output properties were generated, return data as is, along
1458                // with any errors that occurred.
1459                return (Some(data.clone()), errors);
1460            }
1461        }
1462
1463        (Some(output), errors)
1464    }
1465
1466    fn compute_output_shape(
1467        &self,
1468        context: &ShapeContext,
1469        input_shape: Shape,
1470        _previous_dollar_shape: Shape,
1471    ) -> Shape {
1472        // Just as SubSelection::apply_to_path calls apply_to_array when data is
1473        // an array, so compute_output_shape recursively computes the output
1474        // shapes of each array element shape.
1475        if let ShapeCase::Array { prefix, tail } = input_shape.case() {
1476            let new_prefix = prefix
1477                .iter()
1478                .map(|shape| self.compute_output_shape(context, shape.clone(), shape.clone()))
1479                .collect::<Vec<_>>();
1480
1481            let new_tail = if tail.is_none() {
1482                tail.clone()
1483            } else {
1484                self.compute_output_shape(context, tail.clone(), tail.clone())
1485            };
1486
1487            return Shape::array(
1488                new_prefix,
1489                new_tail,
1490                self.shape_location(context.source_id()),
1491            );
1492        }
1493
1494        // If the input shape is a named shape, it might end up being an array,
1495        // so we need to hedge the output shape using a wildcard that maps over
1496        // array elements.
1497        let input_shape = input_shape.any_item(Vec::new());
1498
1499        // The SubSelection rebinds the $ variable to the selected input object,
1500        // so we can ignore _previous_dollar_shape.
1501        let dollar_shape = input_shape.clone();
1502
1503        self.compute_selections_shape_no_rebind(context, input_shape, dollar_shape)
1504    }
1505}
1506
1507impl WithRange<PathList> {
1508    /// Build a [`SelectionTrie`] describing every input path under
1509    /// `namespace` (the variable name including the `$` prefix, e.g.
1510    /// `"$this"`) that this tail consumes. Uses the fused-trie machinery in
1511    /// [`Self::compute_output_shape`], so it correctly traverses through
1512    /// arrow methods, conditional operators (`?`), nested subselections
1513    /// (`$this.items->filter(...) { id name }`), and any predicate
1514    /// sub-expressions inside method arguments — all of which the legacy
1515    /// trie walker treats as opaque leaves.
1516    pub(crate) fn compute_consumption_trie(&self, namespace: &str) -> SelectionTrie {
1517        let context = ShapeContext::new(SourceId::Other("VariableReference".into()));
1518        let var_shape = Shape::name(namespace, Vec::new());
1519
1520        // The recursion records into `context.consumption()` as a byproduct
1521        // of computing the output shape; we discard the output shape here.
1522        let _ = self.compute_output_shape(&context, var_shape.clone(), var_shape);
1523
1524        context
1525            .consumption()
1526            .borrow()
1527            .get(namespace)
1528            .cloned()
1529            .unwrap_or_else(SelectionTrie::new)
1530    }
1531}
1532
1533/// Helper to get the field from a shape or error if the object doesn't have that field.
1534fn field(shape: &Shape, key: &WithRange<Key>, source_id: &SourceId) -> Shape {
1535    if let ShapeCase::One(inner) = shape.case() {
1536        let mut new_fields = Vec::new();
1537        for inner_field in inner.iter() {
1538            new_fields.push(field(inner_field, key, source_id));
1539        }
1540        return Shape::one(new_fields, shape.locations().cloned());
1541    }
1542    if shape.is_none() || shape.is_null() {
1543        return Shape::none();
1544    }
1545    let field_shape = shape.field(key.as_str(), key.shape_location(source_id));
1546    if field_shape.is_none() {
1547        return Shape::error(
1548            format!("field `{field}` not found", field = key.as_str()),
1549            key.shape_location(source_id),
1550        );
1551    }
1552    field_shape
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557    use rstest::rstest;
1558
1559    use super::*;
1560    use crate::assert_debug_snapshot;
1561    use crate::connectors::json_selection::PrettyPrintable;
1562    use crate::selection;
1563
1564    #[rstest]
1565    #[case::v0_2(ConnectSpec::V0_2)]
1566    #[case::v0_3(ConnectSpec::V0_3)]
1567    #[case::v0_4(ConnectSpec::V0_4)]
1568    fn test_apply_to_selection(#[case] spec: ConnectSpec) {
1569        let data = json!({
1570            "hello": "world",
1571            "nested": {
1572                "hello": "world",
1573                "world": "hello",
1574            },
1575            "array": [
1576                { "hello": "world 0" },
1577                { "hello": "world 1" },
1578                { "hello": "world 2" },
1579            ],
1580        });
1581
1582        #[track_caller]
1583        fn check_ok(data: &JSON, selection: JSONSelection, expected_json: JSON) {
1584            let (actual_json, errors) = selection.apply_to(data);
1585            assert_eq!(actual_json, Some(expected_json));
1586            assert_eq!(errors, vec![]);
1587        }
1588
1589        check_ok(&data, selection!("hello", spec), json!({"hello": "world"}));
1590
1591        check_ok(
1592            &data,
1593            selection!("nested", spec),
1594            json!({
1595                "nested": {
1596                    "hello": "world",
1597                    "world": "hello",
1598                },
1599            }),
1600        );
1601
1602        check_ok(&data, selection!("nested.hello", spec), json!("world"));
1603        check_ok(&data, selection!("$.nested.hello", spec), json!("world"));
1604
1605        check_ok(&data, selection!("nested.world", spec), json!("hello"));
1606        check_ok(&data, selection!("$.nested.world", spec), json!("hello"));
1607
1608        check_ok(
1609            &data,
1610            selection!("nested hello", spec),
1611            json!({
1612                "hello": "world",
1613                "nested": {
1614                    "hello": "world",
1615                    "world": "hello",
1616                },
1617            }),
1618        );
1619
1620        check_ok(
1621            &data,
1622            selection!("array { hello }", spec),
1623            json!({
1624                "array": [
1625                    { "hello": "world 0" },
1626                    { "hello": "world 1" },
1627                    { "hello": "world 2" },
1628                ],
1629            }),
1630        );
1631
1632        check_ok(
1633            &data,
1634            selection!("greetings: array { hello }", spec),
1635            json!({
1636                "greetings": [
1637                    { "hello": "world 0" },
1638                    { "hello": "world 1" },
1639                    { "hello": "world 2" },
1640                ],
1641            }),
1642        );
1643
1644        check_ok(
1645            &data,
1646            selection!("$.array { hello }", spec),
1647            json!([
1648                { "hello": "world 0" },
1649                { "hello": "world 1" },
1650                { "hello": "world 2" },
1651            ]),
1652        );
1653
1654        check_ok(
1655            &data,
1656            selection!("worlds: array.hello", spec),
1657            json!({
1658                "worlds": [
1659                    "world 0",
1660                    "world 1",
1661                    "world 2",
1662                ],
1663            }),
1664        );
1665
1666        check_ok(
1667            &data,
1668            selection!("worlds: $.array.hello", spec),
1669            json!({
1670                "worlds": [
1671                    "world 0",
1672                    "world 1",
1673                    "world 2",
1674                ],
1675            }),
1676        );
1677
1678        check_ok(
1679            &data,
1680            selection!("array.hello", spec),
1681            json!(["world 0", "world 1", "world 2",]),
1682        );
1683
1684        check_ok(
1685            &data,
1686            selection!("$.array.hello", spec),
1687            json!(["world 0", "world 1", "world 2",]),
1688        );
1689
1690        check_ok(
1691            &data,
1692            selection!("nested grouped: { hello worlds: array.hello }", spec),
1693            json!({
1694                "nested": {
1695                    "hello": "world",
1696                    "world": "hello",
1697                },
1698                "grouped": {
1699                    "hello": "world",
1700                    "worlds": [
1701                        "world 0",
1702                        "world 1",
1703                        "world 2",
1704                    ],
1705                },
1706            }),
1707        );
1708
1709        check_ok(
1710            &data,
1711            selection!("nested grouped: { hello worlds: $.array.hello }", spec),
1712            json!({
1713                "nested": {
1714                    "hello": "world",
1715                    "world": "hello",
1716                },
1717                "grouped": {
1718                    "hello": "world",
1719                    "worlds": [
1720                        "world 0",
1721                        "world 1",
1722                        "world 2",
1723                    ],
1724                },
1725            }),
1726        );
1727    }
1728
1729    #[rstest]
1730    #[case::v0_2(ConnectSpec::V0_2)]
1731    #[case::v0_3(ConnectSpec::V0_3)]
1732    #[case::v0_4(ConnectSpec::V0_4)]
1733    fn test_apply_to_errors(#[case] spec: ConnectSpec) {
1734        let data = json!({
1735            "hello": "world",
1736            "nested": {
1737                "hello": 123,
1738                "world": true,
1739            },
1740            "array": [
1741                { "hello": 1, "goodbye": "farewell" },
1742                { "hello": "two" },
1743                { "hello": 3.0, "smello": "yellow" },
1744            ],
1745        });
1746
1747        assert_eq!(
1748            selection!("hello", spec).apply_to(&data),
1749            (Some(json!({"hello": "world"})), vec![],)
1750        );
1751
1752        fn make_yellow_errors_expected(
1753            yellow_range: std::ops::Range<usize>,
1754            spec: ConnectSpec,
1755        ) -> Vec<ApplyToError> {
1756            vec![ApplyToError::new(
1757                "Property .yellow not found in object".to_string(),
1758                vec![json!("yellow")],
1759                Some(yellow_range),
1760                spec,
1761            )]
1762        }
1763        assert_eq!(
1764            selection!("yellow", spec).apply_to(&data),
1765            (Some(json!({})), make_yellow_errors_expected(0..6, spec)),
1766        );
1767        assert_eq!(
1768            selection!("$.yellow", spec).apply_to(&data),
1769            (None, make_yellow_errors_expected(2..8, spec)),
1770        );
1771
1772        assert_eq!(
1773            selection!("nested.hello", spec).apply_to(&data),
1774            (Some(json!(123)), vec![],)
1775        );
1776
1777        fn make_quoted_yellow_expected(
1778            yellow_range: std::ops::Range<usize>,
1779            spec: ConnectSpec,
1780        ) -> (Option<JSON>, Vec<ApplyToError>) {
1781            (
1782                None,
1783                vec![ApplyToError::new(
1784                    "Property .\"yellow\" not found in object".to_string(),
1785                    vec![json!("nested"), json!("yellow")],
1786                    Some(yellow_range),
1787                    spec,
1788                )],
1789            )
1790        }
1791        assert_eq!(
1792            selection!("nested.'yellow'", spec).apply_to(&data),
1793            make_quoted_yellow_expected(7..15, spec),
1794        );
1795        assert_eq!(
1796            selection!("nested.\"yellow\"", spec).apply_to(&data),
1797            make_quoted_yellow_expected(7..15, spec),
1798        );
1799        assert_eq!(
1800            selection!("$.nested.'yellow'", spec).apply_to(&data),
1801            make_quoted_yellow_expected(9..17, spec),
1802        );
1803
1804        fn make_nested_path_expected(
1805            hola_range: (usize, usize),
1806            yellow_range: (usize, usize),
1807            spec: ConnectSpec,
1808        ) -> (Option<JSON>, Vec<ApplyToError>) {
1809            (
1810                Some(json!({
1811                    "world": true,
1812                })),
1813                vec![
1814                    ApplyToError::from_json(&json!({
1815                        "message": "Property .hola not found in object",
1816                        "path": ["nested", "hola"],
1817                        "range": hola_range,
1818                        "spec": spec.to_string(),
1819                    })),
1820                    ApplyToError::from_json(&json!({
1821                        "message": "Property .yellow not found in object",
1822                        "path": ["nested", "yellow"],
1823                        "range": yellow_range,
1824                        "spec": spec.to_string(),
1825                    })),
1826                ],
1827            )
1828        }
1829        assert_eq!(
1830            selection!("$.nested { hola yellow world }", spec).apply_to(&data),
1831            make_nested_path_expected((11, 15), (16, 22), spec),
1832        );
1833        assert_eq!(
1834            selection!(" $ . nested { hola yellow world } ", spec).apply_to(&data),
1835            make_nested_path_expected((14, 18), (19, 25), spec),
1836        );
1837
1838        fn make_partial_array_expected(
1839            goodbye_range: (usize, usize),
1840            spec: ConnectSpec,
1841        ) -> (Option<JSON>, Vec<ApplyToError>) {
1842            (
1843                Some(json!({
1844                    "partial": [
1845                        { "hello": 1, "goodbye": "farewell" },
1846                        { "hello": "two" },
1847                        { "hello": 3.0 },
1848                    ],
1849                })),
1850                vec![
1851                    ApplyToError::from_json(&json!({
1852                        "message": "Property .goodbye not found in object",
1853                        "path": ["array", 1, "goodbye"],
1854                        "range": goodbye_range,
1855                        "spec": spec.to_string(),
1856                    })),
1857                    ApplyToError::from_json(&json!({
1858                        "message": "Property .goodbye not found in object",
1859                        "path": ["array", 2, "goodbye"],
1860                        "range": goodbye_range,
1861                        "spec": spec.to_string(),
1862                    })),
1863                ],
1864            )
1865        }
1866        assert_eq!(
1867            selection!("partial: $.array { hello goodbye }", spec).apply_to(&data),
1868            make_partial_array_expected((25, 32), spec),
1869        );
1870        assert_eq!(
1871            selection!(" partial : $ . array { hello goodbye } ", spec).apply_to(&data),
1872            make_partial_array_expected((29, 36), spec),
1873        );
1874
1875        assert_eq!(
1876            selection!("good: array.hello bad: array.smello", spec).apply_to(&data),
1877            (
1878                Some(json!({
1879                    "good": [
1880                        1,
1881                        "two",
1882                        3.0,
1883                    ],
1884                    "bad": [
1885                        null,
1886                        null,
1887                        "yellow",
1888                    ],
1889                })),
1890                vec![
1891                    ApplyToError::from_json(&json!({
1892                        "message": "Property .smello not found in object",
1893                        "path": ["array", 0, "smello"],
1894                        "range": [29, 35],
1895                        "spec": spec.to_string(),
1896                    })),
1897                    ApplyToError::from_json(&json!({
1898                        "message": "Property .smello not found in object",
1899                        "path": ["array", 1, "smello"],
1900                        "range": [29, 35],
1901                        "spec": spec.to_string(),
1902                    })),
1903                ],
1904            )
1905        );
1906
1907        assert_eq!(
1908            selection!("array { hello smello }", spec).apply_to(&data),
1909            (
1910                Some(json!({
1911                    "array": [
1912                        { "hello": 1 },
1913                        { "hello": "two" },
1914                        { "hello": 3.0, "smello": "yellow" },
1915                    ],
1916                })),
1917                vec![
1918                    ApplyToError::from_json(&json!({
1919                        "message": "Property .smello not found in object",
1920                        "path": ["array", 0, "smello"],
1921                        "range": [14, 20],
1922                        "spec": spec.to_string(),
1923                    })),
1924                    ApplyToError::from_json(&json!({
1925                        "message": "Property .smello not found in object",
1926                        "path": ["array", 1, "smello"],
1927                        "range": [14, 20],
1928                        "spec": spec.to_string(),
1929                    })),
1930                ],
1931            )
1932        );
1933
1934        assert_eq!(
1935            selection!("$.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1936            (
1937                Some(json!({
1938                    "grouped": {
1939                        "hello": 123,
1940                        "world": true,
1941                    },
1942                })),
1943                vec![ApplyToError::from_json(&json!({
1944                    "message": "Property .smelly not found in object",
1945                    "path": ["nested", "smelly"],
1946                    "range": [28, 34],
1947                    "spec": spec.to_string(),
1948                }))],
1949            )
1950        );
1951
1952        assert_eq!(
1953            selection!("alias: $.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1954            (
1955                Some(json!({
1956                    "alias": {
1957                        "grouped": {
1958                            "hello": 123,
1959                            "world": true,
1960                        },
1961                    },
1962                })),
1963                vec![ApplyToError::from_json(&json!({
1964                    "message": "Property .smelly not found in object",
1965                    "path": ["nested", "smelly"],
1966                    "range": [35, 41],
1967                    "spec": spec.to_string(),
1968                }))],
1969            )
1970        );
1971    }
1972
1973    #[rstest]
1974    #[case::v0_2(ConnectSpec::V0_2)]
1975    #[case::v0_3(ConnectSpec::V0_3)]
1976    #[case::v0_4(ConnectSpec::V0_4)]
1977    fn test_apply_to_nested_arrays(#[case] spec: ConnectSpec) {
1978        let data = json!({
1979            "arrayOfArrays": [
1980                [
1981                    { "x": 0, "y": 0 },
1982                ],
1983                [
1984                    { "x": 1, "y": 0 },
1985                    { "x": 1, "y": 1 },
1986                    { "x": 1, "y": 2 },
1987                ],
1988                [
1989                    { "x": 2, "y": 0 },
1990                    { "x": 2, "y": 1 },
1991                ],
1992                [],
1993                [
1994                    null,
1995                    { "x": 4, "y": 1 },
1996                    { "x": 4, "why": 2 },
1997                    null,
1998                    { "x": 4, "y": 4 },
1999                ]
2000            ],
2001        });
2002
2003        fn make_array_of_arrays_x_expected(
2004            x_range: (usize, usize),
2005            spec: ConnectSpec,
2006        ) -> (Option<JSON>, Vec<ApplyToError>) {
2007            (
2008                Some(json!([[0], [1, 1, 1], [2, 2], [], [null, 4, 4, null, 4]])),
2009                vec![
2010                    ApplyToError::from_json(&json!({
2011                        "message": "Property .x not found in null",
2012                        "path": ["arrayOfArrays", 4, 0, "x"],
2013                        "range": x_range,
2014                        "spec": spec.to_string(),
2015                    })),
2016                    ApplyToError::from_json(&json!({
2017                        "message": "Property .x not found in null",
2018                        "path": ["arrayOfArrays", 4, 3, "x"],
2019                        "range": x_range,
2020                        "spec": spec.to_string(),
2021                    })),
2022                ],
2023            )
2024        }
2025        assert_eq!(
2026            selection!("arrayOfArrays.x", spec).apply_to(&data),
2027            make_array_of_arrays_x_expected((14, 15), spec),
2028        );
2029        assert_eq!(
2030            selection!("$.arrayOfArrays.x", spec).apply_to(&data),
2031            make_array_of_arrays_x_expected((16, 17), spec),
2032        );
2033
2034        fn make_array_of_arrays_y_expected(
2035            y_range: (usize, usize),
2036            spec: ConnectSpec,
2037        ) -> (Option<JSON>, Vec<ApplyToError>) {
2038            (
2039                Some(json!([
2040                    [0],
2041                    [0, 1, 2],
2042                    [0, 1],
2043                    [],
2044                    [null, 1, null, null, 4],
2045                ])),
2046                vec![
2047                    ApplyToError::from_json(&json!({
2048                        "message": "Property .y not found in null",
2049                        "path": ["arrayOfArrays", 4, 0, "y"],
2050                        "range": y_range,
2051                        "spec": spec.to_string(),
2052                    })),
2053                    ApplyToError::from_json(&json!({
2054                        "message": "Property .y not found in object",
2055                        "path": ["arrayOfArrays", 4, 2, "y"],
2056                        "range": y_range,
2057                        "spec": spec.to_string(),
2058                    })),
2059                    ApplyToError::from_json(&json!({
2060                        "message": "Property .y not found in null",
2061                        "path": ["arrayOfArrays", 4, 3, "y"],
2062                        "range": y_range,
2063                        "spec": spec.to_string(),
2064                    })),
2065                ],
2066            )
2067        }
2068        assert_eq!(
2069            selection!("arrayOfArrays.y", spec).apply_to(&data),
2070            make_array_of_arrays_y_expected((14, 15), spec),
2071        );
2072        assert_eq!(
2073            selection!("$.arrayOfArrays.y", spec).apply_to(&data),
2074            make_array_of_arrays_y_expected((16, 17), spec),
2075        );
2076
2077        assert_eq!(
2078            selection!("alias: arrayOfArrays { x y }", spec).apply_to(&data),
2079            (
2080                Some(json!({
2081                    "alias": [
2082                        [
2083                            { "x": 0, "y": 0 },
2084                        ],
2085                        [
2086                            { "x": 1, "y": 0 },
2087                            { "x": 1, "y": 1 },
2088                            { "x": 1, "y": 2 },
2089                        ],
2090                        [
2091                            { "x": 2, "y": 0 },
2092                            { "x": 2, "y": 1 },
2093                        ],
2094                        [],
2095                        [
2096                            null,
2097                            { "x": 4, "y": 1 },
2098                            { "x": 4 },
2099                            null,
2100                            { "x": 4, "y": 4 },
2101                        ]
2102                    ],
2103                })),
2104                vec![
2105                    ApplyToError::from_json(&json!({
2106                        "message": "Property .x not found in null",
2107                        "path": ["arrayOfArrays", 4, 0, "x"],
2108                        "range": [23, 24],
2109                        "spec": spec.to_string(),
2110                    })),
2111                    ApplyToError::from_json(&json!({
2112                        "message": "Property .y not found in null",
2113                        "path": ["arrayOfArrays", 4, 0, "y"],
2114                        "range": [25, 26],
2115                        "spec": spec.to_string(),
2116                    })),
2117                    ApplyToError::from_json(&json!({
2118                        "message": "Property .y not found in object",
2119                        "path": ["arrayOfArrays", 4, 2, "y"],
2120                        "range": [25, 26],
2121                        "spec": spec.to_string(),
2122                    })),
2123                    ApplyToError::from_json(&json!({
2124                        "message": "Property .x not found in null",
2125                        "path": ["arrayOfArrays", 4, 3, "x"],
2126                        "range": [23, 24],
2127                        "spec": spec.to_string(),
2128                    })),
2129                    ApplyToError::from_json(&json!({
2130                        "message": "Property .y not found in null",
2131                        "path": ["arrayOfArrays", 4, 3, "y"],
2132                        "range": [25, 26],
2133                        "spec": spec.to_string(),
2134                    })),
2135                ],
2136            ),
2137        );
2138
2139        fn make_array_of_arrays_x_y_expected(
2140            x_range: (usize, usize),
2141            y_range: (usize, usize),
2142            spec: ConnectSpec,
2143        ) -> (Option<JSON>, Vec<ApplyToError>) {
2144            (
2145                Some(json!({
2146                    "ys": [
2147                        [0],
2148                        [0, 1, 2],
2149                        [0, 1],
2150                        [],
2151                        [null, 1, null, null, 4],
2152                    ],
2153                    "xs": [
2154                        [0],
2155                        [1, 1, 1],
2156                        [2, 2],
2157                        [],
2158                        [null, 4, 4, null, 4],
2159                    ],
2160                })),
2161                vec![
2162                    ApplyToError::from_json(&json!({
2163                        "message": "Property .y not found in null",
2164                        "path": ["arrayOfArrays", 4, 0, "y"],
2165                        "range": y_range,
2166                        "spec": spec.to_string(),
2167                    })),
2168                    ApplyToError::from_json(&json!({
2169                        "message": "Property .y not found in object",
2170                        "path": ["arrayOfArrays", 4, 2, "y"],
2171                        "range": y_range,
2172                        "spec": spec.to_string(),
2173                    })),
2174                    ApplyToError::from_json(&json!({
2175                        // Reversing the order of "path" and "message" here to make
2176                        // sure that doesn't affect the deduplication logic.
2177                        "path": ["arrayOfArrays", 4, 3, "y"],
2178                        "message": "Property .y not found in null",
2179                        "range": y_range,
2180                        "spec": spec.to_string(),
2181                    })),
2182                    ApplyToError::from_json(&json!({
2183                        "message": "Property .x not found in null",
2184                        "path": ["arrayOfArrays", 4, 0, "x"],
2185                        "range": x_range,
2186                        "spec": spec.to_string(),
2187                    })),
2188                    ApplyToError::from_json(&json!({
2189                        "message": "Property .x not found in null",
2190                        "path": ["arrayOfArrays", 4, 3, "x"],
2191                        "range": x_range,
2192                        "spec": spec.to_string(),
2193                    })),
2194                ],
2195            )
2196        }
2197        assert_eq!(
2198            selection!("ys: arrayOfArrays.y xs: arrayOfArrays.x", spec).apply_to(&data),
2199            make_array_of_arrays_x_y_expected((38, 39), (18, 19), spec),
2200        );
2201        assert_eq!(
2202            selection!("ys: $.arrayOfArrays.y xs: $.arrayOfArrays.x", spec).apply_to(&data),
2203            make_array_of_arrays_x_y_expected((42, 43), (20, 21), spec),
2204        );
2205    }
2206
2207    #[rstest]
2208    #[case::v0_2(ConnectSpec::V0_2)]
2209    #[case::v0_3(ConnectSpec::V0_3)]
2210    #[case::v0_4(ConnectSpec::V0_4)]
2211    fn test_apply_to_variable_expressions(#[case] spec: ConnectSpec) {
2212        let id_object = selection!("id: $", spec).apply_to(&json!(123));
2213        assert_eq!(id_object, (Some(json!({"id": 123})), vec![]));
2214
2215        let data = json!({
2216            "id": 123,
2217            "name": "Ben",
2218            "friend_ids": [234, 345, 456]
2219        });
2220
2221        assert_eq!(
2222            selection!("id name friends: friend_ids { id: $ }", spec).apply_to(&data),
2223            (
2224                Some(json!({
2225                    "id": 123,
2226                    "name": "Ben",
2227                    "friends": [
2228                        { "id": 234 },
2229                        { "id": 345 },
2230                        { "id": 456 },
2231                    ],
2232                })),
2233                vec![],
2234            ),
2235        );
2236
2237        let mut vars = IndexMap::default();
2238        vars.insert("$args".to_string(), json!({ "id": "id from args" }));
2239        assert_eq!(
2240            selection!("id: $args.id name", spec).apply_with_vars(&data, &vars),
2241            (
2242                Some(json!({
2243                    "id": "id from args",
2244                    "name": "Ben"
2245                })),
2246                vec![],
2247            ),
2248        );
2249        assert_eq!(
2250            selection!("nested.path { id: $args.id name }", spec).apply_to(&json!({
2251                "nested": {
2252                    "path": data,
2253                },
2254            })),
2255            (
2256                Some(json!({
2257                    "name": "Ben"
2258                })),
2259                vec![ApplyToError::from_json(&json!({
2260                    "message": "Variable $args not found",
2261                    "path": ["nested", "path"],
2262                    "range": [18, 23],
2263                    "spec": spec.to_string(),
2264                }))],
2265            ),
2266        );
2267        let mut vars_without_args_id = IndexMap::default();
2268        vars_without_args_id.insert("$args".to_string(), json!({ "unused": "ignored" }));
2269        assert_eq!(
2270            selection!("id: $args.id name", spec).apply_with_vars(&data, &vars_without_args_id),
2271            (
2272                Some(json!({
2273                    "name": "Ben"
2274                })),
2275                vec![ApplyToError::from_json(&json!({
2276                    "message": "Property .id not found in object",
2277                    "path": ["$args", "id"],
2278                    "range": [10, 12],
2279                    "spec": spec.to_string(),
2280                }))],
2281            ),
2282        );
2283
2284        // A single variable path should not be mapped over an input array.
2285        assert_eq!(
2286            selection!("$args.id", spec).apply_with_vars(&json!([1, 2, 3]), &vars),
2287            (Some(json!("id from args")), vec![]),
2288        );
2289    }
2290
2291    #[test]
2292    fn test_apply_to_variable_expressions_typename() {
2293        let typename_object =
2294            selection!("__typename: $->echo('Product') reviews { __typename: $->echo('Review') }")
2295                .apply_to(&json!({"reviews": [{}]}));
2296        assert_eq!(
2297            typename_object,
2298            (
2299                Some(json!({"__typename": "Product", "reviews": [{ "__typename": "Review" }] })),
2300                vec![]
2301            )
2302        );
2303    }
2304
2305    #[test]
2306    fn test_literal_expressions_in_parentheses() {
2307        assert_eq!(
2308            selection!("__typename: $('Product')").apply_to(&json!({})),
2309            (Some(json!({"__typename": "Product"})), vec![]),
2310        );
2311
2312        assert_eq!(
2313            selection!(" __typename : 'Product' ").apply_to(&json!({})),
2314            (
2315                Some(json!({})),
2316                vec![ApplyToError::new(
2317                    "Property .\"Product\" not found in object".to_string(),
2318                    vec![json!("Product")],
2319                    Some(14..23),
2320                    ConnectSpec::latest(),
2321                )],
2322            ),
2323        );
2324
2325        assert_eq!(
2326            selection!(
2327                r#"
2328                one: $(1)
2329                two: $(2)
2330                negativeThree: $(-  3)
2331                true: $(true  )
2332                false: $(  false)
2333                null: $(null)
2334                string: $("string")
2335                array: $( [ 1 , 2 , 3 ] )
2336                object: $( { "key" : "value" } )
2337                path: $(nested.path)
2338            "#
2339            )
2340            .apply_to(&json!({
2341                "nested": {
2342                    "path": "nested path value"
2343                }
2344            })),
2345            (
2346                Some(json!({
2347                    "one": 1,
2348                    "two": 2,
2349                    "negativeThree": -3,
2350                    "true": true,
2351                    "false": false,
2352                    "null": null,
2353                    "string": "string",
2354                    "array": [1, 2, 3],
2355                    "object": { "key": "value" },
2356                    "path": "nested path value",
2357                })),
2358                vec![],
2359            ),
2360        );
2361
2362        assert_eq!(
2363            selection!(
2364                r#"
2365                one: $(1)->typeof
2366                two: $(2)->typeof
2367                negativeThree: $(-3)->typeof
2368                true: $(true)->typeof
2369                false: $(false)->typeof
2370                null: $(null)->typeof
2371                string: $("string")->typeof
2372                array: $([1, 2, 3])->typeof
2373                object: $({ "key": "value" })->typeof
2374                path: $(nested.path)->typeof
2375            "#
2376            )
2377            .apply_to(&json!({
2378                "nested": {
2379                    "path": 12345
2380                }
2381            })),
2382            (
2383                Some(json!({
2384                    "one": "number",
2385                    "two": "number",
2386                    "negativeThree": "number",
2387                    "true": "boolean",
2388                    "false": "boolean",
2389                    "null": "null",
2390                    "string": "string",
2391                    "array": "array",
2392                    "object": "object",
2393                    "path": "number",
2394                })),
2395                vec![],
2396            ),
2397        );
2398
2399        assert_eq!(
2400            selection!(
2401                r#"
2402                items: $([
2403                    1,
2404                    -2.0,
2405                    true,
2406                    false,
2407                    null,
2408                    "string",
2409                    [1, 2, 3],
2410                    { "key": "value" },
2411                    nested.path,
2412                ])->map(@->typeof)
2413            "#
2414            )
2415            .apply_to(&json!({
2416                "nested": {
2417                    "path": { "deeply": "nested" }
2418                }
2419            })),
2420            (
2421                Some(json!({
2422                    "items": [
2423                        "number",
2424                        "number",
2425                        "boolean",
2426                        "boolean",
2427                        "null",
2428                        "string",
2429                        "array",
2430                        "object",
2431                        "object",
2432                    ],
2433                })),
2434                vec![],
2435            ),
2436        );
2437
2438        assert_eq!(
2439            selection!(
2440                r#"
2441                $({
2442                    one: 1,
2443                    two: 2,
2444                    negativeThree: -3,
2445                    true: true,
2446                    false: false,
2447                    null: null,
2448                    string: "string",
2449                    array: [1, 2, 3],
2450                    object: { "key": "value" },
2451                    path: $ . nested . path ,
2452                })->entries
2453            "#
2454            )
2455            .apply_to(&json!({
2456                "nested": {
2457                    "path": "nested path value"
2458                }
2459            })),
2460            (
2461                Some(json!([
2462                    { "key": "one", "value": 1 },
2463                    { "key": "two", "value": 2 },
2464                    { "key": "negativeThree", "value": -3 },
2465                    { "key": "true", "value": true },
2466                    { "key": "false", "value": false },
2467                    { "key": "null", "value": null },
2468                    { "key": "string", "value": "string" },
2469                    { "key": "array", "value": [1, 2, 3] },
2470                    { "key": "object", "value": { "key": "value" } },
2471                    { "key": "path", "value": "nested path value" },
2472                ])),
2473                vec![],
2474            ),
2475        );
2476
2477        assert_eq!(
2478            selection!(
2479                r#"
2480                $({
2481                    string: $("string")->slice(1, 4),
2482                    array: $([1, 2, 3])->map(@->add(10)),
2483                    object: $({ "key": "value" })->get("key"),
2484                    path: nested.path->slice($("nested ")->size),
2485                    needlessParens: $("oyez"),
2486                    withoutParens: "oyez",
2487                })
2488            "#
2489            )
2490            .apply_to(&json!({
2491                "nested": {
2492                    "path": "nested path value"
2493                }
2494            })),
2495            (
2496                Some(json!({
2497                    "string": "tri",
2498                    "array": [11, 12, 13],
2499                    "object": "value",
2500                    "path": "path value",
2501                    "needlessParens": "oyez",
2502                    "withoutParens": "oyez",
2503                })),
2504                vec![],
2505            ),
2506        );
2507
2508        assert_eq!(
2509            selection!(
2510                r#"
2511                string: $("string")->slice(1, 4)
2512                array: $([1, 2, 3])->map(@->add(10))
2513                object: $({ "key": "value" })->get("key")
2514                path: nested.path->slice($("nested ")->size)
2515            "#
2516            )
2517            .apply_to(&json!({
2518                "nested": {
2519                    "path": "nested path value"
2520                }
2521            })),
2522            (
2523                Some(json!({
2524                    "string": "tri",
2525                    "array": [11, 12, 13],
2526                    "object": "value",
2527                    "path": "path value",
2528                })),
2529                vec![],
2530            ),
2531        );
2532    }
2533
2534    #[rstest]
2535    #[case::v0_2(ConnectSpec::V0_2)]
2536    #[case::v0_3(ConnectSpec::V0_3)]
2537    #[case::v0_4(ConnectSpec::V0_4)]
2538    fn test_inline_paths_with_subselections(#[case] spec: ConnectSpec) {
2539        let data = json!({
2540            "id": 123,
2541            "created": "2021-01-01T00:00:00Z",
2542            "model": "gpt-4o",
2543            "choices": [{
2544                "index": 0,
2545                "message": {
2546                    "role": "assistant",
2547                    "content": "The capital of Australia is Canberra.",
2548                },
2549            }, {
2550                "index": 1,
2551                "message": {
2552                    "role": "assistant",
2553                    "content": "The capital of Australia is Sydney.",
2554                },
2555            }],
2556        });
2557
2558        {
2559            let expected = (
2560                Some(json!({
2561                    "id": 123,
2562                    "created": "2021-01-01T00:00:00Z",
2563                    "model": "gpt-4o",
2564                    "role": "assistant",
2565                    "content": "The capital of Australia is Canberra.",
2566                })),
2567                vec![],
2568            );
2569
2570            assert_eq!(
2571                selection!(
2572                    r#"
2573                    id
2574                    created
2575                    model
2576                    role: choices->first.message.role
2577                    content: choices->first.message.content
2578                "#,
2579                    spec
2580                )
2581                .apply_to(&data),
2582                expected,
2583            );
2584
2585            assert_eq!(
2586                selection!(
2587                    r#"
2588                    id
2589                    created
2590                    model
2591                    choices->first.message {
2592                        role
2593                        content
2594                    }
2595                "#,
2596                    spec
2597                )
2598                .apply_to(&data),
2599                expected,
2600            );
2601
2602            assert_eq!(
2603                selection!(
2604                    r#"
2605                    id
2606                    choices->first.message {
2607                        role
2608                        content
2609                    }
2610                    created
2611                    model
2612                "#,
2613                    spec
2614                )
2615                .apply_to(&data),
2616                expected,
2617            );
2618        }
2619
2620        {
2621            let expected = (
2622                Some(json!({
2623                    "id": 123,
2624                    "created": "2021-01-01T00:00:00Z",
2625                    "model": "gpt-4o",
2626                    "role": "assistant",
2627                    "message": "The capital of Australia is Sydney.",
2628                })),
2629                vec![],
2630            );
2631
2632            assert_eq!(
2633                selection!(
2634                    r#"
2635                    id
2636                    created
2637                    model
2638                    role: choices->last.message.role
2639                    message: choices->last.message.content
2640                "#,
2641                    spec
2642                )
2643                .apply_to(&data),
2644                expected,
2645            );
2646
2647            assert_eq!(
2648                selection!(
2649                    r#"
2650                    id
2651                    created
2652                    model
2653                    choices->last.message {
2654                        role
2655                        message: content
2656                    }
2657                "#,
2658                    spec
2659                )
2660                .apply_to(&data),
2661                expected,
2662            );
2663
2664            assert_eq!(
2665                selection!(
2666                    r#"
2667                    created
2668                    choices->last.message {
2669                        message: content
2670                        role
2671                    }
2672                    model
2673                    id
2674                "#,
2675                    spec
2676                )
2677                .apply_to(&data),
2678                expected,
2679            );
2680        }
2681
2682        {
2683            let expected = (
2684                Some(json!({
2685                    "id": 123,
2686                    "created": "2021-01-01T00:00:00Z",
2687                    "model": "gpt-4o",
2688                    "role": "assistant",
2689                    "correct": "The capital of Australia is Canberra.",
2690                    "incorrect": "The capital of Australia is Sydney.",
2691                })),
2692                vec![],
2693            );
2694
2695            assert_eq!(
2696                selection!(
2697                    r#"
2698                    id
2699                    created
2700                    model
2701                    role: choices->first.message.role
2702                    correct: choices->first.message.content
2703                    incorrect: choices->last.message.content
2704                "#,
2705                    spec
2706                )
2707                .apply_to(&data),
2708                expected,
2709            );
2710
2711            assert_eq!(
2712                selection!(
2713                    r#"
2714                    id
2715                    created
2716                    model
2717                    choices->first.message {
2718                        role
2719                        correct: content
2720                    }
2721                    choices->last.message {
2722                        incorrect: content
2723                    }
2724                "#,
2725                    spec
2726                )
2727                .apply_to(&data),
2728                expected,
2729            );
2730
2731            assert_eq!(
2732                selection!(
2733                    r#"
2734                    id
2735                    created
2736                    model
2737                    choices->first.message {
2738                        role
2739                        correct: content
2740                    }
2741                    incorrect: choices->last.message.content
2742                "#,
2743                    spec
2744                )
2745                .apply_to(&data),
2746                expected,
2747            );
2748
2749            assert_eq!(
2750                selection!(
2751                    r#"
2752                    id
2753                    created
2754                    model
2755                    choices->first.message {
2756                        correct: content
2757                    }
2758                    choices->last.message {
2759                        role
2760                        incorrect: content
2761                    }
2762                "#,
2763                    spec
2764                )
2765                .apply_to(&data),
2766                expected,
2767            );
2768
2769            assert_eq!(
2770                selection!(
2771                    r#"
2772                    id
2773                    created
2774                    correct: choices->first.message.content
2775                    choices->last.message {
2776                        role
2777                        incorrect: content
2778                    }
2779                    model
2780                "#,
2781                    spec
2782                )
2783                .apply_to(&data),
2784                expected,
2785            );
2786        }
2787
2788        {
2789            let data = json!({
2790                "from": "data",
2791            });
2792
2793            let vars = {
2794                let mut vars = IndexMap::default();
2795                vars.insert(
2796                    "$this".to_string(),
2797                    json!({
2798                        "id": 1234,
2799                    }),
2800                );
2801                vars.insert(
2802                    "$args".to_string(),
2803                    json!({
2804                        "input": {
2805                            "title": "The capital of Australia",
2806                            "body": "Canberra",
2807                        },
2808                        "extra": "extra",
2809                    }),
2810                );
2811                vars
2812            };
2813
2814            let expected = (
2815                Some(json!({
2816                    "id": 1234,
2817                    "title": "The capital of Australia",
2818                    "body": "Canberra",
2819                    "from": "data",
2820                })),
2821                vec![],
2822            );
2823
2824            assert_eq!(
2825                selection!(
2826                    r#"
2827                    id: $this.id
2828                    $args.input {
2829                        title
2830                        body
2831                    }
2832                    from
2833                "#,
2834                    spec
2835                )
2836                .apply_with_vars(&data, &vars),
2837                expected,
2838            );
2839
2840            assert_eq!(
2841                selection!(
2842                    r#"
2843                    from
2844                    $args.input { title body }
2845                    id: $this.id
2846                "#,
2847                    spec
2848                )
2849                .apply_with_vars(&data, &vars),
2850                expected,
2851            );
2852
2853            assert_eq!(
2854                selection!(
2855                    r#"
2856                    $args.input { body title }
2857                    from
2858                    id: $this.id
2859                "#,
2860                    spec
2861                )
2862                .apply_with_vars(&data, &vars),
2863                expected,
2864            );
2865
2866            assert_eq!(
2867                selection!(
2868                    r#"
2869                    id: $this.id
2870                    $args { $.input { title body } }
2871                    from
2872                "#,
2873                    spec
2874                )
2875                .apply_with_vars(&data, &vars),
2876                expected,
2877            );
2878
2879            assert_eq!(
2880                selection!(
2881                    r#"
2882                    id: $this.id
2883                    $args { $.input { title body } extra }
2884                    from: $.from
2885                "#,
2886                    spec
2887                )
2888                .apply_with_vars(&data, &vars),
2889                (
2890                    Some(json!({
2891                        "id": 1234,
2892                        "title": "The capital of Australia",
2893                        "body": "Canberra",
2894                        "extra": "extra",
2895                        "from": "data",
2896                    })),
2897                    vec![],
2898                ),
2899            );
2900
2901            assert_eq!(
2902                selection!(
2903                    r#"
2904                    # Equivalent to id: $this.id
2905                    $this { id }
2906
2907                    $args {
2908                        __typename: $("Args")
2909
2910                        # Requiring $. instead of just . prevents .input from
2911                        # parsing as a key applied to the $("Args") string.
2912                        $.input { title body }
2913
2914                        extra
2915                    }
2916
2917                    from: $.from
2918                "#,
2919                    spec
2920                )
2921                .apply_with_vars(&data, &vars),
2922                (
2923                    Some(json!({
2924                        "id": 1234,
2925                        "title": "The capital of Australia",
2926                        "body": "Canberra",
2927                        "__typename": "Args",
2928                        "extra": "extra",
2929                        "from": "data",
2930                    })),
2931                    vec![],
2932                ),
2933            );
2934        }
2935    }
2936
2937    #[test]
2938    fn test_inline_path_errors() {
2939        {
2940            let data = json!({
2941                "id": 123,
2942                "created": "2021-01-01T00:00:00Z",
2943                "model": "gpt-4o",
2944                "choices": [{
2945                    "message": "The capital of Australia is Canberra.",
2946                }, {
2947                    "message": "The capital of Australia is Sydney.",
2948                }],
2949            });
2950
2951            let expected = (
2952                Some(json!({
2953                    "id": 123,
2954                    "created": "2021-01-01T00:00:00Z",
2955                    "model": "gpt-4o",
2956                })),
2957                vec![
2958                    ApplyToError::new(
2959                        "Property .role not found in string".to_string(),
2960                        vec![
2961                            json!("choices"),
2962                            json!("->first"),
2963                            json!("message"),
2964                            json!("role"),
2965                        ],
2966                        Some(123..127),
2967                        ConnectSpec::latest(),
2968                    ),
2969                    ApplyToError::new(
2970                        "Property .content not found in string".to_string(),
2971                        vec![
2972                            json!("choices"),
2973                            json!("->first"),
2974                            json!("message"),
2975                            json!("content"),
2976                        ],
2977                        Some(128..135),
2978                        ConnectSpec::latest(),
2979                    ),
2980                    ApplyToError::new(
2981                        "Expected object or null, not string".to_string(),
2982                        vec![],
2983                        // This is the range of the whole
2984                        // `choices->first.message { role content }`
2985                        // subselection.
2986                        Some(98..137),
2987                        ConnectSpec::latest(),
2988                    ),
2989                ],
2990            );
2991
2992            assert_eq!(
2993                selection!(
2994                    r#"
2995                    id
2996                    created
2997                    model
2998                    choices->first.message { role content }
2999                "#
3000                )
3001                .apply_to(&data),
3002                expected,
3003            );
3004        }
3005
3006        assert_eq!(
3007            selection!("id nested.path.nonexistent { name }").apply_to(&json!({
3008                "id": 2345,
3009                "nested": {
3010                    "path": "nested path value",
3011                },
3012            })),
3013            (
3014                Some(json!({
3015                    "id": 2345,
3016                })),
3017                vec![
3018                    ApplyToError::new(
3019                        "Property .nonexistent not found in string".to_string(),
3020                        vec![json!("nested"), json!("path"), json!("nonexistent")],
3021                        Some(15..26),
3022                        ConnectSpec::latest(),
3023                    ),
3024                    ApplyToError::new(
3025                        "Inlined path produced no value".to_string(),
3026                        vec![],
3027                        // This is the range of the whole
3028                        // `nested.path.nonexistent { name }` path selection.
3029                        Some(3..35),
3030                        ConnectSpec::latest(),
3031                    ),
3032                ],
3033            ),
3034        );
3035
3036        let valid_inline_path_selection = JSONSelection::named(SubSelection {
3037            selections: vec![NamedSelection {
3038                prefix: NamingPrefix::None,
3039                path: WithRange::new(
3040                    LitExpr::Path(PathSelection {
3041                        path: PathList::Key(
3042                            Key::field("some").into_with_range(),
3043                            PathList::Key(
3044                                Key::field("object").into_with_range(),
3045                                PathList::Empty.into_with_range(),
3046                            )
3047                            .into_with_range(),
3048                        )
3049                        .into_with_range(),
3050                    }),
3051                    None,
3052                ),
3053            }],
3054            ..Default::default()
3055        });
3056
3057        assert_eq!(
3058            valid_inline_path_selection.apply_to(&json!({
3059                "some": {
3060                    "object": {
3061                        "key": "value",
3062                    },
3063                },
3064            })),
3065            (
3066                Some(json!({
3067                    "key": "value",
3068                })),
3069                vec![],
3070            ),
3071        );
3072    }
3073
3074    #[test]
3075    fn test_apply_to_non_identifier_properties() {
3076        let data = json!({
3077            "not an identifier": [
3078                { "also.not.an.identifier": 0 },
3079                { "also.not.an.identifier": 1 },
3080                { "also.not.an.identifier": 2 },
3081            ],
3082            "another": {
3083                "pesky string literal!": {
3084                    "identifier": 123,
3085                    "{ evil braces }": true,
3086                },
3087            },
3088        });
3089
3090        assert_eq!(
3091            // The grammar enforces that we must always provide identifier aliases
3092            // for non-identifier properties, so the data we get back will always be
3093            // GraphQL-safe.
3094            selection!("alias: 'not an identifier' { safe: 'also.not.an.identifier' }")
3095                .apply_to(&data),
3096            (
3097                Some(json!({
3098                    "alias": [
3099                        { "safe": 0 },
3100                        { "safe": 1 },
3101                        { "safe": 2 },
3102                    ],
3103                })),
3104                vec![],
3105            ),
3106        );
3107
3108        assert_eq!(
3109            selection!("'not an identifier'.'also.not.an.identifier'").apply_to(&data),
3110            (Some(json!([0, 1, 2])), vec![],),
3111        );
3112
3113        assert_eq!(
3114            selection!("$.'not an identifier'.'also.not.an.identifier'").apply_to(&data),
3115            (Some(json!([0, 1, 2])), vec![],),
3116        );
3117
3118        assert_eq!(
3119            selection!("$.\"not an identifier\" { safe: \"also.not.an.identifier\" }")
3120                .apply_to(&data),
3121            (
3122                Some(json!([
3123                    { "safe": 0 },
3124                    { "safe": 1 },
3125                    { "safe": 2 },
3126                ])),
3127                vec![],
3128            ),
3129        );
3130
3131        assert_eq!(
3132            selection!(
3133                "another {
3134                pesky: 'pesky string literal!' {
3135                    identifier
3136                    evil: '{ evil braces }'
3137                }
3138            }"
3139            )
3140            .apply_to(&data),
3141            (
3142                Some(json!({
3143                    "another": {
3144                        "pesky": {
3145                            "identifier": 123,
3146                            "evil": true,
3147                        },
3148                    },
3149                })),
3150                vec![],
3151            ),
3152        );
3153
3154        assert_eq!(
3155            selection!("another.'pesky string literal!'.'{ evil braces }'").apply_to(&data),
3156            (Some(json!(true)), vec![],),
3157        );
3158
3159        assert_eq!(
3160            selection!("another.'pesky string literal!'.\"identifier\"").apply_to(&data),
3161            (Some(json!(123)), vec![],),
3162        );
3163
3164        assert_eq!(
3165            selection!("$.another.'pesky string literal!'.\"identifier\"").apply_to(&data),
3166            (Some(json!(123)), vec![],),
3167        );
3168    }
3169
3170    #[rstest]
3171    #[case::latest(ConnectSpec::V0_2)]
3172    #[case::next(ConnectSpec::V0_3)]
3173    #[case::v0_4(ConnectSpec::V0_4)]
3174    fn test_left_associative_path_evaluation(#[case] spec: ConnectSpec) {
3175        assert_eq!(
3176            selection!("batch.id->first", spec).apply_to(&json!({
3177                "batch": [
3178                    { "id": 1 },
3179                    { "id": 2 },
3180                    { "id": 3 },
3181                ],
3182            })),
3183            (Some(json!(1)), vec![]),
3184        );
3185
3186        assert_eq!(
3187            selection!("batch.id->last", spec).apply_to(&json!({
3188                "batch": [
3189                    { "id": 1 },
3190                    { "id": 2 },
3191                    { "id": 3 },
3192                ],
3193            })),
3194            (Some(json!(3)), vec![]),
3195        );
3196
3197        assert_eq!(
3198            selection!("batch.id->size", spec).apply_to(&json!({
3199                "batch": [
3200                    { "id": 1 },
3201                    { "id": 2 },
3202                    { "id": 3 },
3203                ],
3204            })),
3205            (Some(json!(3)), vec![]),
3206        );
3207
3208        assert_eq!(
3209            selection!("batch.id->slice(1)->first", spec).apply_to(&json!({
3210                "batch": [
3211                    { "id": 1 },
3212                    { "id": 2 },
3213                    { "id": 3 },
3214                ],
3215            })),
3216            (Some(json!(2)), vec![]),
3217        );
3218
3219        assert_eq!(
3220            selection!("batch.id->map({ batchId: @ })", spec).apply_to(&json!({
3221                "batch": [
3222                    { "id": 1 },
3223                    { "id": 2 },
3224                    { "id": 3 },
3225                ],
3226            })),
3227            (
3228                Some(json!([
3229                    { "batchId": 1 },
3230                    { "batchId": 2 },
3231                    { "batchId": 3 },
3232                ])),
3233                vec![],
3234            ),
3235        );
3236
3237        let mut vars = IndexMap::default();
3238        vars.insert(
3239            "$batch".to_string(),
3240            json!([
3241                { "id": 4 },
3242                { "id": 5 },
3243                { "id": 6 },
3244            ]),
3245        );
3246        assert_eq!(
3247            selection!("$batch.id->map({ batchId: @ })", spec).apply_with_vars(
3248                &json!({
3249                    "batch": "ignored",
3250                }),
3251                &vars
3252            ),
3253            (
3254                Some(json!([
3255                    { "batchId": 4 },
3256                    { "batchId": 5 },
3257                    { "batchId": 6 },
3258                ])),
3259                vec![],
3260            ),
3261        );
3262
3263        assert_eq!(
3264            selection!("batch.id->map({ batchId: @ })->first", spec).apply_to(&json!({
3265                "batch": [
3266                    { "id": 7 },
3267                    { "id": 8 },
3268                    { "id": 9 },
3269                ],
3270            })),
3271            (Some(json!({ "batchId": 7 })), vec![]),
3272        );
3273
3274        assert_eq!(
3275            selection!("batch.id->map({ batchId: @ })->last", spec).apply_to(&json!({
3276                "batch": [
3277                    { "id": 7 },
3278                    { "id": 8 },
3279                    { "id": 9 },
3280                ],
3281            })),
3282            (Some(json!({ "batchId": 9 })), vec![]),
3283        );
3284
3285        assert_eq!(
3286            selection!("$batch.id->map({ batchId: @ })->first", spec).apply_with_vars(
3287                &json!({
3288                    "batch": "ignored",
3289                }),
3290                &vars
3291            ),
3292            (Some(json!({ "batchId": 4 })), vec![]),
3293        );
3294
3295        assert_eq!(
3296            selection!("$batch.id->map({ batchId: @ })->last", spec).apply_with_vars(
3297                &json!({
3298                    "batch": "ignored",
3299                }),
3300                &vars
3301            ),
3302            (Some(json!({ "batchId": 6 })), vec![]),
3303        );
3304
3305        assert_eq!(
3306            selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3307                "arrays": [
3308                    { "as": { "bs": [10, 20, 30] } },
3309                    { "as": { "bs": [40, 50, 60] } },
3310                    { "as": { "bs": [70, 80, 90] } },
3311                ],
3312            })),
3313            (
3314                Some(json!({
3315                    "echoed": [
3316                        [10, 20, 30],
3317                        [40, 50, 60],
3318                        [70, 80, 90],
3319                    ],
3320                })),
3321                vec![],
3322            ),
3323        );
3324
3325        assert_eq!(
3326            selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3327                "arrays": [
3328                    { "as": { "bs": [10, 20, 30] } },
3329                    { "as": [
3330                        { "bs": [40, 50, 60] },
3331                        { "bs": [70, 80, 90] },
3332                    ] },
3333                    { "as": { "bs": [100, 110, 120] } },
3334                ],
3335            })),
3336            (
3337                Some(json!({
3338                    "echoed": [
3339                        [10, 20, 30],
3340                        [
3341                            [40, 50, 60],
3342                            [70, 80, 90],
3343                        ],
3344                        [100, 110, 120],
3345                    ],
3346                })),
3347                vec![],
3348            ),
3349        );
3350
3351        assert_eq!(
3352            selection!("batch.id->jsonStringify", spec).apply_to(&json!({
3353                "batch": [
3354                    { "id": 1 },
3355                    { "id": 2 },
3356                    { "id": 3 },
3357                ],
3358            })),
3359            (Some(json!("[1,2,3]")), vec![]),
3360        );
3361
3362        assert_eq!(
3363            selection!("batch.id->map([@])->echo([@])->jsonStringify", spec).apply_to(&json!({
3364                "batch": [
3365                    { "id": 1 },
3366                    { "id": 2 },
3367                    { "id": 3 },
3368                ],
3369            })),
3370            (Some(json!("[[[1],[2],[3]]]")), vec![]),
3371        );
3372
3373        assert_eq!(
3374            selection!("batch.id->map([@])->echo([@])->jsonStringify->typeof", spec).apply_to(
3375                &json!({
3376                    "batch": [
3377                        { "id": 1 },
3378                        { "id": 2 },
3379                        { "id": 3 },
3380                    ],
3381                })
3382            ),
3383            (Some(json!("string")), vec![]),
3384        );
3385    }
3386
3387    #[test]
3388    fn test_left_associative_output_shapes_v0_2() {
3389        let spec = ConnectSpec::V0_2;
3390
3391        assert_eq!(
3392            selection!("$batch.id", spec).shape().pretty_print(),
3393            "$batch.id"
3394        );
3395
3396        assert_eq!(
3397            selection!("$batch.id->first", spec).shape().pretty_print(),
3398            "Unknown",
3399        );
3400
3401        assert_eq!(
3402            selection!("$batch.id->last", spec).shape().pretty_print(),
3403            "Unknown",
3404        );
3405
3406        let mut named_shapes = IndexMap::default();
3407        named_shapes.insert(
3408            "$batch".to_string(),
3409            Shape::list(
3410                Shape::record(
3411                    {
3412                        let mut map = Shape::empty_map();
3413                        map.insert("id".to_string(), Shape::int([]));
3414                        map
3415                    },
3416                    [],
3417                ),
3418                [],
3419            ),
3420        );
3421
3422        let root_shape = Shape::name("$root", []);
3423        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3424            .with_spec(spec)
3425            .with_named_shapes(named_shapes);
3426
3427        let computed_batch_id =
3428            selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3429        assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3430
3431        let computed_first = selection!("$batch.id->first", spec)
3432            .compute_output_shape(&shape_context, root_shape.clone());
3433        assert_eq!(computed_first.pretty_print(), "Unknown");
3434
3435        let computed_last = selection!("$batch.id->last", spec)
3436            .compute_output_shape(&shape_context, root_shape.clone());
3437        assert_eq!(computed_last.pretty_print(), "Unknown");
3438
3439        assert_eq!(
3440            selection!("$batch.id->jsonStringify", spec)
3441                .shape()
3442                .pretty_print(),
3443            "Unknown",
3444        );
3445
3446        assert_eq!(
3447            selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3448                .shape()
3449                .pretty_print(),
3450            "Unknown",
3451        );
3452
3453        assert_eq!(
3454            selection!("$batch.id->map(@)->echo(@)", spec)
3455                .shape()
3456                .pretty_print(),
3457            "Unknown",
3458        );
3459
3460        assert_eq!(
3461            selection!("$batch.id->map(@)->echo([@])", spec)
3462                .shape()
3463                .pretty_print(),
3464            "Unknown",
3465        );
3466
3467        assert_eq!(
3468            selection!("$batch.id->map([@])->echo(@)", spec)
3469                .shape()
3470                .pretty_print(),
3471            "Unknown",
3472        );
3473
3474        assert_eq!(
3475            selection!("$batch.id->map([@])->echo([@])", spec)
3476                .shape()
3477                .pretty_print(),
3478            "Unknown",
3479        );
3480
3481        assert_eq!(
3482            selection!("$batch.id->map([@])->echo([@])", spec)
3483                .compute_output_shape(&shape_context, root_shape,)
3484                .pretty_print(),
3485            "Unknown",
3486        );
3487    }
3488
3489    #[test]
3490    fn test_left_associative_output_shapes_v0_3() {
3491        let spec = ConnectSpec::V0_3;
3492
3493        assert_eq!(
3494            selection!("$batch.id", spec).shape().pretty_print(),
3495            "$batch.id"
3496        );
3497
3498        assert_eq!(
3499            selection!("$batch.id->first", spec).shape().pretty_print(),
3500            "$batch.id.0",
3501        );
3502
3503        assert_eq!(
3504            selection!("$batch.id->last", spec).shape().pretty_print(),
3505            "$batch.id.*",
3506        );
3507
3508        let mut named_shapes = IndexMap::default();
3509        named_shapes.insert(
3510            "$batch".to_string(),
3511            Shape::list(
3512                Shape::record(
3513                    {
3514                        let mut map = Shape::empty_map();
3515                        map.insert("id".to_string(), Shape::int([]));
3516                        map
3517                    },
3518                    [],
3519                ),
3520                [],
3521            ),
3522        );
3523
3524        let root_shape = Shape::name("$root", []);
3525        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3526            .with_spec(spec)
3527            .with_named_shapes(named_shapes.clone());
3528
3529        let computed_batch_id =
3530            selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3531        assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3532
3533        let computed_first = selection!("$batch.id->first", spec)
3534            .compute_output_shape(&shape_context, root_shape.clone());
3535        assert_eq!(computed_first.pretty_print(), "One<Int, None>");
3536
3537        let computed_last = selection!("$batch.id->last", spec)
3538            .compute_output_shape(&shape_context, root_shape.clone());
3539        assert_eq!(computed_last.pretty_print(), "One<Int, None>");
3540
3541        assert_eq!(
3542            selection!("$batch.id->jsonStringify", spec)
3543                .shape()
3544                .pretty_print(),
3545            "String",
3546        );
3547
3548        assert_eq!(
3549            selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3550                .shape()
3551                .pretty_print(),
3552            "String",
3553        );
3554
3555        assert_eq!(
3556            selection!("$batch.id->map(@)->echo(@)", spec)
3557                .shape()
3558                .pretty_print(),
3559            "List<$batch.id.*>",
3560        );
3561
3562        assert_eq!(
3563            selection!("$batch.id->map(@)->echo([@])", spec)
3564                .shape()
3565                .pretty_print(),
3566            "[List<$batch.id.*>]",
3567        );
3568
3569        assert_eq!(
3570            selection!("$batch.id->map([@])->echo(@)", spec)
3571                .shape()
3572                .pretty_print(),
3573            "List<[$batch.id.*]>",
3574        );
3575
3576        assert_eq!(
3577            selection!("$batch.id->map([@])->echo([@])", spec)
3578                .shape()
3579                .pretty_print(),
3580            "[List<[$batch.id.*]>]",
3581        );
3582
3583        assert_eq!(
3584            selection!("$batch.id->map([@])->echo([@])", spec)
3585                .compute_output_shape(&shape_context, root_shape,)
3586                .pretty_print(),
3587            "[List<[Int]>]",
3588        );
3589    }
3590
3591    // Shared fixture for the lit_paths_* family of tests, which each exercise
3592    // exactly one literal-headed path shape in isolation so a regression
3593    // in any one case cannot mask or be masked by another.
3594    fn lit_paths_data() -> JSON {
3595        json!({
3596            "value": {
3597                "key": 123,
3598            },
3599        })
3600    }
3601
3602    #[test]
3603    fn test_lit_paths_string_head_first() {
3604        assert_eq!(
3605            selection!("$(\"a\")->first").apply_to(&lit_paths_data()),
3606            (Some(json!("a")), vec![]),
3607        );
3608    }
3609
3610    #[test]
3611    fn test_lit_paths_string_head_last_inner() {
3612        assert_eq!(
3613            selection!("$('asdf'->last)").apply_to(&lit_paths_data()),
3614            (Some(json!("f")), vec![]),
3615        );
3616    }
3617
3618    #[test]
3619    fn test_lit_paths_number_head_add_outer() {
3620        assert_eq!(
3621            selection!("$(1234)->add(1111)").apply_to(&lit_paths_data()),
3622            (Some(json!(2345)), vec![]),
3623        );
3624    }
3625
3626    #[test]
3627    fn test_lit_paths_number_head_add_inner() {
3628        assert_eq!(
3629            selection!("$(1234->add(1111))").apply_to(&lit_paths_data()),
3630            (Some(json!(2345)), vec![]),
3631        );
3632    }
3633
3634    #[test]
3635    fn test_lit_paths_path_head_mul_inner() {
3636        assert_eq!(
3637            selection!("$(value.key->mul(10))").apply_to(&lit_paths_data()),
3638            (Some(json!(1230)), vec![]),
3639        );
3640    }
3641
3642    #[test]
3643    fn test_lit_paths_path_head_mul_outer() {
3644        assert_eq!(
3645            selection!("$(value.key)->mul(10)").apply_to(&lit_paths_data()),
3646            (Some(json!(1230)), vec![]),
3647        );
3648    }
3649
3650    #[test]
3651    fn test_lit_paths_path_head_typeof_inner() {
3652        assert_eq!(
3653            selection!("$(value.key->typeof)").apply_to(&lit_paths_data()),
3654            (Some(json!("number")), vec![]),
3655        );
3656    }
3657
3658    #[test]
3659    fn test_lit_paths_path_head_typeof_outer() {
3660        assert_eq!(
3661            selection!("$(value.key)->typeof").apply_to(&lit_paths_data()),
3662            (Some(json!("number")), vec![]),
3663        );
3664    }
3665
3666    #[test]
3667    fn test_lit_paths_array_head_last_outer() {
3668        assert_eq!(
3669            selection!("$([1, 2, 3])->last").apply_to(&lit_paths_data()),
3670            (Some(json!(3)), vec![]),
3671        );
3672    }
3673
3674    #[test]
3675    fn test_lit_paths_array_head_first_inner() {
3676        assert_eq!(
3677            selection!("$([1, 2, 3]->first)").apply_to(&lit_paths_data()),
3678            (Some(json!(1)), vec![]),
3679        );
3680    }
3681
3682    #[test]
3683    fn test_lit_paths_object_head_key_outer() {
3684        assert_eq!(
3685            selection!("$({ a: 'ay', b: 1 }).a").apply_to(&lit_paths_data()),
3686            (Some(json!("ay")), vec![]),
3687        );
3688    }
3689
3690    #[test]
3691    fn test_lit_paths_object_head_key_inner() {
3692        assert_eq!(
3693            selection!("$({ a: 'ay', b: 2 }.a)").apply_to(&lit_paths_data()),
3694            (Some(json!("ay")), vec![]),
3695        );
3696    }
3697
3698    #[test]
3699    fn test_lit_paths_negative_literal_precedence() {
3700        // The `->` has lower precedence than the unary `-`, so `-1` is a
3701        // completed expression before `->add(10)` is applied. Result is 9
3702        // rather than -11.
3703        assert_eq!(
3704            selection!("$(-1->add(10))").apply_to(&lit_paths_data()),
3705            (Some(json!(9)), vec![]),
3706        );
3707    }
3708
3709    // V0_4 cousins: the same shapes, but with the `$(...)` wrapper dropped
3710    // where the unified grammar now accepts the bare literal head at the
3711    // top level. These pin the round-trip story down per-shape.
3712
3713    #[test]
3714    fn test_lit_paths_v0_4_string_head_first() {
3715        assert_eq!(
3716            selection!(r#""a"->first"#, ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3717            (Some(json!("a")), vec![]),
3718        );
3719    }
3720
3721    #[test]
3722    fn test_lit_paths_v0_4_string_head_last() {
3723        assert_eq!(
3724            selection!("'asdf'->last", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3725            (Some(json!("f")), vec![]),
3726        );
3727    }
3728
3729    #[test]
3730    fn test_lit_paths_v0_4_number_head_add() {
3731        assert_eq!(
3732            selection!("1234->add(1111)", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3733            (Some(json!(2345)), vec![]),
3734        );
3735    }
3736
3737    #[test]
3738    fn test_lit_paths_v0_4_array_head_last() {
3739        assert_eq!(
3740            selection!("[1, 2, 3]->last", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3741            (Some(json!(3)), vec![]),
3742        );
3743    }
3744
3745    #[test]
3746    fn test_lit_paths_v0_4_array_head_first() {
3747        assert_eq!(
3748            selection!("[1, 2, 3]->first", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3749            (Some(json!(1)), vec![]),
3750        );
3751    }
3752
3753    #[test]
3754    fn test_lit_paths_v0_4_object_head_key() {
3755        assert_eq!(
3756            selection!("{ a: 'ay', b: 1 }.a", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3757            (Some(json!("ay")), vec![]),
3758        );
3759    }
3760
3761    #[test]
3762    fn test_lit_paths_v0_4_negative_literal_precedence() {
3763        assert_eq!(
3764            selection!("-1->add(10)", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3765            (Some(json!(9)), vec![]),
3766        );
3767    }
3768
3769    #[test]
3770    fn test_lit_paths_v0_4_path_head_mul() {
3771        assert_eq!(
3772            selection!("value.key->mul(10)", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3773            (Some(json!(1230)), vec![]),
3774        );
3775    }
3776
3777    #[test]
3778    fn test_lit_paths_v0_4_path_head_typeof() {
3779        assert_eq!(
3780            selection!("value.key->typeof", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3781            (Some(json!("number")), vec![]),
3782        );
3783    }
3784
3785    // Tests for dotted access to keys whose names happen to match reserved
3786    // LitExpr shapes (true, false, null) or contain characters that require
3787    // quoting. Each is isolated so a regression in any one case is legible.
3788    fn reserved_key_data() -> JSON {
3789        json!({
3790            "true": "yes",
3791            "false": "no",
3792            "null": "nope",
3793            "some property": "with spaces",
3794            "weird \"quoted\" key": "escapes work",
3795        })
3796    }
3797
3798    #[test]
3799    fn test_reserved_key_true() {
3800        assert_eq!(
3801            selection!("$.true").apply_to(&reserved_key_data()),
3802            (Some(json!("yes")), vec![]),
3803        );
3804    }
3805
3806    #[test]
3807    fn test_reserved_key_false() {
3808        assert_eq!(
3809            selection!("$.false").apply_to(&reserved_key_data()),
3810            (Some(json!("no")), vec![]),
3811        );
3812    }
3813
3814    #[test]
3815    fn test_reserved_key_null() {
3816        assert_eq!(
3817            selection!("$.null").apply_to(&reserved_key_data()),
3818            (Some(json!("nope")), vec![]),
3819        );
3820    }
3821
3822    #[test]
3823    fn test_reserved_key_quoted_with_space() {
3824        assert_eq!(
3825            selection!(r#"$."some property""#).apply_to(&reserved_key_data()),
3826            (Some(json!("with spaces")), vec![]),
3827        );
3828    }
3829
3830    #[test]
3831    fn test_reserved_key_quoted_with_escapes() {
3832        assert_eq!(
3833            selection!(r#"$."weird \"quoted\" key""#).apply_to(&reserved_key_data()),
3834            (Some(json!("escapes work")), vec![]),
3835        );
3836    }
3837
3838    #[test]
3839    fn test_reserved_key_v0_4_true() {
3840        assert_eq!(
3841            selection!("$.true", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3842            (Some(json!("yes")), vec![]),
3843        );
3844    }
3845
3846    #[test]
3847    fn test_reserved_key_v0_4_false() {
3848        assert_eq!(
3849            selection!("$.false", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3850            (Some(json!("no")), vec![]),
3851        );
3852    }
3853
3854    #[test]
3855    fn test_reserved_key_v0_4_null() {
3856        assert_eq!(
3857            selection!("$.null", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3858            (Some(json!("nope")), vec![]),
3859        );
3860    }
3861
3862    #[test]
3863    fn test_reserved_key_v0_4_quoted_with_space() {
3864        assert_eq!(
3865            selection!(r#"$."some property""#, ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3866            (Some(json!("with spaces")), vec![]),
3867        );
3868    }
3869
3870    // In v0.3 and earlier, bare `true`/`false`/`null` parse as naked field
3871    // names (`{ true: $.true }` etc.). In v0.4 they parse as JSON literals
3872    // (LitExpr::Bool / LitExpr::Null). These tests pin that divergence.
3873
3874    #[test]
3875    fn test_bare_keyword_true_v0_3_is_field_name() {
3876        assert_eq!(
3877            selection!("true", ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3878            (Some(json!({ "true": "yes" })), vec![]),
3879        );
3880    }
3881
3882    #[test]
3883    fn test_bare_keyword_false_v0_3_is_field_name() {
3884        assert_eq!(
3885            selection!("false", ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3886            (Some(json!({ "false": "no" })), vec![]),
3887        );
3888    }
3889
3890    #[test]
3891    fn test_bare_keyword_null_v0_3_is_field_name() {
3892        assert_eq!(
3893            selection!("null", ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3894            (Some(json!({ "null": "nope" })), vec![]),
3895        );
3896    }
3897
3898    #[test]
3899    fn test_bare_keyword_true_v0_4_is_literal() {
3900        assert_eq!(
3901            selection!("true", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3902            (Some(json!(true)), vec![]),
3903        );
3904    }
3905
3906    #[test]
3907    fn test_bare_keyword_false_v0_4_is_literal() {
3908        assert_eq!(
3909            selection!("false", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3910            (Some(json!(false)), vec![]),
3911        );
3912    }
3913
3914    #[test]
3915    fn test_bare_keyword_null_v0_4_is_literal() {
3916        assert_eq!(
3917            selection!("null", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3918            (Some(json!(null)), vec![]),
3919        );
3920    }
3921
3922    // Same divergence for bare quoted strings: in v0.3 a quoted string at the
3923    // top level is a naked single-Key field name; in v0.4 it's a String
3924    // literal.
3925
3926    #[test]
3927    fn test_bare_quoted_string_v0_3_is_field_name() {
3928        assert_eq!(
3929            selection!(r#""some property""#, ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3930            (Some(json!({ "some property": "with spaces" })), vec![]),
3931        );
3932    }
3933
3934    #[test]
3935    fn test_bare_quoted_string_v0_4_is_literal() {
3936        assert_eq!(
3937            selection!(r#""some property""#, ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3938            (Some(json!("some property")), vec![]),
3939        );
3940    }
3941
3942    // The `?` PathList::Question operator propagates None when applied to a
3943    // null value. Under v0.4 it composes with bare literal heads at the top
3944    // level; `null?` short-circuits before any tail runs, `true?` / `false?`
3945    // pass through.
3946
3947    #[test]
3948    fn test_bare_null_question_v0_4_short_circuits() {
3949        assert_eq!(
3950            selection!("null?", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3951            (None, vec![]),
3952        );
3953    }
3954
3955    #[test]
3956    fn test_bare_true_question_v0_4_passes_through() {
3957        assert_eq!(
3958            selection!("true?", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3959            (Some(json!(true)), vec![]),
3960        );
3961    }
3962
3963    #[test]
3964    fn test_bare_false_question_v0_4_passes_through() {
3965        assert_eq!(
3966            selection!("false?", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3967            (Some(json!(false)), vec![]),
3968        );
3969    }
3970
3971    #[rstest]
3972    #[case::number("1234", "1234")]
3973    #[case::negative_number("-1", "-1")]
3974    #[case::bool_true("true", "true")]
3975    #[case::bool_false("false", "false")]
3976    #[case::null("null", "null")]
3977    #[case::string(r#""hello""#, r#""hello""#)]
3978    #[case::array("[1, 2, 3]", "[1, 2, 3]")]
3979    #[case::object("{ a: 1, b: 2 }", "{ a: 1, b: 2 }")]
3980    #[case::object_key_access("{ a: 1, b: 2 }.a", "1")]
3981    #[case::array_last("[1, 2, 3]->last", "3")]
3982    #[case::array_first("[1, 2, 3]->first", "1")]
3983    #[case::number_add("1234->add(1111)", "Int")]
3984    #[case::null_question("null?", "None")]
3985    #[case::true_question("true?", "true")]
3986    #[case::nullish_coalescing(r#"$args.maybe ?? "fallback""#, r#"One<$args.maybe?!, "fallback">"#)]
3987    #[case::multiline_nested_object(
3988        r#"{
3989            a: 1,
3990            b: {
3991                x: "hi",
3992                y: [1, 2, 3],
3993            },
3994        }"#,
3995        r#"{ a: 1, b: { x: "hi", y: [1, 2, 3] } }"#
3996    )]
3997    #[case::multiline_array_of_objects(
3998        r#"[
3999            { id: 1, label: "one" },
4000            { id: 2, label: "two" },
4001        ]"#,
4002        r#"[{ id: 1, label: "one" }, { id: 2, label: "two" }]"#
4003    )]
4004    #[case::multiline_method_chain(
4005        r#"[1, 2, 3]
4006            ->map(@)
4007            ->first"#,
4008        "1"
4009    )]
4010    #[case::multiline_object_with_comment(
4011        r#"{
4012            # pick the user id
4013            userid: $args.userid,
4014            name: "fixed",
4015        }"#,
4016        r#"{ name: "fixed", userid: $args.userid }"#
4017    )]
4018    #[case::multiline_nullish_coalescing_chain(
4019        r#"$args.a
4020            ?? $args.b
4021            ?? "fallback""#,
4022        r#"One<$args.a?!, $args.b?!, "fallback">"#
4023    )]
4024    #[case::multiline_copy_paste_json(
4025        r#"{
4026            hello: "world",
4027            count: 3,
4028            items: ["a", "b", "c"],
4029            when: $args.when,
4030        }"#,
4031        "{\n  count: 3,\n  hello: \"world\",\n  items: [\"a\", \"b\", \"c\"],\n  when: $args.when,\n}"
4032    )]
4033    fn test_v0_4_top_level_shape_literal(#[case] input: &str, #[case] expected: &str) {
4034        assert_eq!(
4035            selection!(input, ConnectSpec::V0_4).shape().pretty_print(),
4036            expected,
4037        );
4038    }
4039
4040    #[test]
4041    fn test_compute_output_shape() {
4042        let spec = ConnectSpec::V0_3;
4043
4044        assert_eq!(selection!("", spec).shape().pretty_print(), "{}");
4045
4046        assert_eq!(
4047            selection!("id name", spec).shape().pretty_print(),
4048            "{ id: $root.*.id, name: $root.*.name }",
4049        );
4050
4051        assert_eq!(
4052            selection!("$.data { thisOrThat: $(maybe.this ?? maybe.that) }", spec)
4053                .shape()
4054                .pretty_print(),
4055            "{ thisOrThat: One<$root.data.*.maybe.this?!, $root.data.*.maybe.that> }",
4056        );
4057
4058        assert_eq!(
4059            selection!(
4060                r#"
4061                id
4062                name
4063                friends: friend_ids { id: @ }
4064                alias: arrayOfArrays { x y }
4065                ys: arrayOfArrays.y xs: arrayOfArrays.x
4066            "#,
4067                spec
4068            )
4069            .shape()
4070            .pretty_print(),
4071            // This output shape is wrong if $root.friend_ids turns out to be an
4072            // array, and it's tricky to see how to transform the shape to what
4073            // it would have been if we knew that, where friends: List<{ id:
4074            // $root.friend_ids.* }> (note the * meaning any array index),
4075            // because who's to say it's not the id field that should become the
4076            // List, rather than the friends field?
4077            r#"{
4078  alias: {
4079    x: $root.*.arrayOfArrays.*.x,
4080    y: $root.*.arrayOfArrays.*.y,
4081  },
4082  friends: { id: $root.*.friend_ids.* },
4083  id: $root.*.id,
4084  name: $root.*.name,
4085  xs: $root.*.arrayOfArrays.x,
4086  ys: $root.*.arrayOfArrays.y,
4087}"#,
4088        );
4089
4090        assert_eq!(
4091            selection!(
4092                r#"
4093                id
4094                name
4095                friends: friend_ids->map({ id: @ })
4096                alias: arrayOfArrays { x y }
4097                ys: arrayOfArrays.y xs: arrayOfArrays.x
4098            "#,
4099                spec
4100            )
4101            .shape()
4102            .pretty_print(),
4103            r#"{
4104  alias: {
4105    x: $root.*.arrayOfArrays.*.x,
4106    y: $root.*.arrayOfArrays.*.y,
4107  },
4108  friends: List<{ id: $root.*.friend_ids.* }>,
4109  id: $root.*.id,
4110  name: $root.*.name,
4111  xs: $root.*.arrayOfArrays.x,
4112  ys: $root.*.arrayOfArrays.y,
4113}"#,
4114        );
4115
4116        assert_eq!(
4117            selection!("$->echo({ thrice: [@, @, @] })", spec)
4118                .shape()
4119                .pretty_print(),
4120            "{ thrice: [$root, $root, $root] }",
4121        );
4122
4123        assert_eq!(
4124            selection!("$->echo({ thrice: [@, @, @] })->entries", spec)
4125                .shape()
4126                .pretty_print(),
4127            "[{ key: \"thrice\", value: [$root, $root, $root] }]",
4128        );
4129
4130        assert_eq!(
4131            selection!("$->echo({ thrice: [@, @, @] })->entries.key", spec)
4132                .shape()
4133                .pretty_print(),
4134            "[\"thrice\"]",
4135        );
4136
4137        assert_eq!(
4138            selection!("$->echo({ thrice: [@, @, @] })->entries.value", spec)
4139                .shape()
4140                .pretty_print(),
4141            "[[$root, $root, $root]]",
4142        );
4143
4144        assert_eq!(
4145            selection!("$->echo({ wrapped: @ })->entries { k: key v: value }", spec)
4146                .shape()
4147                .pretty_print(),
4148            "[{ k: \"wrapped\", v: $root }]",
4149        );
4150    }
4151
4152    #[rstest]
4153    #[case::v0_3(ConnectSpec::V0_3)]
4154    #[case::v0_4(ConnectSpec::V0_4)]
4155    fn test_optional_key_access_with_existing_property(#[case] spec: ConnectSpec) {
4156        use serde_json_bytes::json;
4157
4158        let data = json!({
4159            "user": {
4160                "profile": {
4161                    "name": "Alice"
4162                }
4163            }
4164        });
4165
4166        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4167            .unwrap()
4168            .apply_to(&data);
4169        assert!(errors.is_empty());
4170        assert_eq!(result, Some(json!("Alice")));
4171    }
4172
4173    #[rstest]
4174    #[case::v0_3(ConnectSpec::V0_3)]
4175    #[case::v0_4(ConnectSpec::V0_4)]
4176    fn test_optional_key_access_with_null_value(#[case] spec: ConnectSpec) {
4177        use serde_json_bytes::json;
4178
4179        let data_null = json!({
4180            "user": null
4181        });
4182
4183        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4184            .unwrap()
4185            .apply_to(&data_null);
4186        assert!(errors.is_empty());
4187        assert_eq!(result, None);
4188    }
4189
4190    #[rstest]
4191    #[case::v0_3(ConnectSpec::V0_3)]
4192    #[case::v0_4(ConnectSpec::V0_4)]
4193    fn test_optional_key_access_on_non_object(#[case] spec: ConnectSpec) {
4194        use serde_json_bytes::json;
4195
4196        let data_non_obj = json!({
4197            "user": "not an object"
4198        });
4199
4200        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4201            .unwrap()
4202            .apply_to(&data_non_obj);
4203        assert_eq!(errors.len(), 1);
4204        assert!(
4205            errors[0]
4206                .message()
4207                .contains("Property .profile not found in string")
4208        );
4209        assert_eq!(result, None);
4210    }
4211
4212    #[rstest]
4213    #[case::v0_3(ConnectSpec::V0_3)]
4214    #[case::v0_4(ConnectSpec::V0_4)]
4215    fn test_optional_key_access_with_missing_property(#[case] spec: ConnectSpec) {
4216        use serde_json_bytes::json;
4217
4218        let data = json!({
4219            "user": {
4220                "other": "value"
4221            }
4222        });
4223
4224        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4225            .unwrap()
4226            .apply_to(&data);
4227        assert_eq!(errors.len(), 1);
4228        assert!(
4229            errors[0]
4230                .message()
4231                .contains("Property .profile not found in object")
4232        );
4233        assert_eq!(result, None);
4234    }
4235
4236    #[rstest]
4237    #[case::v0_3(ConnectSpec::V0_3)]
4238    #[case::v0_4(ConnectSpec::V0_4)]
4239    fn test_chained_optional_key_access(#[case] spec: ConnectSpec) {
4240        use serde_json_bytes::json;
4241
4242        let data = json!({
4243            "user": {
4244                "profile": {
4245                    "name": "Alice"
4246                }
4247            }
4248        });
4249
4250        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
4251            .unwrap()
4252            .apply_to(&data);
4253        assert!(errors.is_empty());
4254        assert_eq!(result, Some(json!("Alice")));
4255    }
4256
4257    #[rstest]
4258    #[case::v0_3(ConnectSpec::V0_3)]
4259    #[case::v0_4(ConnectSpec::V0_4)]
4260    fn test_chained_optional_access_with_null_in_middle(#[case] spec: ConnectSpec) {
4261        use serde_json_bytes::json;
4262
4263        let data_partial_null = json!({
4264            "user": {
4265                "profile": null
4266            }
4267        });
4268
4269        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
4270            .unwrap()
4271            .apply_to(&data_partial_null);
4272        assert!(errors.is_empty());
4273        assert_eq!(result, None);
4274    }
4275
4276    #[rstest]
4277    #[case::v0_3(ConnectSpec::V0_3)]
4278    #[case::v0_4(ConnectSpec::V0_4)]
4279    fn test_optional_method_on_null(#[case] spec: ConnectSpec) {
4280        use serde_json_bytes::json;
4281
4282        let data = json!({
4283            "items": null
4284        });
4285
4286        let (result, errors) = JSONSelection::parse_with_spec("$.items?->first", spec)
4287            .unwrap()
4288            .apply_to(&data);
4289        assert!(errors.is_empty());
4290        assert_eq!(result, None);
4291    }
4292
4293    #[rstest]
4294    #[case::v0_3(ConnectSpec::V0_3)]
4295    #[case::v0_4(ConnectSpec::V0_4)]
4296    fn test_optional_method_with_valid_method(#[case] spec: ConnectSpec) {
4297        use serde_json_bytes::json;
4298
4299        let data = json!({
4300            "values": [1, 2, 3]
4301        });
4302
4303        let (result, errors) = JSONSelection::parse_with_spec("$.values?->first", spec)
4304            .unwrap()
4305            .apply_to(&data);
4306        assert!(errors.is_empty());
4307        assert_eq!(result, Some(json!(1)));
4308    }
4309
4310    #[rstest]
4311    #[case::v0_3(ConnectSpec::V0_3)]
4312    #[case::v0_4(ConnectSpec::V0_4)]
4313    fn test_optional_method_with_unknown_method(#[case] spec: ConnectSpec) {
4314        use serde_json_bytes::json;
4315
4316        let data = json!({
4317            "values": [1, 2, 3]
4318        });
4319
4320        let (result, errors) = JSONSelection::parse_with_spec("$.values?->length", spec)
4321            .unwrap()
4322            .apply_to(&data);
4323        assert_eq!(errors.len(), 1);
4324        assert!(errors[0].message().contains("Method ->length not found"));
4325        assert_eq!(result, None);
4326    }
4327
4328    #[rstest]
4329    #[case::v0_3(ConnectSpec::V0_3)]
4330    #[case::v0_4(ConnectSpec::V0_4)]
4331    fn test_optional_chaining_with_subselection_on_valid_data(#[case] spec: ConnectSpec) {
4332        use serde_json_bytes::json;
4333
4334        let data = json!({
4335            "user": {
4336                "profile": {
4337                    "name": "Alice",
4338                    "age": 30,
4339                    "email": "alice@example.com"
4340                }
4341            }
4342        });
4343
4344        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
4345            .unwrap()
4346            .apply_to(&data);
4347        assert!(errors.is_empty());
4348        assert_eq!(
4349            result,
4350            Some(json!({
4351                "name": "Alice",
4352                "age": 30
4353            }))
4354        );
4355    }
4356
4357    #[rstest]
4358    #[case::v0_3(ConnectSpec::V0_3)]
4359    #[case::v0_4(ConnectSpec::V0_4)]
4360    fn test_optional_chaining_with_subselection_on_null_data(#[case] spec: ConnectSpec) {
4361        use serde_json_bytes::json;
4362
4363        let data_null = json!({
4364            "user": null
4365        });
4366
4367        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
4368            .unwrap()
4369            .apply_to(&data_null);
4370        assert!(errors.is_empty());
4371        assert_eq!(result, None);
4372    }
4373
4374    #[rstest]
4375    #[case::v0_3(ConnectSpec::V0_3)]
4376    #[case::v0_4(ConnectSpec::V0_4)]
4377    fn test_mixed_regular_and_optional_chaining_working_case(#[case] spec: ConnectSpec) {
4378        use serde_json_bytes::json;
4379
4380        let data = json!({
4381            "response": {
4382                "data": {
4383                    "user": {
4384                        "profile": {
4385                            "name": "Bob"
4386                        }
4387                    }
4388                }
4389            }
4390        });
4391
4392        let (result, errors) =
4393            JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
4394                .unwrap()
4395                .apply_to(&data);
4396        assert!(errors.is_empty());
4397        assert_eq!(result, Some(json!("Bob")));
4398    }
4399
4400    #[rstest]
4401    #[case::v0_3(ConnectSpec::V0_3)]
4402    #[case::v0_4(ConnectSpec::V0_4)]
4403    fn test_mixed_regular_and_optional_chaining_with_null(#[case] spec: ConnectSpec) {
4404        use serde_json_bytes::json;
4405
4406        let data_null_data = json!({
4407            "response": {
4408                "data": null
4409            }
4410        });
4411
4412        let (result, errors) =
4413            JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
4414                .unwrap()
4415                .apply_to(&data_null_data);
4416        assert!(errors.is_empty());
4417        assert_eq!(result, None);
4418    }
4419
4420    #[rstest]
4421    #[case::v0_3(ConnectSpec::V0_3)]
4422    #[case::v0_4(ConnectSpec::V0_4)]
4423    fn test_optional_selection_set_with_valid_data(#[case] spec: ConnectSpec) {
4424        use serde_json_bytes::json;
4425
4426        let data = json!({
4427            "user": {
4428                "id": 123,
4429                "name": "Alice"
4430            }
4431        });
4432
4433        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4434            .unwrap()
4435            .apply_to(&data);
4436        assert_eq!(
4437            result,
4438            Some(json!({
4439                "id": 123,
4440                "name": "Alice"
4441            }))
4442        );
4443        assert_eq!(errors, vec![]);
4444    }
4445
4446    #[rstest]
4447    #[case::v0_3(ConnectSpec::V0_3)]
4448    #[case::v0_4(ConnectSpec::V0_4)]
4449    fn test_optional_selection_set_with_null_data(#[case] spec: ConnectSpec) {
4450        use serde_json_bytes::json;
4451
4452        let data = json!({
4453            "user": null
4454        });
4455
4456        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4457            .unwrap()
4458            .apply_to(&data);
4459        assert_eq!(result, None);
4460        assert_eq!(errors, vec![]);
4461    }
4462
4463    #[rstest]
4464    #[case::v0_3(ConnectSpec::V0_3)]
4465    #[case::v0_4(ConnectSpec::V0_4)]
4466    fn test_optional_selection_set_with_missing_property(#[case] spec: ConnectSpec) {
4467        use serde_json_bytes::json;
4468
4469        let data = json!({
4470            "other": "value"
4471        });
4472
4473        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4474            .unwrap()
4475            .apply_to(&data);
4476        assert_eq!(result, None);
4477        assert_eq!(errors.len(), 0);
4478    }
4479
4480    #[rstest]
4481    #[case::v0_3(ConnectSpec::V0_3)]
4482    #[case::v0_4(ConnectSpec::V0_4)]
4483    fn test_optional_selection_set_with_non_object(#[case] spec: ConnectSpec) {
4484        use serde_json_bytes::json;
4485
4486        let data = json!({
4487            "user": "not an object"
4488        });
4489
4490        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4491            .unwrap()
4492            .apply_to(&data);
4493        // When data is not null but not an object, SubSelection still tries to access properties
4494        // This results in errors, but returns the original value since no properties were found
4495        assert_eq!(result, Some(json!("not an object")));
4496        assert_eq!(errors.len(), 2);
4497        assert!(
4498            errors[0]
4499                .message()
4500                .contains("Property .id not found in string")
4501        );
4502        assert!(
4503            errors[1]
4504                .message()
4505                .contains("Property .name not found in string")
4506        );
4507    }
4508
4509    #[test]
4510    fn test_optional_field_selections() {
4511        let spec = ConnectSpec::V0_3;
4512        let author_selection = selection!("author? { age middleName? }", spec);
4513        assert_debug_snapshot!(author_selection);
4514        assert_eq!(
4515            author_selection.pretty_print(),
4516            "author? { age middleName? }",
4517        );
4518        assert_eq!(
4519            author_selection.shape().pretty_print(),
4520            r#"{ author: One<
4521    {
4522      age: $root.*.author?.*.age,
4523      middleName: $root.*.author?.*.middleName?,
4524    },
4525    None,
4526  > }"#,
4527        );
4528    }
4529
4530    #[test]
4531    fn test_optional_input_shape_with_selection() {
4532        let spec = ConnectSpec::V0_3;
4533        let optional_author_shape_selection =
4534            selection!("unreliableAuthor { age middleName? }", spec);
4535
4536        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
4537            .with_spec(spec)
4538            .with_named_shapes([(
4539                "$root".to_string(),
4540                Shape::record(
4541                    {
4542                        let mut map = Shape::empty_map();
4543                        map.insert(
4544                            "unreliableAuthor".to_string(),
4545                            Shape::one(
4546                                [
4547                                    Shape::record(
4548                                        {
4549                                            let mut map = Shape::empty_map();
4550                                            map.insert("age".to_string(), Shape::int([]));
4551                                            map.insert(
4552                                                "middleName".to_string(),
4553                                                Shape::one([Shape::string([]), Shape::none()], []),
4554                                            );
4555                                            map
4556                                        },
4557                                        [],
4558                                    ),
4559                                    Shape::none(),
4560                                ],
4561                                [],
4562                            ),
4563                        );
4564                        map
4565                    },
4566                    [],
4567                ),
4568            )]);
4569
4570        assert_eq!(
4571            optional_author_shape_selection
4572                .compute_output_shape(
4573                    &shape_context,
4574                    shape_context.named_shapes().get("$root").unwrap().clone(),
4575                )
4576                .pretty_print(),
4577            "{ unreliableAuthor: One<{ age: Int, middleName: One<String, None> }, None> }",
4578        );
4579    }
4580
4581    // TODO Reenable these tests in ConnectSpec::V0_4 when we support ... spread
4582    // syntax and abstract types.
4583    /** #[cfg(test)]
4584    mod spread {
4585        use serde_json_bytes::Value as JSON;
4586        use serde_json_bytes::json;
4587        use shape::Shape;
4588        use shape::location::SourceId;
4589
4590        use crate::connectors::ConnectSpec;
4591        use crate::connectors::json_selection::ShapeContext;
4592
4593        #[derive(Debug)]
4594        pub(super) struct SetupItems {
4595            pub data: JSON,
4596            pub shape_context: ShapeContext,
4597            pub root_shape: Shape,
4598        }
4599
4600        pub(super) fn setup(spec: ConnectSpec) -> SetupItems {
4601            let a_b_data = json!({
4602                "a": { "phonetic": "ay" },
4603                "b": { "phonetic": "bee" },
4604            });
4605
4606            let a_b_data_shape = Shape::from_json_bytes(&a_b_data);
4607
4608            let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
4609                .with_spec(spec)
4610                .with_named_shapes([("$root".to_string(), a_b_data_shape)]);
4611
4612            let root_shape = shape_context.named_shapes().get("$root").unwrap().clone();
4613
4614            SetupItems {
4615                data: a_b_data,
4616                shape_context,
4617                root_shape,
4618            }
4619        }
4620    }
4621
4622    #[test]
4623    fn test_spread_syntax_spread_a() {
4624        let spec = ConnectSpec::V0_4;
4625        let spread::SetupItems {
4626            data: a_b_data,
4627            shape_context,
4628            root_shape,
4629        } = spread::setup(spec);
4630
4631        let spread_a = selection!("...a", spec);
4632        assert_eq!(
4633            spread_a.apply_to(&a_b_data),
4634            (Some(json!({"phonetic": "ay"})), vec![]),
4635        );
4636        assert_eq!(spread_a.shape().pretty_print(), "$root.*.a",);
4637        assert_eq!(
4638            spread_a
4639                .compute_output_shape(&shape_context, root_shape)
4640                .pretty_print(),
4641            "{ phonetic: \"ay\" }",
4642        );
4643    }
4644    **/
4645
4646    #[rstest]
4647    #[case::v0_3(ConnectSpec::V0_3)]
4648    #[case::v0_4(ConnectSpec::V0_4)]
4649    fn test_nested_optional_selection_sets(#[case] spec: ConnectSpec) {
4650        use serde_json_bytes::json;
4651
4652        let data = json!({
4653            "user": {
4654                "profile": {
4655                    "name": "Alice",
4656                    "email": "alice@example.com"
4657                }
4658            }
4659        });
4660
4661        let (result, errors) =
4662            JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4663                .unwrap()
4664                .apply_to(&data);
4665        assert_eq!(
4666            result,
4667            Some(json!({
4668                "name": "Alice",
4669                "email": "alice@example.com"
4670            }))
4671        );
4672        assert_eq!(errors, vec![]);
4673
4674        // Test with null nested data
4675        let data_with_null_profile = json!({
4676            "user": {
4677                "profile": null
4678            }
4679        });
4680
4681        let (result, errors) =
4682            JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4683                .unwrap()
4684                .apply_to(&data_with_null_profile);
4685        assert_eq!(result, None);
4686        assert_eq!(errors, vec![]);
4687    }
4688
4689    #[rstest]
4690    #[case::v0_3(ConnectSpec::V0_3)]
4691    #[case::v0_4(ConnectSpec::V0_4)]
4692    fn test_mixed_optional_selection_and_optional_chaining(#[case] spec: ConnectSpec) {
4693        use serde_json_bytes::json;
4694
4695        let data = json!({
4696            "user": {
4697                "id": 123,
4698                "profile": null
4699            }
4700        });
4701
4702        let (result, errors) =
4703            JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4704                .unwrap()
4705                .apply_to(&data);
4706        assert_eq!(
4707            result,
4708            Some(json!({
4709                "id": 123
4710            }))
4711        );
4712        assert_eq!(errors, vec![]);
4713
4714        // Test with missing user
4715        let data_no_user = json!({
4716            "other": "value"
4717        });
4718
4719        let (result, errors) =
4720            JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4721                .unwrap()
4722                .apply_to(&data_no_user);
4723        assert_eq!(result, None);
4724        assert_eq!(errors.len(), 0);
4725    }
4726
4727    #[rstest]
4728    #[case::v0_3(ConnectSpec::V0_3)]
4729    #[case::v0_4(ConnectSpec::V0_4)]
4730    fn test_optional_selection_set_parsing(#[case] spec: ConnectSpec) {
4731        // Test that the parser correctly handles optional selection sets
4732        let selection = JSONSelection::parse_with_spec("$.user? { id name }", spec).unwrap();
4733        assert_eq!(selection.pretty_print(), "$.user? { id name }");
4734
4735        // Test with nested optional selection sets
4736        let selection = JSONSelection::parse_with_spec("$.user.profile? { name }", spec).unwrap();
4737        assert_eq!(selection.pretty_print(), "$.user.profile? { name }");
4738
4739        // Test mixed with regular selection sets
4740        let selection =
4741            JSONSelection::parse_with_spec("$.user? { id profile { name } }", spec).unwrap();
4742        assert_eq!(selection.pretty_print(), "$.user? { id profile { name } }");
4743    }
4744
4745    #[rstest]
4746    #[case::v0_3(ConnectSpec::V0_3)]
4747    #[case::v0_4(ConnectSpec::V0_4)]
4748    fn test_optional_selection_set_with_arrays(#[case] spec: ConnectSpec) {
4749        use serde_json_bytes::json;
4750
4751        let data = json!({
4752            "users": [
4753                {
4754                    "id": 1,
4755                    "name": "Alice"
4756                },
4757                null,
4758                {
4759                    "id": 3,
4760                    "name": "Charlie"
4761                }
4762            ]
4763        });
4764
4765        let (result, errors) = JSONSelection::parse_with_spec("$.users ?{ id name }", spec)
4766            .unwrap()
4767            .apply_to(&data);
4768        assert_eq!(
4769            result,
4770            Some(json!([
4771                {
4772                    "id": 1,
4773                    "name": "Alice"
4774                },
4775                null,
4776                {
4777                    "id": 3,
4778                    "name": "Charlie"
4779                }
4780            ]))
4781        );
4782
4783        assert_eq!(errors.len(), 2);
4784        assert!(
4785            errors[0]
4786                .message()
4787                .contains("Property .id not found in null")
4788        );
4789        assert!(
4790            errors[1]
4791                .message()
4792                .contains("Property .name not found in null")
4793        );
4794    }
4795
4796    // TODO Reenable these tests in ConnectSpec::V0_4 when we support ... spread
4797    // syntax and abstract types.
4798    /*
4799    #[test]
4800    fn test_spread_syntax_a_spread_b() {
4801        let spec = ConnectSpec::V0_4;
4802        let spread::SetupItems {
4803            data: a_b_data,
4804            shape_context,
4805            root_shape,
4806        } = spread::setup(spec);
4807
4808        let a_spread_b = selection!("a...b", spec);
4809        assert_eq!(
4810            a_spread_b.apply_to(&a_b_data),
4811            (
4812                Some(json!({"a": { "phonetic": "ay" }, "phonetic": "bee" })),
4813                vec![]
4814            ),
4815        );
4816        assert_eq!(
4817            a_spread_b.shape().pretty_print(),
4818            "All<$root.*.b, { a: $root.*.a }>",
4819        );
4820        assert_eq!(
4821            a_spread_b
4822                .compute_output_shape(&shape_context, root_shape)
4823                .pretty_print(),
4824            "{ a: { phonetic: \"ay\" }, phonetic: \"bee\" }",
4825        );
4826    }
4827
4828    #[test]
4829    fn test_spread_syntax_spread_a_b() {
4830        let spec = ConnectSpec::V0_4;
4831        let spread::SetupItems {
4832            data: a_b_data,
4833            shape_context,
4834            root_shape,
4835        } = spread::setup(spec);
4836
4837        let spread_a_b = selection!("...a b", spec);
4838        assert_eq!(
4839            spread_a_b.apply_to(&a_b_data),
4840            (
4841                Some(json!({"phonetic": "ay", "b": { "phonetic": "bee" }})),
4842                vec![]
4843            ),
4844        );
4845        assert_eq!(
4846            spread_a_b.shape().pretty_print(),
4847            "All<$root.*.a, { b: $root.*.b }>",
4848        );
4849        assert_eq!(
4850            spread_a_b
4851                .compute_output_shape(&shape_context, root_shape)
4852                .pretty_print(),
4853            "{ b: { phonetic: \"bee\" }, phonetic: \"ay\" }",
4854        );
4855    }
4856
4857    #[test]
4858    fn test_spread_match_none() {
4859        let spec = ConnectSpec::V0_4;
4860
4861        let sel = selection!(
4862            "before ...condition->match([true, { matched: true }]) after",
4863            spec
4864        );
4865        assert_eq!(
4866            sel.shape().pretty_print(),
4867            r#"One<
4868  {
4869    after: $root.*.after,
4870    before: $root.*.before,
4871    matched: true,
4872  },
4873  { after: $root.*.after, before: $root.*.before },
4874>"#,
4875        );
4876
4877        assert_eq!(
4878            sel.apply_to(&json!({
4879                "before": "before value",
4880                "after": "after value",
4881                "condition": true,
4882            })),
4883            (
4884                Some(json!({
4885                    "before": "before value",
4886                    "after": "after value",
4887                    "matched": true,
4888                })),
4889                vec![],
4890            ),
4891        );
4892
4893        assert_eq!(
4894            sel.apply_to(&json!({
4895                "before": "before value",
4896                "after": "after value",
4897                "condition": false,
4898            })),
4899            (
4900                Some(json!({
4901                    "before": "before value",
4902                    "after": "after value",
4903                })),
4904                vec![
4905                    ApplyToError::new(
4906                        "Method ->match did not match any [candidate, value] pair".to_string(),
4907                        vec![json!("condition"), json!("->match")],
4908                        Some(21..53),
4909                        spec,
4910                    ),
4911                    ApplyToError::new(
4912                        "Inlined path produced no value".to_string(),
4913                        vec![],
4914                        Some(10..53),
4915                        spec,
4916                    )
4917                ],
4918            ),
4919        );
4920    }
4921
4922    #[cfg(test)]
4923    mod spread_with_match {
4924        use crate::connectors::ConnectSpec;
4925        use crate::connectors::JSONSelection;
4926        use crate::selection;
4927
4928        pub(super) fn get_selection(spec: ConnectSpec) -> JSONSelection {
4929            let sel = selection!(
4930                r#"
4931                upc
4932                ... type->match(
4933                    ["book", {
4934                        __typename: "Book",
4935                        title: title,
4936                        author: { name: author.name },
4937                    }],
4938                    ["movie", {
4939                        __typename: "Movie",
4940                        title: title,
4941                        director: director.name,
4942                    }],
4943                    ["magazine", {
4944                        __typename: "Magazine",
4945                        title: title,
4946                        editor: editor.name,
4947                    }],
4948                    ["dummy", {}],
4949                    [@, null],
4950                )
4951                "#,
4952                spec
4953            );
4954
4955            assert_eq!(
4956                sel.shape().pretty_print(),
4957                r#"One<
4958  {
4959    __typename: "Book",
4960    author: { name: $root.*.author.name },
4961    title: $root.*.title,
4962    upc: $root.*.upc,
4963  },
4964  {
4965    __typename: "Movie",
4966    director: $root.*.director.name,
4967    title: $root.*.title,
4968    upc: $root.*.upc,
4969  },
4970  {
4971    __typename: "Magazine",
4972    editor: $root.*.editor.name,
4973    title: $root.*.title,
4974    upc: $root.*.upc,
4975  },
4976  { upc: $root.*.upc },
4977  null,
4978>"#
4979            );
4980
4981            sel
4982        }
4983    }
4984
4985    #[test]
4986    fn test_spread_with_match_book() {
4987        let spec = ConnectSpec::V0_4;
4988        let sel = spread_with_match::get_selection(spec);
4989
4990        let book_data = json!({
4991            "upc": "1234567890",
4992            "type": "book",
4993            "title": "The Great Gatsby",
4994            "author": { "name": "F. Scott Fitzgerald" },
4995        });
4996        assert_eq!(
4997            sel.apply_to(&book_data),
4998            (
4999                Some(json!({
5000                    "__typename": "Book",
5001                    "upc": "1234567890",
5002                    "title": "The Great Gatsby",
5003                    "author": { "name": "F. Scott Fitzgerald" },
5004                })),
5005                vec![],
5006            ),
5007        );
5008    }
5009
5010    #[test]
5011    fn test_spread_with_match_movie() {
5012        let spec = ConnectSpec::V0_4;
5013        let sel = spread_with_match::get_selection(spec);
5014
5015        let movie_data = json!({
5016            "upc": "0987654321",
5017            "type": "movie",
5018            "title": "Inception",
5019            "director": { "name": "Christopher Nolan" },
5020        });
5021        assert_eq!(
5022            sel.apply_to(&movie_data),
5023            (
5024                Some(json!({
5025                    "__typename": "Movie",
5026                    "upc": "0987654321",
5027                    "title": "Inception",
5028                    "director": "Christopher Nolan",
5029                })),
5030                vec![],
5031            ),
5032        );
5033    }
5034
5035    #[test]
5036    fn test_spread_with_match_magazine() {
5037        let spec = ConnectSpec::V0_4;
5038        let sel = spread_with_match::get_selection(spec);
5039
5040        let magazine_data = json!({
5041            "upc": "1122334455",
5042            "type": "magazine",
5043            "title": "National Geographic",
5044            "editor": { "name": "Susan Goldberg" },
5045        });
5046        assert_eq!(
5047            sel.apply_to(&magazine_data),
5048            (
5049                Some(json!({
5050                    "__typename": "Magazine",
5051                    "upc": "1122334455",
5052                    "title": "National Geographic",
5053                    "editor": "Susan Goldberg",
5054                })),
5055                vec![],
5056            ),
5057        );
5058    }
5059
5060    #[test]
5061    fn test_spread_with_match_dummy() {
5062        let spec = ConnectSpec::V0_4;
5063        let sel = spread_with_match::get_selection(spec);
5064
5065        let dummy_data = json!({
5066            "upc": "5566778899",
5067            "type": "dummy",
5068        });
5069        assert_eq!(
5070            sel.apply_to(&dummy_data),
5071            (
5072                Some(json!({
5073                    "upc": "5566778899",
5074                })),
5075                vec![],
5076            ),
5077        );
5078    }
5079
5080    #[test]
5081    fn test_spread_with_match_unknown() {
5082        let spec = ConnectSpec::V0_4;
5083        let sel = spread_with_match::get_selection(spec);
5084
5085        let unknown_data = json!({
5086            "upc": "9988776655",
5087            "type": "music",
5088            "title": "The White Stripes",
5089            "artist": { "name": "Jack White" },
5090        });
5091        assert_eq!(sel.apply_to(&unknown_data), (Some(json!(null)), vec![]));
5092    }
5093
5094    #[test]
5095    fn test_spread_null() {
5096        let spec = ConnectSpec::V0_4;
5097        assert_eq!(
5098            selection!("...$(null)", spec).apply_to(&json!({ "ignored": "data" })),
5099            (Some(json!(null)), vec![]),
5100        );
5101        assert_eq!(
5102            selection!("ignored ...$(null)", spec).apply_to(&json!({ "ignored": "data" })),
5103            (Some(json!(null)), vec![]),
5104        );
5105        assert_eq!(
5106            selection!("...$(null) ignored", spec).apply_to(&json!({ "ignored": "data" })),
5107            (Some(json!(null)), vec![]),
5108        );
5109        assert_eq!(
5110            selection!("group: { a ...b }", spec).apply_to(&json!({ "a": "ay", "b": null })),
5111            (Some(json!({ "group": null })), vec![]),
5112        );
5113    }
5114
5115    #[test]
5116    fn test_spread_missing() {
5117        let spec = ConnectSpec::V0_4;
5118
5119        assert_eq!(
5120            selection!("a ...missing z", spec).apply_to(&json!({ "a": "ay", "z": "zee" })),
5121            (
5122                Some(json!({
5123                    "a": "ay",
5124                    "z": "zee",
5125                })),
5126                vec![
5127                    ApplyToError::new(
5128                        "Property .missing not found in object".to_string(),
5129                        vec![json!("missing")],
5130                        Some(5..12),
5131                        spec,
5132                    ),
5133                    ApplyToError::new(
5134                        "Inlined path produced no value".to_string(),
5135                        vec![],
5136                        Some(5..12),
5137                        spec,
5138                    ),
5139                ],
5140            ),
5141        );
5142
5143        assert_eq!(
5144            selection!("a ...$(missing) z", spec).apply_to(&json!({ "a": "ay", "z": "zee" })),
5145            (
5146                Some(json!({
5147                    "a": "ay",
5148                    "z": "zee",
5149                })),
5150                vec![
5151                    ApplyToError::new(
5152                        "Property .missing not found in object".to_string(),
5153                        vec![json!("missing")],
5154                        Some(7..14),
5155                        spec,
5156                    ),
5157                    ApplyToError::new(
5158                        "Inlined path produced no value".to_string(),
5159                        vec![],
5160                        Some(5..15),
5161                        spec,
5162                    ),
5163                ],
5164            ),
5165        );
5166    }
5167
5168    #[test]
5169    fn test_spread_invalid_numbers() {
5170        let spec = ConnectSpec::V0_4;
5171
5172        assert_eq!(
5173            selection!("...invalid", spec).apply_to(&json!({ "invalid": 123 })),
5174            (
5175                Some(json!({})),
5176                vec![ApplyToError::new(
5177                    "Expected object or null, not number".to_string(),
5178                    vec![],
5179                    Some(3..10),
5180                    spec,
5181                )],
5182            ),
5183        );
5184
5185        assert_eq!(
5186            selection!(" ... $( invalid ) ", spec).apply_to(&json!({ "invalid": 234 })),
5187            (
5188                Some(json!({})),
5189                vec![ApplyToError::new(
5190                    "Expected object or null, not number".to_string(),
5191                    vec![],
5192                    Some(5..17),
5193                    spec,
5194                )],
5195            ),
5196        );
5197    }
5198
5199    #[test]
5200    fn test_spread_invalid_bools() {
5201        let spec = ConnectSpec::V0_4;
5202
5203        assert_eq!(
5204            selection!("...invalid", spec).apply_to(&json!({ "invalid": true })),
5205            (
5206                Some(json!({})),
5207                vec![ApplyToError::new(
5208                    "Expected object or null, not boolean".to_string(),
5209                    vec![],
5210                    Some(3..10),
5211                    spec,
5212                )],
5213            ),
5214        );
5215
5216        assert_eq!(
5217            selection!("...$(invalid)", spec).apply_to(&json!({ "invalid": false })),
5218            (
5219                Some(json!({})),
5220                vec![ApplyToError::new(
5221                    "Expected object or null, not boolean".to_string(),
5222                    vec![],
5223                    Some(3..13),
5224                    spec,
5225                )],
5226            ),
5227        );
5228    }
5229
5230    #[test]
5231    fn test_spread_invalid_strings() {
5232        let spec = ConnectSpec::V0_4;
5233
5234        assert_eq!(
5235            selection!("...invalid", spec).apply_to(&json!({ "invalid": "string" })),
5236            (
5237                Some(json!({})),
5238                vec![ApplyToError::new(
5239                    "Expected object or null, not string".to_string(),
5240                    vec![],
5241                    Some(3..10),
5242                    spec,
5243                )],
5244            ),
5245        );
5246
5247        assert_eq!(
5248            selection!("...$(invalid)", spec).apply_to(&json!({ "invalid": "string" })),
5249            (
5250                Some(json!({})),
5251                vec![ApplyToError::new(
5252                    "Expected object or null, not string".to_string(),
5253                    vec![],
5254                    Some(3..13),
5255                    spec,
5256                )],
5257            ),
5258        );
5259    }
5260
5261    #[test]
5262    fn test_spread_invalid_arrays() {
5263        let spec = ConnectSpec::V0_4;
5264
5265        // The ... operator only works for objects for now, as it spreads their
5266        // keys into some larger object. We may support array spreading in the
5267        // future, but it will probably work somewhat differently (it may be
5268        // available only within literal expressions, for example).
5269        assert_eq!(
5270            selection!("...invalid", spec).apply_to(&json!({ "invalid": [1, 2, 3] })),
5271            (
5272                Some(json!({})),
5273                vec![ApplyToError::new(
5274                    "Expected object or null, not array".to_string(),
5275                    vec![],
5276                    Some(3..10),
5277                    spec,
5278                )],
5279            ),
5280        );
5281
5282        assert_eq!(
5283            selection!("...$(invalid)", spec).apply_to(&json!({ "invalid": [] })),
5284            (
5285                Some(json!({})),
5286                vec![ApplyToError::new(
5287                    "Expected object or null, not array".to_string(),
5288                    vec![],
5289                    Some(3..13),
5290                    spec,
5291                )],
5292            ),
5293        );
5294    }
5295
5296    #[test]
5297    fn test_spread_output_shapes() {
5298        let spec = ConnectSpec::V0_4;
5299
5300        assert_eq!(selection!("...a", spec).shape().pretty_print(), "$root.*.a");
5301        assert_eq!(
5302            selection!("...$(a)", spec).shape().pretty_print(),
5303            "$root.*.a",
5304        );
5305
5306        assert_eq!(
5307            selection!("a ...b", spec).shape().pretty_print(),
5308            "All<$root.*.b, { a: $root.*.a }>",
5309        );
5310        assert_eq!(
5311            selection!("a ...$(b)", spec).shape().pretty_print(),
5312            "All<$root.*.b, { a: $root.*.a }>",
5313        );
5314
5315        assert_eq!(
5316            selection!("a ...b c", spec).shape().pretty_print(),
5317            "All<$root.*.b, { a: $root.*.a, c: $root.*.c }>",
5318        );
5319        assert_eq!(
5320            selection!("a ...$(b) c", spec).shape().pretty_print(),
5321            "All<$root.*.b, { a: $root.*.a, c: $root.*.c }>",
5322        );
5323    }
5324    **/
5325
5326    #[test]
5327    fn null_coalescing_should_return_left_when_left_not_null() {
5328        let spec = ConnectSpec::V0_3;
5329        assert_eq!(
5330            selection!("$('Foo' ?? 'Bar')", spec).apply_to(&json!({})),
5331            (Some(json!("Foo")), vec![]),
5332        );
5333    }
5334
5335    #[test]
5336    fn null_coalescing_should_return_right_when_left_is_null() {
5337        let spec = ConnectSpec::V0_3;
5338        assert_eq!(
5339            selection!("$(null ?? 'Bar')", spec).apply_to(&json!({})),
5340            (Some(json!("Bar")), vec![]),
5341        );
5342    }
5343
5344    #[test]
5345    fn none_coalescing_should_return_left_when_left_not_none() {
5346        let spec = ConnectSpec::V0_3;
5347        assert_eq!(
5348            selection!("$('Foo' ?! 'Bar')", spec).apply_to(&json!({})),
5349            (Some(json!("Foo")), vec![]),
5350        );
5351    }
5352
5353    #[test]
5354    fn none_coalescing_should_preserve_null_when_left_is_null() {
5355        let spec = ConnectSpec::V0_3;
5356        assert_eq!(
5357            selection!("$(null ?! 'Bar')", spec).apply_to(&json!({})),
5358            (Some(json!(null)), vec![]),
5359        );
5360    }
5361
5362    #[test]
5363    fn nullish_coalescing_should_return_final_null() {
5364        let spec = ConnectSpec::V0_3;
5365        assert_eq!(
5366            selection!("$(missing ?? null)", spec).apply_to(&json!({})),
5367            (Some(json!(null)), vec![]),
5368        );
5369        assert_eq!(
5370            selection!("$(missing ?! null)", spec).apply_to(&json!({})),
5371            (Some(json!(null)), vec![]),
5372        );
5373    }
5374
5375    #[test]
5376    fn nullish_coalescing_should_return_final_none() {
5377        let spec = ConnectSpec::V0_3;
5378        assert_eq!(
5379            selection!("$(missing ?? also_missing)", spec).apply_to(&json!({})),
5380            (
5381                None,
5382                vec![
5383                    ApplyToError::new(
5384                        "Property .missing not found in object".to_string(),
5385                        vec![json!("missing")],
5386                        Some(2..9),
5387                        spec,
5388                    ),
5389                    ApplyToError::new(
5390                        "Property .also_missing not found in object".to_string(),
5391                        vec![json!("also_missing")],
5392                        Some(13..25),
5393                        spec,
5394                    ),
5395                ]
5396            ),
5397        );
5398        assert_eq!(
5399            selection!("maybe: $(missing ?! also_missing)", spec).apply_to(&json!({})),
5400            (
5401                Some(json!({})),
5402                vec![
5403                    ApplyToError::new(
5404                        "Property .missing not found in object".to_string(),
5405                        vec![json!("missing")],
5406                        Some(9..16),
5407                        spec,
5408                    ),
5409                    ApplyToError::new(
5410                        "Property .also_missing not found in object".to_string(),
5411                        vec![json!("also_missing")],
5412                        Some(20..32),
5413                        spec,
5414                    ),
5415                ]
5416            ),
5417        );
5418    }
5419
5420    #[test]
5421    fn coalescing_operators_should_return_earlier_values_if_later_missing() {
5422        let spec = ConnectSpec::V0_3;
5423        assert_eq!(
5424            selection!("$(1234 ?? missing)", spec).apply_to(&json!({})),
5425            (Some(json!(1234)), vec![]),
5426        );
5427        assert_eq!(
5428            selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": 1234 })),
5429            (Some(json!(1234)), vec![]),
5430        );
5431        assert_eq!(
5432            selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": null })),
5433            (
5434                None,
5435                vec![ApplyToError::new(
5436                    "Property .missing not found in object".to_string(),
5437                    vec![json!("missing")],
5438                    Some(10..17),
5439                    spec,
5440                )]
5441            ),
5442        );
5443        assert_eq!(
5444            selection!("$(null ?! missing)", spec).apply_to(&json!({})),
5445            (Some(json!(null)), vec![]),
5446        );
5447        assert_eq!(
5448            selection!("$(item ?! missing)", spec).apply_to(&json!({ "item": null })),
5449            (Some(json!(null)), vec![]),
5450        );
5451    }
5452
5453    #[test]
5454    fn null_coalescing_should_chain_left_to_right_when_multiple_nulls() {
5455        // TODO: TEST HERE
5456        let spec = ConnectSpec::V0_3;
5457        assert_eq!(
5458            selection!("$(null ?? null ?? 'Bar')", spec).apply_to(&json!({})),
5459            (Some(json!("Bar")), vec![]),
5460        );
5461    }
5462
5463    #[test]
5464    fn null_coalescing_should_stop_at_first_non_null_when_chaining() {
5465        let spec = ConnectSpec::V0_3;
5466        assert_eq!(
5467            selection!("$('Foo' ?? null ?? 'Bar')", spec).apply_to(&json!({})),
5468            (Some(json!("Foo")), vec![]),
5469        );
5470    }
5471
5472    #[test]
5473    fn null_coalescing_should_fallback_when_field_is_null() {
5474        let spec = ConnectSpec::V0_3;
5475        let data = json!({"field1": null, "field2": "value2"});
5476        assert_eq!(
5477            selection!("$($.field1 ?? $.field2)", spec).apply_to(&data),
5478            (Some(json!("value2")), vec![]),
5479        );
5480    }
5481
5482    #[test]
5483    fn null_coalescing_should_use_literal_fallback_when_all_fields_null() {
5484        let spec = ConnectSpec::V0_3;
5485        let data = json!({"field1": null, "field3": null});
5486        assert_eq!(
5487            selection!("$($.field1 ?? $.field3 ?? 'fallback')", spec).apply_to(&data),
5488            (Some(json!("fallback")), vec![]),
5489        );
5490    }
5491
5492    #[test]
5493    fn none_coalescing_should_preserve_null_field() {
5494        let spec = ConnectSpec::V0_3;
5495        let data = json!({"nullField": null});
5496        assert_eq!(
5497            selection!("$($.nullField ?! 'fallback')", spec).apply_to(&data),
5498            (Some(json!(null)), vec![]),
5499        );
5500    }
5501
5502    #[test]
5503    fn none_coalescing_should_replace_missing_field() {
5504        let spec = ConnectSpec::V0_3;
5505        let data = json!({"nullField": null});
5506        assert_eq!(
5507            selection!("$($.missingField ?! 'fallback')", spec).apply_to(&data),
5508            (Some(json!("fallback")), vec![]),
5509        );
5510    }
5511
5512    #[test]
5513    fn null_coalescing_should_replace_null_field() {
5514        let spec = ConnectSpec::V0_3;
5515        let data = json!({"nullField": null});
5516        assert_eq!(
5517            selection!("$($.nullField ?? 'fallback')", spec).apply_to(&data),
5518            (Some(json!("fallback")), vec![]),
5519        );
5520    }
5521
5522    #[test]
5523    fn null_coalescing_should_replace_missing_field() {
5524        let spec = ConnectSpec::V0_3;
5525        let data = json!({"nullField": null});
5526        assert_eq!(
5527            selection!("$($.missingField ?? 'fallback')", spec).apply_to(&data),
5528            (Some(json!("fallback")), vec![]),
5529        );
5530    }
5531
5532    #[test]
5533    fn null_coalescing_should_preserve_number_type() {
5534        let spec = ConnectSpec::V0_3;
5535        assert_eq!(
5536            selection!("$(null ?? 42)", spec).apply_to(&json!({})),
5537            (Some(json!(42)), vec![]),
5538        );
5539    }
5540
5541    #[test]
5542    fn null_coalescing_should_preserve_boolean_type() {
5543        let spec = ConnectSpec::V0_3;
5544        assert_eq!(
5545            selection!("$(null ?? true)", spec).apply_to(&json!({})),
5546            (Some(json!(true)), vec![]),
5547        );
5548    }
5549
5550    #[test]
5551    fn null_coalescing_should_preserve_object_type() {
5552        let spec = ConnectSpec::V0_3;
5553        assert_eq!(
5554            selection!("$(null ?? {'key': 'value'})", spec).apply_to(&json!({})),
5555            (Some(json!({"key": "value"})), vec![]),
5556        );
5557    }
5558
5559    #[test]
5560    fn null_coalescing_should_preserve_array_type() {
5561        let spec = ConnectSpec::V0_3;
5562        assert_eq!(
5563            selection!("$(null ?? [1, 2, 3])", spec).apply_to(&json!({})),
5564            (Some(json!([1, 2, 3])), vec![]),
5565        );
5566    }
5567
5568    #[test]
5569    fn null_coalescing_should_fallback_when_null_used_as_method_arg() {
5570        let spec = ConnectSpec::V0_3;
5571        assert_eq!(
5572            selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": null, "c": 5})),
5573            (Some(json!(10)), vec![]),
5574        );
5575    }
5576
5577    #[test]
5578    fn null_coalescing_should_fallback_when_none_used_as_method_arg() {
5579        let spec = ConnectSpec::V0_3;
5580        assert_eq!(
5581            selection!("$.a->add(missing ?? c)", spec)
5582                .apply_to(&json!({"a": 5, "b": null, "c": 5})),
5583            (Some(json!(10)), vec![]),
5584        );
5585    }
5586
5587    #[test]
5588    fn null_coalescing_should_not_fallback_when_not_null_used_as_method_arg() {
5589        let spec = ConnectSpec::V0_3;
5590        assert_eq!(
5591            selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": 3, "c": 5})),
5592            (Some(json!(8)), vec![]),
5593        );
5594    }
5595
5596    #[test]
5597    fn null_coalescing_should_allow_multiple_method_args() {
5598        let spec = ConnectSpec::V0_3;
5599        let add_selection = selection!("a->add(b ?? c, missing ?! c)", spec);
5600        assert_eq!(
5601            add_selection.apply_to(&json!({ "a": 5, "b": 3, "c": 7 })),
5602            (Some(json!(15)), vec![]),
5603        );
5604        assert_eq!(
5605            add_selection.apply_to(&json!({ "a": 5, "b": null, "c": 7 })),
5606            (Some(json!(19)), vec![]),
5607        );
5608    }
5609
5610    // TODO Reenable this test in ConnectSpec::V0_4 when we support ... spread
5611    // syntax and abstract types.
5612    /*
5613    #[test]
5614    fn none_coalescing_should_allow_defaulting_match() {
5615        let spec = ConnectSpec::V0_4;
5616
5617        assert_eq!(
5618            selection!("a ...b->match(['match', { b: 'world' }])", spec)
5619                .apply_to(&json!({ "a": "hello", "b": "match" })),
5620            (Some(json!({ "a": "hello", "b": "world" })), vec![]),
5621        );
5622
5623        assert_eq!(
5624            selection!("a ...$(b->match(['match', { b: 'world' }]) ?? {})", spec)
5625                .apply_to(&json!({ "a": "hello", "b": "match" })),
5626            (Some(json!({ "a": "hello", "b": "world" })), vec![]),
5627        );
5628
5629        assert_eq!(
5630            selection!("a ...$(b->match(['match', { b: 'world' }]) ?? {})", spec)
5631                .apply_to(&json!({ "a": "hello", "b": "bogus" })),
5632            (Some(json!({ "a": "hello" })), vec![]),
5633        );
5634
5635        assert_eq!(
5636            selection!("a ...$(b->match(['match', { b: 'world' }]) ?! null)", spec)
5637                .apply_to(&json!({ "a": "hello", "b": "bogus" })),
5638            (Some(json!(null)), vec![]),
5639        );
5640    }
5641
5642    #[test]
5643    fn nullish_coalescing_chains_should_have_predictable_shape() {
5644        let spec = ConnectSpec::V0_4;
5645
5646        let chain = selection!("$(1 ?? true ?? null)", spec);
5647        assert_eq!(chain.shape().pretty_print(), "One<1, true, null>",);
5648
5649        let complex_chain = selection!(
5650            r#"
5651            ... $(
5652                message?->echo({ __typename: "Good", message: @ }) ??
5653                error?->echo({ __typename: "Bad", error: @ }) ??
5654                null
5655            )
5656        "#,
5657            spec
5658        );
5659        assert_eq!(
5660            complex_chain.shape().pretty_print(),
5661            "One<{ __typename: \"Good\", message: $root.*.message }, { __typename: \"Bad\", error: $root.*.error }, null>",
5662        );
5663
5664        let complex_chain_no_fallback = selection!(
5665            r#"
5666            ... $(
5667                message?->echo({ __typename: "Good", message: @ }) ??
5668                error?->echo({ __typename: "Bad", error: @ })
5669            )
5670        "#,
5671            spec
5672        );
5673        assert_eq!(
5674            complex_chain_no_fallback.shape().pretty_print(),
5675            // None should not be an option here, even though both message and
5676            // error might not exist, because the ... spread operator spreads
5677            // nothing in that case.
5678            // "One<{ __typename: \"Good\", message: $root.*.message }, { __typename: \"Bad\", error: $root.*.error }, None>",
5679            "One<{ __typename: \"Good\", message: $root.*.message }, { __typename: \"Bad\", error: $root.*.error }, {}>",
5680        );
5681        assert_eq!(
5682            complex_chain_no_fallback.apply_to(&json!({})),
5683            (Some(json!({})), vec![
5684                ApplyToError::new(
5685                    "Inlined path produced no value".to_string(),
5686                    vec![],
5687                    Some(17..165),
5688                    spec,
5689                ),
5690            ]),
5691        );
5692    }
5693    */
5694
5695    #[test]
5696    fn wtf_operator_should_not_exclude_null_from_nullable_union_shape() {
5697        let spec = ConnectSpec::V0_3;
5698
5699        // We're also testing the ?? operator here, to show the difference.
5700        let nullish_selection = selection!("$($value ?? 'fallback')", spec);
5701        let wtf_selection = selection!("$($value ?! 'fallback')", spec);
5702
5703        let mut vars = IndexMap::default();
5704        vars.insert("$value".to_string(), json!(null));
5705
5706        assert_eq!(
5707            nullish_selection.apply_with_vars(&json!({}), &vars),
5708            (Some(json!("fallback")), vec![]),
5709        );
5710
5711        assert_eq!(
5712            wtf_selection.apply_with_vars(&json!({}), &vars),
5713            (Some(json!(null)), vec![]),
5714        );
5715
5716        let mut vars_with_string_value = IndexMap::default();
5717        vars_with_string_value.insert("$value".to_string(), json!("fine"));
5718
5719        assert_eq!(
5720            nullish_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5721            (Some(json!("fine")), vec![]),
5722        );
5723
5724        assert_eq!(
5725            wtf_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5726            (Some(json!("fine")), vec![]),
5727        );
5728
5729        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5730            .with_spec(spec)
5731            .with_named_shapes([(
5732                "$value".to_string(),
5733                Shape::one([Shape::string([]), Shape::null([]), Shape::none()], []),
5734            )]);
5735
5736        assert_eq!(
5737            nullish_selection
5738                // Since we're not using the input shape $root here, it should
5739                // not be a problem to pass Shape::none() as $root.
5740                .compute_output_shape(&shape_context, Shape::none())
5741                .pretty_print(),
5742            "String",
5743        );
5744
5745        assert_eq!(
5746            wtf_selection
5747                // Since we're not using the input shape $root here, it should
5748                // not be a problem to pass Shape::none() as $root.
5749                .compute_output_shape(&shape_context, Shape::none())
5750                .pretty_print(),
5751            "One<String, null>",
5752        );
5753    }
5754
5755    #[test]
5756    fn question_operator_should_map_null_to_none() {
5757        let spec = ConnectSpec::V0_3;
5758
5759        let nullish_string_selection = selection!("$(stringOrNull?)", spec);
5760        assert_eq!(
5761            nullish_string_selection.apply_to(&json!({"stringOrNull": "a string"})),
5762            (Some(json!("a string")), vec![]),
5763        );
5764        assert_eq!(
5765            nullish_string_selection.apply_to(&json!({"stringOrNull": null})),
5766            (None, vec![]),
5767        );
5768        assert_eq!(
5769            nullish_string_selection.apply_to(&json!({})),
5770            (None, vec![]),
5771        );
5772
5773        let shape_context = {
5774            let mut named_shapes = IndexMap::default();
5775
5776            named_shapes.insert(
5777                "$root".to_string(),
5778                Shape::record(
5779                    {
5780                        let mut map = Shape::empty_map();
5781                        map.insert(
5782                            "stringOrNull".to_string(),
5783                            Shape::one([Shape::string([]), Shape::null([])], []),
5784                        );
5785                        map
5786                    },
5787                    [],
5788                ),
5789            );
5790
5791            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5792                .with_spec(spec)
5793                .with_named_shapes(named_shapes)
5794        };
5795
5796        assert_eq!(
5797            nullish_string_selection
5798                .compute_output_shape(
5799                    &shape_context,
5800                    shape_context.named_shapes()["$root"].clone()
5801                )
5802                .pretty_print(),
5803            "One<String, None>",
5804        );
5805    }
5806
5807    #[test]
5808    fn question_operator_should_add_none_to_named_shapes() {
5809        let spec = ConnectSpec::V0_3;
5810
5811        let string_or_null_expr = selection!("$(stringOrNull?)", spec);
5812
5813        assert_eq!(
5814            string_or_null_expr.shape().pretty_print(),
5815            "$root.stringOrNull?",
5816        );
5817    }
5818
5819    #[test]
5820    fn question_operator_with_nested_objects() {
5821        let spec = ConnectSpec::V0_3;
5822
5823        let nested_selection = selection!("$(user?.profile?.name)", spec);
5824        assert_eq!(
5825            nested_selection.apply_to(&json!({"user": {"profile": {"name": "Alice"}}})),
5826            (Some(json!("Alice")), vec![]),
5827        );
5828        assert_eq!(
5829            nested_selection.apply_to(&json!({"user": null})),
5830            (None, vec![]),
5831        );
5832        assert_eq!(
5833            nested_selection.apply_to(&json!({"user": {"profile": null}})),
5834            (None, vec![]),
5835        );
5836        assert_eq!(nested_selection.apply_to(&json!({})), (None, vec![]));
5837    }
5838
5839    #[test]
5840    fn question_operator_with_nested_objects_shape() {
5841        let spec = ConnectSpec::V0_3;
5842
5843        let shape_context = {
5844            let mut named_shapes = IndexMap::default();
5845
5846            let person_shape = Shape::record(
5847                {
5848                    let mut map = Shape::empty_map();
5849                    map.insert("name".to_string(), Shape::string([]));
5850                    map.insert("age".to_string(), Shape::int([]));
5851                    map
5852                },
5853                [],
5854            );
5855
5856            named_shapes.insert(
5857                "$root".to_string(),
5858                Shape::record(
5859                    {
5860                        let mut map = Shape::empty_map();
5861                        map.insert("person".to_string(), person_shape);
5862                        map
5863                    },
5864                    [],
5865                ),
5866            );
5867
5868            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5869                .with_spec(spec)
5870                .with_named_shapes(named_shapes)
5871        };
5872
5873        let nested_selection = selection!("$(person?.name)", spec);
5874        assert_eq!(
5875            nested_selection
5876                .compute_output_shape(
5877                    &shape_context,
5878                    shape_context.named_shapes()["$root"].clone()
5879                )
5880                .pretty_print(),
5881            "One<String, None>",
5882        );
5883    }
5884
5885    #[test]
5886    fn question_operator_with_array_access() {
5887        let spec = ConnectSpec::V0_3;
5888
5889        let array_selection = selection!("$(items?->first?.name)", spec);
5890        assert_eq!(
5891            array_selection.apply_to(&json!({"items": [{"name": "first"}]})),
5892            (Some(json!("first")), vec![]),
5893        );
5894        assert_eq!(
5895            array_selection.apply_to(&json!({"items": []})),
5896            (None, vec![]),
5897        );
5898        assert_eq!(
5899            array_selection.apply_to(&json!({"items": null})),
5900            (None, vec![]),
5901        );
5902        assert_eq!(array_selection.apply_to(&json!({})), (None, vec![]));
5903    }
5904
5905    #[test]
5906    fn question_operator_with_union_shapes() {
5907        let spec = ConnectSpec::V0_3;
5908
5909        let shape_context = {
5910            let mut named_shapes = IndexMap::default();
5911
5912            named_shapes.insert(
5913                "$root".to_string(),
5914                Shape::record(
5915                    {
5916                        let mut map = Shape::empty_map();
5917                        map.insert(
5918                            "unionField".to_string(),
5919                            Shape::one([Shape::string([]), Shape::int([]), Shape::null([])], []),
5920                        );
5921                        map
5922                    },
5923                    [],
5924                ),
5925            );
5926
5927            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5928                .with_spec(spec)
5929                .with_named_shapes(named_shapes)
5930        };
5931
5932        let union_selection = selection!("$(unionField?)", spec);
5933
5934        assert_eq!(
5935            union_selection
5936                .compute_output_shape(
5937                    &shape_context,
5938                    shape_context.named_shapes().get("$root").unwrap().clone(),
5939                )
5940                .pretty_print(),
5941            "One<String, Int, None>",
5942        );
5943    }
5944
5945    #[test]
5946    fn question_operator_with_union_shapes_non_nullable() {
5947        let spec = ConnectSpec::V0_3;
5948
5949        let shape_context = {
5950            let mut named_shapes = IndexMap::default();
5951
5952            named_shapes.insert(
5953                "$root".to_string(),
5954                Shape::record(
5955                    {
5956                        let mut map = Shape::empty_map();
5957                        map.insert(
5958                            "value".to_string(),
5959                            Shape::one([Shape::string([]), Shape::int([])], []),
5960                        );
5961                        map
5962                    },
5963                    [],
5964                ),
5965            );
5966
5967            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5968                .with_spec(spec)
5969                .with_named_shapes(named_shapes)
5970        };
5971
5972        let union_selection = selection!("$(value?)", spec);
5973        assert_eq!(
5974            union_selection
5975                .compute_output_shape(
5976                    &shape_context,
5977                    shape_context.named_shapes()["$root"].clone()
5978                )
5979                .pretty_print(),
5980            "One<String, Int>",
5981        );
5982    }
5983
5984    #[test]
5985    fn question_operator_with_error_shapes() {
5986        let spec = ConnectSpec::V0_3;
5987
5988        let shape_context = {
5989            let mut named_shapes = IndexMap::default();
5990
5991            named_shapes.insert(
5992                "$root".to_string(),
5993                Shape::record(
5994                    {
5995                        let mut map = Shape::empty_map();
5996                        map.insert(
5997                            "errorField".to_string(),
5998                            Shape::error_with_partial(
5999                                "Test error".to_string(),
6000                                Shape::one([Shape::string([]), Shape::null([])], []),
6001                                [],
6002                            ),
6003                        );
6004                        map
6005                    },
6006                    [],
6007                ),
6008            );
6009
6010            ShapeContext::new(SourceId::Other("JSONSelection".into()))
6011                .with_spec(spec)
6012                .with_named_shapes(named_shapes)
6013        };
6014
6015        let error_selection = selection!("$(errorField?)", spec);
6016
6017        let result_shape = error_selection.compute_output_shape(
6018            &shape_context,
6019            shape_context.named_shapes().get("$root").unwrap().clone(),
6020        );
6021
6022        // The question mark should be applied recursively to the partial shape within the error
6023        assert!(result_shape.pretty_print().contains("Error"));
6024        assert!(result_shape.pretty_print().contains("None"));
6025    }
6026
6027    #[test]
6028    fn question_operator_with_simple_error_shapes() {
6029        let spec = ConnectSpec::V0_3;
6030
6031        let shape_context = {
6032            let mut named_shapes = IndexMap::default();
6033
6034            named_shapes.insert(
6035                "$root".to_string(),
6036                Shape::record(
6037                    {
6038                        let mut map = Shape::empty_map();
6039                        map.insert(
6040                            "error".to_string(),
6041                            Shape::error("Something went wrong".to_string(), []),
6042                        );
6043                        map
6044                    },
6045                    [],
6046                ),
6047            );
6048
6049            ShapeContext::new(SourceId::Other("JSONSelection".into()))
6050                .with_spec(spec)
6051                .with_named_shapes(named_shapes)
6052        };
6053
6054        let error_selection = selection!("$(error?)", spec);
6055        assert_eq!(
6056            error_selection
6057                .compute_output_shape(
6058                    &shape_context,
6059                    shape_context.named_shapes()["$root"].clone()
6060                )
6061                .pretty_print(),
6062            "Error<\"Something went wrong\">",
6063        );
6064    }
6065
6066    #[test]
6067    fn question_operator_with_all_shapes() {
6068        let spec = ConnectSpec::V0_3;
6069
6070        let shape_context = {
6071            let mut named_shapes = IndexMap::default();
6072
6073            named_shapes.insert(
6074                "$root".to_string(),
6075                Shape::record(
6076                    {
6077                        let mut map = Shape::empty_map();
6078                        map.insert(
6079                            "allField".to_string(),
6080                            Shape::all(
6081                                [
6082                                    Shape::string([]),
6083                                    Shape::one([Shape::string([]), Shape::null([])], []),
6084                                ],
6085                                [],
6086                            ),
6087                        );
6088                        map
6089                    },
6090                    [],
6091                ),
6092            );
6093
6094            ShapeContext::new(SourceId::Other("JSONSelection".into()))
6095                .with_spec(spec)
6096                .with_named_shapes(named_shapes)
6097        };
6098
6099        let all_selection = selection!("$(allField?)", spec);
6100
6101        assert_eq!(
6102            all_selection
6103                .compute_output_shape(
6104                    &shape_context,
6105                    shape_context.named_shapes().get("$root").unwrap().clone(),
6106                )
6107                .pretty_print(),
6108            "One<String, None>",
6109        );
6110    }
6111
6112    #[test]
6113    fn question_operator_with_simple_all_shapes() {
6114        let spec = ConnectSpec::V0_3;
6115
6116        let shape_context = {
6117            let mut named_shapes = IndexMap::default();
6118
6119            named_shapes.insert(
6120                "$root".to_string(),
6121                Shape::record(
6122                    {
6123                        let mut map = Shape::empty_map();
6124                        map.insert(
6125                            "intersection".to_string(),
6126                            Shape::all([Shape::string([]), Shape::int([])], []),
6127                        );
6128                        map
6129                    },
6130                    [],
6131                ),
6132            );
6133
6134            ShapeContext::new(SourceId::Other("JSONSelection".into()))
6135                .with_spec(spec)
6136                .with_named_shapes(named_shapes)
6137        };
6138
6139        let all_selection = selection!("$(intersection?)", spec);
6140        assert_eq!(
6141            all_selection
6142                .compute_output_shape(
6143                    &shape_context,
6144                    shape_context.named_shapes()["$root"].clone()
6145                )
6146                .pretty_print(),
6147            "All<String, Int>",
6148        );
6149    }
6150
6151    #[test]
6152    fn question_operator_preserves_non_null_shapes() {
6153        let spec = ConnectSpec::V0_3;
6154
6155        let shape_context = {
6156            let mut named_shapes = IndexMap::default();
6157
6158            named_shapes.insert(
6159                "$root".to_string(),
6160                Shape::record(
6161                    {
6162                        let mut map = Shape::empty_map();
6163                        map.insert("nonNullString".to_string(), Shape::string([]));
6164                        map
6165                    },
6166                    [],
6167                ),
6168            );
6169
6170            ShapeContext::new(SourceId::Other("JSONSelection".into()))
6171                .with_spec(spec)
6172                .with_named_shapes(named_shapes)
6173        };
6174
6175        let non_null_selection = selection!("$(nonNullString?)", spec);
6176
6177        assert_eq!(
6178            non_null_selection
6179                .compute_output_shape(
6180                    &shape_context,
6181                    shape_context.named_shapes().get("$root").unwrap().clone(),
6182                )
6183                .pretty_print(),
6184            "String",
6185        );
6186    }
6187
6188    #[test]
6189    fn question_operator_with_multiple_operators_in_chain() {
6190        let spec = ConnectSpec::V0_3;
6191
6192        // Test combining ? with other operators
6193        let mixed_chain_selection = selection!("$(field? ?? 'default')", spec);
6194        assert_eq!(
6195            mixed_chain_selection.apply_to(&json!({"field": "value"})),
6196            (Some(json!("value")), vec![]),
6197        );
6198        assert_eq!(
6199            mixed_chain_selection.apply_to(&json!({"field": null})),
6200            (Some(json!("default")), vec![]),
6201        );
6202        assert_eq!(
6203            mixed_chain_selection.apply_to(&json!({})),
6204            (Some(json!("default")), vec![]),
6205        );
6206    }
6207
6208    #[test]
6209    fn question_operator_chained_shape() {
6210        let spec = ConnectSpec::V0_3;
6211
6212        let shape_context = {
6213            let mut named_shapes = IndexMap::default();
6214
6215            named_shapes.insert(
6216                "$root".to_string(),
6217                Shape::record(
6218                    {
6219                        let mut map = Shape::empty_map();
6220                        map.insert(
6221                            "level1".to_string(),
6222                            Shape::record(
6223                                {
6224                                    let mut inner_map = Shape::empty_map();
6225                                    inner_map.insert(
6226                                        "level2".to_string(),
6227                                        Shape::record(
6228                                            {
6229                                                let mut inner_inner_map = Shape::empty_map();
6230                                                inner_inner_map
6231                                                    .insert("value".to_string(), Shape::string([]));
6232                                                inner_inner_map
6233                                            },
6234                                            [],
6235                                        ),
6236                                    );
6237                                    inner_map
6238                                },
6239                                [],
6240                            ),
6241                        );
6242                        map
6243                    },
6244                    [],
6245                ),
6246            );
6247
6248            ShapeContext::new(SourceId::Other("JSONSelection".into()))
6249                .with_spec(spec)
6250                .with_named_shapes(named_shapes)
6251        };
6252
6253        let chained_selection = selection!("$(level1?.level2?.value)", spec);
6254        assert_eq!(
6255            chained_selection
6256                .compute_output_shape(
6257                    &shape_context,
6258                    shape_context.named_shapes()["$root"].clone()
6259                )
6260                .pretty_print(),
6261            "One<String, None>",
6262        );
6263    }
6264
6265    #[test]
6266    fn question_operator_direct_null_input_shape() {
6267        let spec = ConnectSpec::V0_3;
6268
6269        let shape_context = {
6270            let mut named_shapes = IndexMap::default();
6271
6272            named_shapes.insert("$root".to_string(), Shape::null([]));
6273
6274            ShapeContext::new(SourceId::Other("JSONSelection".into()))
6275                .with_spec(spec)
6276                .with_named_shapes(named_shapes)
6277        };
6278
6279        let null_selection = selection!("$root?", spec);
6280
6281        assert_eq!(
6282            null_selection
6283                .compute_output_shape(
6284                    &shape_context,
6285                    shape_context.named_shapes().get("$root").unwrap().clone(),
6286                )
6287                .pretty_print(),
6288            "None",
6289        );
6290    }
6291
6292    #[test]
6293    fn test_unknown_name() {
6294        let spec = ConnectSpec::V0_3;
6295        let sel = selection!("book.author? { name age? }", spec);
6296        assert_eq!(
6297            sel.shape().pretty_print(),
6298            r#"One<
6299  {
6300    age: $root.book.author?.*.age?,
6301    name: $root.book.author?.*.name,
6302  },
6303  None,
6304>"#,
6305        );
6306    }
6307
6308    #[test]
6309    fn test_nullish_coalescing_shape() {
6310        let spec = ConnectSpec::V0_3;
6311        let sel = selection!("$(a ?? b ?? c)", spec);
6312        assert_eq!(
6313            sel.shape().pretty_print(),
6314            "One<$root.a?!, $root.b?!, $root.c>",
6315        );
6316
6317        let mut named_shapes = IndexMap::default();
6318        named_shapes.insert(
6319            "$root".to_string(),
6320            Shape::record(
6321                {
6322                    let mut map = Shape::empty_map();
6323                    map.insert(
6324                        "a".to_string(),
6325                        Shape::one([Shape::string([]), Shape::null([])], []),
6326                    );
6327                    map.insert("b".to_string(), Shape::string([]));
6328                    map.insert("c".to_string(), Shape::int([]));
6329                    map
6330                },
6331                [],
6332            ),
6333        );
6334
6335        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
6336            .with_spec(spec)
6337            .with_named_shapes(named_shapes);
6338
6339        assert_eq!(
6340            sel.compute_output_shape(
6341                &shape_context,
6342                shape_context.named_shapes().get("$root").unwrap().clone(),
6343            )
6344            .pretty_print(),
6345            "One<String, Int>",
6346        );
6347    }
6348
6349    #[test]
6350    fn test_none_coalescing_shape() {
6351        let spec = ConnectSpec::V0_3;
6352        let sel = selection!("$(a ?! b ?! c)", spec);
6353        assert_eq!(
6354            sel.shape().pretty_print(),
6355            "One<$root.a!, $root.b!, $root.c>",
6356        );
6357
6358        let mut named_shapes = IndexMap::default();
6359        named_shapes.insert(
6360            "$root".to_string(),
6361            Shape::record(
6362                {
6363                    let mut map = Shape::empty_map();
6364                    map.insert(
6365                        "a".to_string(),
6366                        Shape::one([Shape::string([]), Shape::null([])], []),
6367                    );
6368                    map.insert(
6369                        "b".to_string(),
6370                        Shape::one([Shape::string([]), Shape::none()], []),
6371                    );
6372                    map.insert("c".to_string(), Shape::null([]));
6373                    map
6374                },
6375                [],
6376            ),
6377        );
6378
6379        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
6380            .with_spec(spec)
6381            .with_named_shapes(named_shapes);
6382
6383        assert_eq!(
6384            sel.compute_output_shape(
6385                &shape_context,
6386                shape_context.named_shapes().get("$root").unwrap().clone(),
6387            )
6388            .pretty_print(),
6389            "One<String, null>",
6390        );
6391    }
6392
6393    #[test]
6394    fn test_none_coalescing_with_literal_fallback() {
6395        let spec = ConnectSpec::V0_3;
6396
6397        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
6398            .with_spec(spec)
6399            .with_named_shapes([(
6400                "$root".to_string(),
6401                Shape::record(
6402                    {
6403                        let mut map = Shape::empty_map();
6404                        map.insert(
6405                            "optional".to_string(),
6406                            Shape::one([Shape::string([]), Shape::none()], []),
6407                        );
6408                        map
6409                    },
6410                    [],
6411                ),
6412            )]);
6413
6414        let none_coalesce = selection!("$(optional ?! 'fallback')", spec);
6415        assert_eq!(
6416            none_coalesce
6417                .compute_output_shape(
6418                    &shape_context,
6419                    shape_context.named_shapes()["$root"].clone()
6420                )
6421                .pretty_print(),
6422            "String",
6423        );
6424    }
6425
6426    /// Test that multiple `... ->match()` spreads produce correct shape structure.
6427    ///
6428    /// When both spreads set __typename, the cartesian product creates
6429    /// intersections like All<"Book", "Digital"> which are unsatisfiable.
6430    /// This test documents the shape structure before filtering.
6431    #[test]
6432    fn test_multiple_spreads_shape_with_conflicting_typename() {
6433        let spec = ConnectSpec::V0_4;
6434
6435        // Two spreads both setting __typename - creates conflicts
6436        let selection = selection!(
6437            r#"
6438            upc
6439            ... category->match(
6440                ['book', { __typename: $('Book'), title: $.title }],
6441                ['film', { __typename: $('Film'), director: $.director }],
6442                [@, null]
6443            )
6444            ... format->match(
6445                ['digital', { __typename: $('Digital'), downloadUrl: $.url }],
6446                ['physical', { __typename: $('Physical'), weight: $.weight }],
6447                [@, null]
6448            )
6449            "#,
6450            spec
6451        );
6452
6453        // Cartesian product of spreads creates unsatisfiable All<> intersections
6454        assert_eq!(
6455            selection.shape().pretty_print(),
6456            r#"One<
6457  {
6458    __typename: All<"Book", "Digital">,
6459    downloadUrl: $root.*.url,
6460    title: $root.*.title,
6461    upc: $root.*.upc,
6462  },
6463  {
6464    __typename: All<"Book", "Physical">,
6465    title: $root.*.title,
6466    upc: $root.*.upc,
6467    weight: $root.*.weight,
6468  },
6469  null,
6470  {
6471    __typename: All<"Film", "Digital">,
6472    director: $root.*.director,
6473    downloadUrl: $root.*.url,
6474    upc: $root.*.upc,
6475  },
6476  {
6477    __typename: All<"Film", "Physical">,
6478    director: $root.*.director,
6479    upc: $root.*.upc,
6480    weight: $root.*.weight,
6481  },
6482>"#,
6483        );
6484    }
6485
6486    /// Test multiple spreads where only the first sets __typename (valid pattern).
6487    ///
6488    /// This is the correct pattern: first spread determines __typename,
6489    /// second spread adds fields without conflicting.
6490    #[test]
6491    fn test_multiple_spreads_shape_without_typename_conflict() {
6492        let spec = ConnectSpec::V0_4;
6493
6494        // First spread sets __typename, second adds fields without __typename
6495        let selection = selection!(
6496            r#"
6497            upc
6498            ... category->match(
6499                ['book', { __typename: $('Book'), title: $.title }],
6500                ['film', { __typename: $('Film'), director: $.director }],
6501                [@, null]
6502            )
6503            ... format->match(
6504                ['digital', { downloadUrl: $.url }],
6505                ['physical', { weight: $.weight }],
6506                [@, null]
6507            )
6508            "#,
6509            spec
6510        );
6511
6512        // No conflict: second spread adds fields but doesn't override __typename
6513        assert_eq!(
6514            selection.shape().pretty_print(),
6515            r#"One<
6516  {
6517    __typename: "Book",
6518    downloadUrl: $root.*.url,
6519    title: $root.*.title,
6520    upc: $root.*.upc,
6521  },
6522  {
6523    __typename: "Book",
6524    title: $root.*.title,
6525    upc: $root.*.upc,
6526    weight: $root.*.weight,
6527  },
6528  null,
6529  {
6530    __typename: "Film",
6531    director: $root.*.director,
6532    downloadUrl: $root.*.url,
6533    upc: $root.*.upc,
6534  },
6535  {
6536    __typename: "Film",
6537    director: $root.*.director,
6538    upc: $root.*.upc,
6539    weight: $root.*.weight,
6540  },
6541>"#,
6542        );
6543    }
6544
6545    /// Test single spread with ->match() to verify baseline behavior.
6546    #[test]
6547    fn test_single_spread_shape() {
6548        let spec = ConnectSpec::V0_4;
6549
6550        let selection = selection!(
6551            r#"
6552            upc
6553            ... category->match(
6554                ['book', { __typename: $('Book'), title: $.title }],
6555                ['film', { __typename: $('Film'), director: $.director }],
6556                [@, null]
6557            )
6558            "#,
6559            spec
6560        );
6561
6562        // Single spread produces clean One<> with distinct __typename values
6563        assert_eq!(
6564            selection.shape().pretty_print(),
6565            r#"One<
6566  {
6567    __typename: "Book",
6568    title: $root.*.title,
6569    upc: $root.*.upc,
6570  },
6571  {
6572    __typename: "Film",
6573    director: $root.*.director,
6574    upc: $root.*.upc,
6575  },
6576  null,
6577>"#,
6578        );
6579    }
6580
6581    /// Test multiple spreads where both specify the same __typename.
6582    ///
6583    /// When both spreads set __typename to the same value (e.g., "Book"),
6584    /// the second spread matches category->match independently.
6585    #[test]
6586    fn test_multiple_spreads_same_typename_no_conflict() {
6587        let spec = ConnectSpec::V0_4;
6588
6589        // Both spreads match 'book' and set __typename: "Book"
6590        let selection = selection!(
6591            r#"
6592            upc
6593            ... category->match(
6594                ['book', { __typename: $('Book'), title: $.title }],
6595                ['film', { __typename: $('Film'), director: $.director }],
6596                [@, null]
6597            )
6598            ... category->match(
6599                ['book', { __typename: $('Book'), author: $.author }]
6600            )
6601            "#,
6602            spec
6603        );
6604
6605        // Cartesian product: each first spread case × each second spread case
6606        // - Book (first) + Book (second) = Book with both fields
6607        // - Book (first) + no match (second) = Book without author
6608        // - Film (first) + Book (second) = conflicting All<"Film", "Book">
6609        // - Film (first) + no match (second) = Film unchanged
6610        assert_eq!(
6611            selection.shape().pretty_print(),
6612            r#"One<
6613  {
6614    __typename: "Book",
6615    author: $root.*.author,
6616    title: $root.*.title,
6617    upc: $root.*.upc,
6618  },
6619  {
6620    __typename: "Book",
6621    title: $root.*.title,
6622    upc: $root.*.upc,
6623  },
6624  {
6625    __typename: All<"Film", "Book">,
6626    author: $root.*.author,
6627    director: $root.*.director,
6628    upc: $root.*.upc,
6629  },
6630  {
6631    __typename: "Film",
6632    director: $root.*.director,
6633    upc: $root.*.upc,
6634  },
6635  null,
6636>"#,
6637        );
6638    }
6639
6640    /// Test field shadowing when second spread overwrites a field from first.
6641    #[test]
6642    fn test_multiple_spreads_field_shadowing() {
6643        let spec = ConnectSpec::V0_4;
6644
6645        // First spread sets greeting: "hello", second sets greeting: "goodbye"
6646        let selection = selection!(
6647            r#"
6648            upc
6649            ... category->match(
6650                ['book', { __typename: $('Book'), greeting: $('hello') }],
6651                [@, null]
6652            )
6653            ... category->match(
6654                ['book', { __typename: $('Book'), greeting: $('goodbye') }]
6655            )
6656            "#,
6657            spec
6658        );
6659
6660        // Two Book shapes depending on whether second spread matched:
6661        // - Both matched: greeting is All<"hello", "goodbye">
6662        // - Only first matched: greeting is just "hello"
6663        assert_eq!(
6664            selection.shape().pretty_print(),
6665            r#"One<
6666  {
6667    __typename: "Book",
6668    greeting: All<"hello", "goodbye">,
6669    upc: $root.*.upc,
6670  },
6671  { __typename: "Book", greeting: "hello", upc: $root.*.upc },
6672  null,
6673>"#,
6674        );
6675    }
6676
6677    /// Test that second spread without __typename adds to all shapes.
6678    #[test]
6679    fn test_second_spread_no_typename_adds_to_all() {
6680        let spec = ConnectSpec::V0_4;
6681
6682        // First spread sets __typename, second spread has no __typename
6683        // so its fields merge with ALL shapes from first spread
6684        let selection = selection!(
6685            r#"
6686            upc
6687            ... category->match(
6688                ['book', { __typename: $('Book') }],
6689                ['film', { __typename: $('Film') }],
6690                [@, null]
6691            )
6692            ... format->match(
6693                ['digital', { isDigital: $(true) }],
6694                [@, null]
6695            )
6696            "#,
6697            spec
6698        );
6699
6700        // isDigital field appears on both Book and Film shapes
6701        assert_eq!(
6702            selection.shape().pretty_print(),
6703            r#"One<
6704  { __typename: "Book", isDigital: true, upc: $root.*.upc },
6705  null,
6706  { __typename: "Film", isDigital: true, upc: $root.*.upc },
6707>"#,
6708        );
6709    }
6710
6711    /// Test cartesian product of spreads with different match conditions.
6712    ///
6713    /// Shape analysis is conservative: it creates all combinations of spread
6714    /// results, even when runtime conditions would prevent some combinations.
6715    /// Here, the second spread only matches 'book', but shape analysis still
6716    /// creates Film × extraField combinations since it doesn't track that
6717    /// category='film' would never match ['book', ...].
6718    #[test]
6719    fn test_spread_cartesian_product_conservative() {
6720        let spec = ConnectSpec::V0_4;
6721
6722        // First spread handles all categories, second only handles 'book'
6723        let selection = selection!(
6724            r#"
6725            upc
6726            ... category->match(
6727                ['book', { __typename: $('Book'), title: $.title }],
6728                ['film', { __typename: $('Film'), director: $.director }],
6729                [@, null]
6730            )
6731            ... category->match(
6732                ['book', { extraField: $('only for books') }]
6733            )
6734            "#,
6735            spec
6736        );
6737
6738        // Shape analysis conservatively creates ALL combinations:
6739        // - Book × extraField-matched = Book with extraField
6740        // - Book × extraField-unmatched = Book without extraField
6741        // - Film × extraField-matched = Film with extraField (even though runtime won't produce this)
6742        // - Film × extraField-unmatched = Film without extraField
6743        assert_eq!(
6744            selection.shape().pretty_print(),
6745            r#"One<
6746  {
6747    __typename: "Book",
6748    extraField: "only for books",
6749    title: $root.*.title,
6750    upc: $root.*.upc,
6751  },
6752  {
6753    __typename: "Book",
6754    title: $root.*.title,
6755    upc: $root.*.upc,
6756  },
6757  {
6758    __typename: "Film",
6759    director: $root.*.director,
6760    extraField: "only for books",
6761    upc: $root.*.upc,
6762  },
6763  {
6764    __typename: "Film",
6765    director: $root.*.director,
6766    upc: $root.*.upc,
6767  },
6768  null,
6769>"#,
6770        );
6771    }
6772
6773    // ==========================================
6774    // Runtime tests for multiple spread behavior
6775    // ==========================================
6776
6777    /// Runtime test: Both spreads match 'book', fields are merged.
6778    #[test]
6779    fn test_multiple_spreads_runtime_both_match_book() {
6780        let spec = ConnectSpec::V0_4;
6781        let selection = selection!(
6782            r#"
6783            upc
6784            ... category->match(
6785                ['book', { __typename: $('Book'), title: $.title }],
6786                ['film', { __typename: $('Film'), director: $.director }],
6787                [@, null]
6788            )
6789            ... category->match(
6790                ['book', { __typename: $('Book'), author: $.author }]
6791            )
6792            "#,
6793            spec
6794        );
6795
6796        // Book: both spreads match, fields merge
6797        let book_data = json!({
6798            "upc": "123",
6799            "category": "book",
6800            "title": "Great Gatsby",
6801            "author": "Fitzgerald"
6802        });
6803        let (result, errors) = selection.apply_to(&book_data);
6804        assert_eq!(errors, vec![]);
6805        assert_eq!(
6806            result,
6807            Some(json!({
6808                "upc": "123",
6809                "__typename": "Book",
6810                "title": "Great Gatsby",
6811                "author": "Fitzgerald"
6812            }))
6813        );
6814    }
6815
6816    /// Runtime test: First spread matches 'film', second doesn't match.
6817    #[test]
6818    fn test_multiple_spreads_runtime_film_no_second_match() {
6819        let spec = ConnectSpec::V0_4;
6820        let selection = selection!(
6821            r#"
6822            upc
6823            ... category->match(
6824                ['book', { __typename: $('Book'), title: $.title }],
6825                ['film', { __typename: $('Film'), director: $.director }],
6826                [@, null]
6827            )
6828            ... category->match(
6829                ['book', { __typename: $('Book'), author: $.author }]
6830            )
6831            "#,
6832            spec
6833        );
6834
6835        // Film: first spread matches, second doesn't (category != 'book')
6836        let film_data = json!({
6837            "upc": "456",
6838            "category": "film",
6839            "director": "Nolan"
6840        });
6841        let (result, errors) = selection.apply_to(&film_data);
6842        // Second spread produces no-match error but result is still valid
6843        assert!(!errors.is_empty());
6844        assert_eq!(
6845            result,
6846            Some(json!({
6847                "upc": "456",
6848                "__typename": "Film",
6849                "director": "Nolan"
6850            }))
6851        );
6852    }
6853
6854    /// Runtime test: Field shadowing - second spread overwrites field.
6855    #[test]
6856    fn test_multiple_spreads_runtime_field_shadowing() {
6857        let spec = ConnectSpec::V0_4;
6858        let selection = selection!(
6859            r#"
6860            upc
6861            ... category->match(
6862                ['book', { __typename: $('Book'), greeting: $('hello') }],
6863                [@, null]
6864            )
6865            ... category->match(
6866                ['book', { __typename: $('Book'), greeting: $('goodbye') }]
6867            )
6868            "#,
6869            spec
6870        );
6871
6872        // Book: both spreads match, second overwrites 'greeting'
6873        let book_data = json!({
6874            "upc": "789",
6875            "category": "book"
6876        });
6877        let (result, errors) = selection.apply_to(&book_data);
6878        assert_eq!(errors, vec![]);
6879        // Second spread's greeting: 'goodbye' shadows first's 'hello'
6880        assert_eq!(
6881            result,
6882            Some(json!({
6883                "upc": "789",
6884                "__typename": "Book",
6885                "greeting": "goodbye"
6886            }))
6887        );
6888    }
6889
6890    /// Runtime test: Second spread without __typename adds to all types.
6891    #[test]
6892    fn test_multiple_spreads_runtime_no_typename_adds_to_all() {
6893        let spec = ConnectSpec::V0_4;
6894        let selection = selection!(
6895            r#"
6896            upc
6897            ... category->match(
6898                ['book', { __typename: $('Book') }],
6899                ['film', { __typename: $('Film') }],
6900                [@, null]
6901            )
6902            ... format->match(
6903                ['digital', { isDigital: $(true) }],
6904                [@, null]
6905            )
6906            "#,
6907            spec
6908        );
6909
6910        // Book + digital: both spreads match
6911        let book_digital = json!({
6912            "upc": "111",
6913            "category": "book",
6914            "format": "digital"
6915        });
6916        let (result, errors) = selection.apply_to(&book_digital);
6917        assert_eq!(errors, vec![]);
6918        assert_eq!(
6919            result,
6920            Some(json!({
6921                "upc": "111",
6922                "__typename": "Book",
6923                "isDigital": true
6924            }))
6925        );
6926
6927        // Film + digital: both spreads match
6928        let film_digital = json!({
6929            "upc": "222",
6930            "category": "film",
6931            "format": "digital"
6932        });
6933        let (result, errors) = selection.apply_to(&film_digital);
6934        assert_eq!(errors, vec![]);
6935        assert_eq!(
6936            result,
6937            Some(json!({
6938                "upc": "222",
6939                "__typename": "Film",
6940                "isDigital": true
6941            }))
6942        );
6943
6944        // Book + physical: first spread matches, second spread hits [@, null] fallback
6945        // which returns null for the whole spread, resulting in null for the output
6946        let book_physical = json!({
6947            "upc": "333",
6948            "category": "book",
6949            "format": "physical"
6950        });
6951        let (result, errors) = selection.apply_to(&book_physical);
6952        assert_eq!(errors, vec![]);
6953        // When second spread returns null via [@, null], the entire result becomes null
6954        assert_eq!(result, Some(json!(null)));
6955    }
6956
6957    /// Runtime test: Category doesn't match any case, returns null.
6958    #[test]
6959    fn test_multiple_spreads_runtime_no_match_returns_null() {
6960        let spec = ConnectSpec::V0_4;
6961        let selection = selection!(
6962            r#"
6963            upc
6964            ... category->match(
6965                ['book', { __typename: $('Book'), title: $.title }],
6966                ['film', { __typename: $('Film'), director: $.director }],
6967                [@, null]
6968            )
6969            "#,
6970            spec
6971        );
6972
6973        // Unknown category: matches [@, null] fallback
6974        let unknown_data = json!({
6975            "upc": "999",
6976            "category": "unknown"
6977        });
6978        let (result, errors) = selection.apply_to(&unknown_data);
6979        assert_eq!(errors, vec![]);
6980        assert_eq!(result, Some(json!(null)));
6981    }
6982}