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