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
28impl JSONSelection {
29    // Applying a selection to a JSON value produces a new JSON value, along
30    // with any/all errors encountered in the process. The value is represented
31    // as an Option to allow for undefined/missing values (which JSON does not
32    // explicitly support), which are distinct from null values (which it does
33    // support).
34    pub fn apply_to(&self, data: &JSON) -> (Option<JSON>, Vec<ApplyToError>) {
35        self.apply_with_vars(data, &IndexMap::default())
36    }
37
38    pub fn apply_with_vars(
39        &self,
40        data: &JSON,
41        vars: &IndexMap<String, JSON>,
42    ) -> (Option<JSON>, Vec<ApplyToError>) {
43        // Using IndexSet over HashSet to preserve the order of the errors.
44        let mut errors = IndexSet::default();
45
46        let mut vars_with_paths: VarsWithPathsMap = IndexMap::default();
47        for (var_name, var_data) in vars {
48            vars_with_paths.insert(
49                KnownVariable::External(var_name.clone()),
50                (var_data, InputPath::empty().append(json!(var_name))),
51            );
52        }
53        // The $ variable initially refers to the root data value, but is
54        // rebound by nested selection sets to refer to the root value the
55        // selection set was applied to.
56        vars_with_paths.insert(KnownVariable::Dollar, (data, InputPath::empty()));
57
58        let spec = self.spec();
59        let (value, apply_errors) =
60            self.apply_to_path(data, &vars_with_paths, &InputPath::empty(), spec);
61
62        // Since errors is an IndexSet, this line effectively deduplicates the
63        // errors, in an attempt to make them less verbose. However, now that we
64        // include both path and range information in the errors, there's an
65        // argument to be made that errors can no longer be meaningfully
66        // deduplicated, so we might consider sticking with a Vec<ApplyToError>.
67        errors.extend(apply_errors);
68
69        (value, errors.into_iter().collect())
70    }
71
72    pub fn shape(&self) -> Shape {
73        let context =
74            ShapeContext::new(SourceId::Other("JSONSelection".into())).with_spec(self.spec());
75
76        self.compute_output_shape(
77            // Relatively static/unchanging inputs to compute_output_shape,
78            // passed down by immutable shared reference.
79            &context,
80            // If we don't know anything about the shape of the input data, we
81            // can represent the data symbolically using the $root variable
82            // shape. Subproperties needed from this shape will show up as
83            // subpaths like $root.books.4.isbn in the output shape.
84            //
85            // While we do not currently have a $root variable available as a
86            // KnownVariable during apply_to_path execution, we might consider
87            // adding it, since it would align with the way we process other
88            // variable shapes. For now, $root exists only as a shape name that
89            // we are inventing right here.
90            Shape::name("$root", Vec::new()),
91        )
92    }
93
94    pub(crate) fn compute_output_shape(&self, context: &ShapeContext, input_shape: Shape) -> Shape {
95        debug_assert_eq!(context.spec(), self.spec());
96
97        let computable: &dyn ApplyToInternal = match &self.inner {
98            TopLevelSelection::Named(selection) => selection,
99            TopLevelSelection::Path(path_selection) => path_selection,
100        };
101
102        let dollar_shape = input_shape.clone();
103
104        if Some(&input_shape) == context.named_shapes().get("$root") {
105            // If the $root variable happens to be bound to the input shape,
106            // context does not need to be cloned or modified.
107            computable.compute_output_shape(context, input_shape, dollar_shape)
108        } else {
109            // Otherwise, we'll want to register the input_shape as $root in a
110            // cloned_context, so $root is reliably defined either way.
111            let cloned_context = context
112                .clone()
113                .with_named_shapes([("$root".to_string(), input_shape.clone())]);
114            computable.compute_output_shape(&cloned_context, input_shape, dollar_shape)
115        }
116    }
117}
118
119pub(super) type VarsWithPathsMap<'a> = IndexMap<KnownVariable, (&'a JSON, InputPath<JSON>)>;
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.as_ref() {
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.iter().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.iter().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.iter().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::none();
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_none() {
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 {
1358            new_fields.push(field(inner_field, key, source_id));
1359        }
1360        return Shape::one(new_fields, shape.locations.iter().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    fn test_apply_to_selection(#[case] spec: ConnectSpec) {
1388        let data = json!({
1389            "hello": "world",
1390            "nested": {
1391                "hello": "world",
1392                "world": "hello",
1393            },
1394            "array": [
1395                { "hello": "world 0" },
1396                { "hello": "world 1" },
1397                { "hello": "world 2" },
1398            ],
1399        });
1400
1401        #[track_caller]
1402        fn check_ok(data: &JSON, selection: JSONSelection, expected_json: JSON) {
1403            let (actual_json, errors) = selection.apply_to(data);
1404            assert_eq!(actual_json, Some(expected_json));
1405            assert_eq!(errors, vec![]);
1406        }
1407
1408        check_ok(&data, selection!("hello", spec), json!({"hello": "world"}));
1409
1410        check_ok(
1411            &data,
1412            selection!("nested", spec),
1413            json!({
1414                "nested": {
1415                    "hello": "world",
1416                    "world": "hello",
1417                },
1418            }),
1419        );
1420
1421        check_ok(&data, selection!("nested.hello", spec), json!("world"));
1422        check_ok(&data, selection!("$.nested.hello", spec), json!("world"));
1423
1424        check_ok(&data, selection!("nested.world", spec), json!("hello"));
1425        check_ok(&data, selection!("$.nested.world", spec), json!("hello"));
1426
1427        check_ok(
1428            &data,
1429            selection!("nested hello", spec),
1430            json!({
1431                "hello": "world",
1432                "nested": {
1433                    "hello": "world",
1434                    "world": "hello",
1435                },
1436            }),
1437        );
1438
1439        check_ok(
1440            &data,
1441            selection!("array { hello }", spec),
1442            json!({
1443                "array": [
1444                    { "hello": "world 0" },
1445                    { "hello": "world 1" },
1446                    { "hello": "world 2" },
1447                ],
1448            }),
1449        );
1450
1451        check_ok(
1452            &data,
1453            selection!("greetings: array { hello }", spec),
1454            json!({
1455                "greetings": [
1456                    { "hello": "world 0" },
1457                    { "hello": "world 1" },
1458                    { "hello": "world 2" },
1459                ],
1460            }),
1461        );
1462
1463        check_ok(
1464            &data,
1465            selection!("$.array { hello }", spec),
1466            json!([
1467                { "hello": "world 0" },
1468                { "hello": "world 1" },
1469                { "hello": "world 2" },
1470            ]),
1471        );
1472
1473        check_ok(
1474            &data,
1475            selection!("worlds: array.hello", spec),
1476            json!({
1477                "worlds": [
1478                    "world 0",
1479                    "world 1",
1480                    "world 2",
1481                ],
1482            }),
1483        );
1484
1485        check_ok(
1486            &data,
1487            selection!("worlds: $.array.hello", spec),
1488            json!({
1489                "worlds": [
1490                    "world 0",
1491                    "world 1",
1492                    "world 2",
1493                ],
1494            }),
1495        );
1496
1497        check_ok(
1498            &data,
1499            selection!("array.hello", spec),
1500            json!(["world 0", "world 1", "world 2",]),
1501        );
1502
1503        check_ok(
1504            &data,
1505            selection!("$.array.hello", spec),
1506            json!(["world 0", "world 1", "world 2",]),
1507        );
1508
1509        check_ok(
1510            &data,
1511            selection!("nested grouped: { hello worlds: array.hello }", spec),
1512            json!({
1513                "nested": {
1514                    "hello": "world",
1515                    "world": "hello",
1516                },
1517                "grouped": {
1518                    "hello": "world",
1519                    "worlds": [
1520                        "world 0",
1521                        "world 1",
1522                        "world 2",
1523                    ],
1524                },
1525            }),
1526        );
1527
1528        check_ok(
1529            &data,
1530            selection!("nested grouped: { hello worlds: $.array.hello }", spec),
1531            json!({
1532                "nested": {
1533                    "hello": "world",
1534                    "world": "hello",
1535                },
1536                "grouped": {
1537                    "hello": "world",
1538                    "worlds": [
1539                        "world 0",
1540                        "world 1",
1541                        "world 2",
1542                    ],
1543                },
1544            }),
1545        );
1546    }
1547
1548    #[rstest]
1549    #[case::v0_2(ConnectSpec::V0_2)]
1550    #[case::v0_3(ConnectSpec::V0_3)]
1551    fn test_apply_to_errors(#[case] spec: ConnectSpec) {
1552        let data = json!({
1553            "hello": "world",
1554            "nested": {
1555                "hello": 123,
1556                "world": true,
1557            },
1558            "array": [
1559                { "hello": 1, "goodbye": "farewell" },
1560                { "hello": "two" },
1561                { "hello": 3.0, "smello": "yellow" },
1562            ],
1563        });
1564
1565        assert_eq!(
1566            selection!("hello", spec).apply_to(&data),
1567            (Some(json!({"hello": "world"})), vec![],)
1568        );
1569
1570        fn make_yellow_errors_expected(
1571            yellow_range: std::ops::Range<usize>,
1572            spec: ConnectSpec,
1573        ) -> Vec<ApplyToError> {
1574            vec![ApplyToError::new(
1575                "Property .yellow not found in object".to_string(),
1576                vec![json!("yellow")],
1577                Some(yellow_range),
1578                spec,
1579            )]
1580        }
1581        assert_eq!(
1582            selection!("yellow", spec).apply_to(&data),
1583            (Some(json!({})), make_yellow_errors_expected(0..6, spec)),
1584        );
1585        assert_eq!(
1586            selection!("$.yellow", spec).apply_to(&data),
1587            (None, make_yellow_errors_expected(2..8, spec)),
1588        );
1589
1590        assert_eq!(
1591            selection!("nested.hello", spec).apply_to(&data),
1592            (Some(json!(123)), vec![],)
1593        );
1594
1595        fn make_quoted_yellow_expected(
1596            yellow_range: std::ops::Range<usize>,
1597            spec: ConnectSpec,
1598        ) -> (Option<JSON>, Vec<ApplyToError>) {
1599            (
1600                None,
1601                vec![ApplyToError::new(
1602                    "Property .\"yellow\" not found in object".to_string(),
1603                    vec![json!("nested"), json!("yellow")],
1604                    Some(yellow_range),
1605                    spec,
1606                )],
1607            )
1608        }
1609        assert_eq!(
1610            selection!("nested.'yellow'", spec).apply_to(&data),
1611            make_quoted_yellow_expected(7..15, spec),
1612        );
1613        assert_eq!(
1614            selection!("nested.\"yellow\"", spec).apply_to(&data),
1615            make_quoted_yellow_expected(7..15, spec),
1616        );
1617        assert_eq!(
1618            selection!("$.nested.'yellow'", spec).apply_to(&data),
1619            make_quoted_yellow_expected(9..17, spec),
1620        );
1621
1622        fn make_nested_path_expected(
1623            hola_range: (usize, usize),
1624            yellow_range: (usize, usize),
1625            spec: ConnectSpec,
1626        ) -> (Option<JSON>, Vec<ApplyToError>) {
1627            (
1628                Some(json!({
1629                    "world": true,
1630                })),
1631                vec![
1632                    ApplyToError::from_json(&json!({
1633                        "message": "Property .hola not found in object",
1634                        "path": ["nested", "hola"],
1635                        "range": hola_range,
1636                        "spec": spec.to_string(),
1637                    })),
1638                    ApplyToError::from_json(&json!({
1639                        "message": "Property .yellow not found in object",
1640                        "path": ["nested", "yellow"],
1641                        "range": yellow_range,
1642                        "spec": spec.to_string(),
1643                    })),
1644                ],
1645            )
1646        }
1647        assert_eq!(
1648            selection!("$.nested { hola yellow world }", spec).apply_to(&data),
1649            make_nested_path_expected((11, 15), (16, 22), spec),
1650        );
1651        assert_eq!(
1652            selection!(" $ . nested { hola yellow world } ", spec).apply_to(&data),
1653            make_nested_path_expected((14, 18), (19, 25), spec),
1654        );
1655
1656        fn make_partial_array_expected(
1657            goodbye_range: (usize, usize),
1658            spec: ConnectSpec,
1659        ) -> (Option<JSON>, Vec<ApplyToError>) {
1660            (
1661                Some(json!({
1662                    "partial": [
1663                        { "hello": 1, "goodbye": "farewell" },
1664                        { "hello": "two" },
1665                        { "hello": 3.0 },
1666                    ],
1667                })),
1668                vec![
1669                    ApplyToError::from_json(&json!({
1670                        "message": "Property .goodbye not found in object",
1671                        "path": ["array", 1, "goodbye"],
1672                        "range": goodbye_range,
1673                        "spec": spec.to_string(),
1674                    })),
1675                    ApplyToError::from_json(&json!({
1676                        "message": "Property .goodbye not found in object",
1677                        "path": ["array", 2, "goodbye"],
1678                        "range": goodbye_range,
1679                        "spec": spec.to_string(),
1680                    })),
1681                ],
1682            )
1683        }
1684        assert_eq!(
1685            selection!("partial: $.array { hello goodbye }", spec).apply_to(&data),
1686            make_partial_array_expected((25, 32), spec),
1687        );
1688        assert_eq!(
1689            selection!(" partial : $ . array { hello goodbye } ", spec).apply_to(&data),
1690            make_partial_array_expected((29, 36), spec),
1691        );
1692
1693        assert_eq!(
1694            selection!("good: array.hello bad: array.smello", spec).apply_to(&data),
1695            (
1696                Some(json!({
1697                    "good": [
1698                        1,
1699                        "two",
1700                        3.0,
1701                    ],
1702                    "bad": [
1703                        null,
1704                        null,
1705                        "yellow",
1706                    ],
1707                })),
1708                vec![
1709                    ApplyToError::from_json(&json!({
1710                        "message": "Property .smello not found in object",
1711                        "path": ["array", 0, "smello"],
1712                        "range": [29, 35],
1713                        "spec": spec.to_string(),
1714                    })),
1715                    ApplyToError::from_json(&json!({
1716                        "message": "Property .smello not found in object",
1717                        "path": ["array", 1, "smello"],
1718                        "range": [29, 35],
1719                        "spec": spec.to_string(),
1720                    })),
1721                ],
1722            )
1723        );
1724
1725        assert_eq!(
1726            selection!("array { hello smello }", spec).apply_to(&data),
1727            (
1728                Some(json!({
1729                    "array": [
1730                        { "hello": 1 },
1731                        { "hello": "two" },
1732                        { "hello": 3.0, "smello": "yellow" },
1733                    ],
1734                })),
1735                vec![
1736                    ApplyToError::from_json(&json!({
1737                        "message": "Property .smello not found in object",
1738                        "path": ["array", 0, "smello"],
1739                        "range": [14, 20],
1740                        "spec": spec.to_string(),
1741                    })),
1742                    ApplyToError::from_json(&json!({
1743                        "message": "Property .smello not found in object",
1744                        "path": ["array", 1, "smello"],
1745                        "range": [14, 20],
1746                        "spec": spec.to_string(),
1747                    })),
1748                ],
1749            )
1750        );
1751
1752        assert_eq!(
1753            selection!("$.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1754            (
1755                Some(json!({
1756                    "grouped": {
1757                        "hello": 123,
1758                        "world": true,
1759                    },
1760                })),
1761                vec![ApplyToError::from_json(&json!({
1762                    "message": "Property .smelly not found in object",
1763                    "path": ["nested", "smelly"],
1764                    "range": [28, 34],
1765                    "spec": spec.to_string(),
1766                }))],
1767            )
1768        );
1769
1770        assert_eq!(
1771            selection!("alias: $.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1772            (
1773                Some(json!({
1774                    "alias": {
1775                        "grouped": {
1776                            "hello": 123,
1777                            "world": true,
1778                        },
1779                    },
1780                })),
1781                vec![ApplyToError::from_json(&json!({
1782                    "message": "Property .smelly not found in object",
1783                    "path": ["nested", "smelly"],
1784                    "range": [35, 41],
1785                    "spec": spec.to_string(),
1786                }))],
1787            )
1788        );
1789    }
1790
1791    #[rstest]
1792    #[case::v0_2(ConnectSpec::V0_2)]
1793    #[case::v0_3(ConnectSpec::V0_3)]
1794    fn test_apply_to_nested_arrays(#[case] spec: ConnectSpec) {
1795        let data = json!({
1796            "arrayOfArrays": [
1797                [
1798                    { "x": 0, "y": 0 },
1799                ],
1800                [
1801                    { "x": 1, "y": 0 },
1802                    { "x": 1, "y": 1 },
1803                    { "x": 1, "y": 2 },
1804                ],
1805                [
1806                    { "x": 2, "y": 0 },
1807                    { "x": 2, "y": 1 },
1808                ],
1809                [],
1810                [
1811                    null,
1812                    { "x": 4, "y": 1 },
1813                    { "x": 4, "why": 2 },
1814                    null,
1815                    { "x": 4, "y": 4 },
1816                ]
1817            ],
1818        });
1819
1820        fn make_array_of_arrays_x_expected(
1821            x_range: (usize, usize),
1822            spec: ConnectSpec,
1823        ) -> (Option<JSON>, Vec<ApplyToError>) {
1824            (
1825                Some(json!([[0], [1, 1, 1], [2, 2], [], [null, 4, 4, null, 4]])),
1826                vec![
1827                    ApplyToError::from_json(&json!({
1828                        "message": "Property .x not found in null",
1829                        "path": ["arrayOfArrays", 4, 0, "x"],
1830                        "range": x_range,
1831                        "spec": spec.to_string(),
1832                    })),
1833                    ApplyToError::from_json(&json!({
1834                        "message": "Property .x not found in null",
1835                        "path": ["arrayOfArrays", 4, 3, "x"],
1836                        "range": x_range,
1837                        "spec": spec.to_string(),
1838                    })),
1839                ],
1840            )
1841        }
1842        assert_eq!(
1843            selection!("arrayOfArrays.x", spec).apply_to(&data),
1844            make_array_of_arrays_x_expected((14, 15), spec),
1845        );
1846        assert_eq!(
1847            selection!("$.arrayOfArrays.x", spec).apply_to(&data),
1848            make_array_of_arrays_x_expected((16, 17), spec),
1849        );
1850
1851        fn make_array_of_arrays_y_expected(
1852            y_range: (usize, usize),
1853            spec: ConnectSpec,
1854        ) -> (Option<JSON>, Vec<ApplyToError>) {
1855            (
1856                Some(json!([
1857                    [0],
1858                    [0, 1, 2],
1859                    [0, 1],
1860                    [],
1861                    [null, 1, null, null, 4],
1862                ])),
1863                vec![
1864                    ApplyToError::from_json(&json!({
1865                        "message": "Property .y not found in null",
1866                        "path": ["arrayOfArrays", 4, 0, "y"],
1867                        "range": y_range,
1868                        "spec": spec.to_string(),
1869                    })),
1870                    ApplyToError::from_json(&json!({
1871                        "message": "Property .y not found in object",
1872                        "path": ["arrayOfArrays", 4, 2, "y"],
1873                        "range": y_range,
1874                        "spec": spec.to_string(),
1875                    })),
1876                    ApplyToError::from_json(&json!({
1877                        "message": "Property .y not found in null",
1878                        "path": ["arrayOfArrays", 4, 3, "y"],
1879                        "range": y_range,
1880                        "spec": spec.to_string(),
1881                    })),
1882                ],
1883            )
1884        }
1885        assert_eq!(
1886            selection!("arrayOfArrays.y", spec).apply_to(&data),
1887            make_array_of_arrays_y_expected((14, 15), spec),
1888        );
1889        assert_eq!(
1890            selection!("$.arrayOfArrays.y", spec).apply_to(&data),
1891            make_array_of_arrays_y_expected((16, 17), spec),
1892        );
1893
1894        assert_eq!(
1895            selection!("alias: arrayOfArrays { x y }", spec).apply_to(&data),
1896            (
1897                Some(json!({
1898                    "alias": [
1899                        [
1900                            { "x": 0, "y": 0 },
1901                        ],
1902                        [
1903                            { "x": 1, "y": 0 },
1904                            { "x": 1, "y": 1 },
1905                            { "x": 1, "y": 2 },
1906                        ],
1907                        [
1908                            { "x": 2, "y": 0 },
1909                            { "x": 2, "y": 1 },
1910                        ],
1911                        [],
1912                        [
1913                            null,
1914                            { "x": 4, "y": 1 },
1915                            { "x": 4 },
1916                            null,
1917                            { "x": 4, "y": 4 },
1918                        ]
1919                    ],
1920                })),
1921                vec![
1922                    ApplyToError::from_json(&json!({
1923                        "message": "Property .x not found in null",
1924                        "path": ["arrayOfArrays", 4, 0, "x"],
1925                        "range": [23, 24],
1926                        "spec": spec.to_string(),
1927                    })),
1928                    ApplyToError::from_json(&json!({
1929                        "message": "Property .y not found in null",
1930                        "path": ["arrayOfArrays", 4, 0, "y"],
1931                        "range": [25, 26],
1932                        "spec": spec.to_string(),
1933                    })),
1934                    ApplyToError::from_json(&json!({
1935                        "message": "Property .y not found in object",
1936                        "path": ["arrayOfArrays", 4, 2, "y"],
1937                        "range": [25, 26],
1938                        "spec": spec.to_string(),
1939                    })),
1940                    ApplyToError::from_json(&json!({
1941                        "message": "Property .x not found in null",
1942                        "path": ["arrayOfArrays", 4, 3, "x"],
1943                        "range": [23, 24],
1944                        "spec": spec.to_string(),
1945                    })),
1946                    ApplyToError::from_json(&json!({
1947                        "message": "Property .y not found in null",
1948                        "path": ["arrayOfArrays", 4, 3, "y"],
1949                        "range": [25, 26],
1950                        "spec": spec.to_string(),
1951                    })),
1952                ],
1953            ),
1954        );
1955
1956        fn make_array_of_arrays_x_y_expected(
1957            x_range: (usize, usize),
1958            y_range: (usize, usize),
1959            spec: ConnectSpec,
1960        ) -> (Option<JSON>, Vec<ApplyToError>) {
1961            (
1962                Some(json!({
1963                    "ys": [
1964                        [0],
1965                        [0, 1, 2],
1966                        [0, 1],
1967                        [],
1968                        [null, 1, null, null, 4],
1969                    ],
1970                    "xs": [
1971                        [0],
1972                        [1, 1, 1],
1973                        [2, 2],
1974                        [],
1975                        [null, 4, 4, null, 4],
1976                    ],
1977                })),
1978                vec![
1979                    ApplyToError::from_json(&json!({
1980                        "message": "Property .y not found in null",
1981                        "path": ["arrayOfArrays", 4, 0, "y"],
1982                        "range": y_range,
1983                        "spec": spec.to_string(),
1984                    })),
1985                    ApplyToError::from_json(&json!({
1986                        "message": "Property .y not found in object",
1987                        "path": ["arrayOfArrays", 4, 2, "y"],
1988                        "range": y_range,
1989                        "spec": spec.to_string(),
1990                    })),
1991                    ApplyToError::from_json(&json!({
1992                        // Reversing the order of "path" and "message" here to make
1993                        // sure that doesn't affect the deduplication logic.
1994                        "path": ["arrayOfArrays", 4, 3, "y"],
1995                        "message": "Property .y not found in null",
1996                        "range": y_range,
1997                        "spec": spec.to_string(),
1998                    })),
1999                    ApplyToError::from_json(&json!({
2000                        "message": "Property .x not found in null",
2001                        "path": ["arrayOfArrays", 4, 0, "x"],
2002                        "range": x_range,
2003                        "spec": spec.to_string(),
2004                    })),
2005                    ApplyToError::from_json(&json!({
2006                        "message": "Property .x not found in null",
2007                        "path": ["arrayOfArrays", 4, 3, "x"],
2008                        "range": x_range,
2009                        "spec": spec.to_string(),
2010                    })),
2011                ],
2012            )
2013        }
2014        assert_eq!(
2015            selection!("ys: arrayOfArrays.y xs: arrayOfArrays.x", spec).apply_to(&data),
2016            make_array_of_arrays_x_y_expected((38, 39), (18, 19), spec),
2017        );
2018        assert_eq!(
2019            selection!("ys: $.arrayOfArrays.y xs: $.arrayOfArrays.x", spec).apply_to(&data),
2020            make_array_of_arrays_x_y_expected((42, 43), (20, 21), spec),
2021        );
2022    }
2023
2024    #[rstest]
2025    #[case::v0_2(ConnectSpec::V0_2)]
2026    #[case::v0_3(ConnectSpec::V0_3)]
2027    fn test_apply_to_variable_expressions(#[case] spec: ConnectSpec) {
2028        let id_object = selection!("id: $", spec).apply_to(&json!(123));
2029        assert_eq!(id_object, (Some(json!({"id": 123})), vec![]));
2030
2031        let data = json!({
2032            "id": 123,
2033            "name": "Ben",
2034            "friend_ids": [234, 345, 456]
2035        });
2036
2037        assert_eq!(
2038            selection!("id name friends: friend_ids { id: $ }", spec).apply_to(&data),
2039            (
2040                Some(json!({
2041                    "id": 123,
2042                    "name": "Ben",
2043                    "friends": [
2044                        { "id": 234 },
2045                        { "id": 345 },
2046                        { "id": 456 },
2047                    ],
2048                })),
2049                vec![],
2050            ),
2051        );
2052
2053        let mut vars = IndexMap::default();
2054        vars.insert("$args".to_string(), json!({ "id": "id from args" }));
2055        assert_eq!(
2056            selection!("id: $args.id name", spec).apply_with_vars(&data, &vars),
2057            (
2058                Some(json!({
2059                    "id": "id from args",
2060                    "name": "Ben"
2061                })),
2062                vec![],
2063            ),
2064        );
2065        assert_eq!(
2066            selection!("nested.path { id: $args.id name }", spec).apply_to(&json!({
2067                "nested": {
2068                    "path": data,
2069                },
2070            })),
2071            (
2072                Some(json!({
2073                    "name": "Ben"
2074                })),
2075                vec![ApplyToError::from_json(&json!({
2076                    "message": "Variable $args not found",
2077                    "path": ["nested", "path"],
2078                    "range": [18, 23],
2079                    "spec": spec.to_string(),
2080                }))],
2081            ),
2082        );
2083        let mut vars_without_args_id = IndexMap::default();
2084        vars_without_args_id.insert("$args".to_string(), json!({ "unused": "ignored" }));
2085        assert_eq!(
2086            selection!("id: $args.id name", spec).apply_with_vars(&data, &vars_without_args_id),
2087            (
2088                Some(json!({
2089                    "name": "Ben"
2090                })),
2091                vec![ApplyToError::from_json(&json!({
2092                    "message": "Property .id not found in object",
2093                    "path": ["$args", "id"],
2094                    "range": [10, 12],
2095                    "spec": spec.to_string(),
2096                }))],
2097            ),
2098        );
2099
2100        // A single variable path should not be mapped over an input array.
2101        assert_eq!(
2102            selection!("$args.id", spec).apply_with_vars(&json!([1, 2, 3]), &vars),
2103            (Some(json!("id from args")), vec![]),
2104        );
2105    }
2106
2107    #[test]
2108    fn test_apply_to_variable_expressions_typename() {
2109        let typename_object =
2110            selection!("__typename: $->echo('Product') reviews { __typename: $->echo('Review') }")
2111                .apply_to(&json!({"reviews": [{}]}));
2112        assert_eq!(
2113            typename_object,
2114            (
2115                Some(json!({"__typename": "Product", "reviews": [{ "__typename": "Review" }] })),
2116                vec![]
2117            )
2118        );
2119    }
2120
2121    #[test]
2122    fn test_literal_expressions_in_parentheses() {
2123        assert_eq!(
2124            selection!("__typename: $('Product')").apply_to(&json!({})),
2125            (Some(json!({"__typename": "Product"})), vec![]),
2126        );
2127
2128        assert_eq!(
2129            selection!(" __typename : 'Product' ").apply_to(&json!({})),
2130            (
2131                Some(json!({})),
2132                vec![ApplyToError::new(
2133                    "Property .\"Product\" not found in object".to_string(),
2134                    vec![json!("Product")],
2135                    Some(14..23),
2136                    ConnectSpec::latest(),
2137                )],
2138            ),
2139        );
2140
2141        assert_eq!(
2142            selection!(
2143                r#"
2144                one: $(1)
2145                two: $(2)
2146                negativeThree: $(-  3)
2147                true: $(true  )
2148                false: $(  false)
2149                null: $(null)
2150                string: $("string")
2151                array: $( [ 1 , 2 , 3 ] )
2152                object: $( { "key" : "value" } )
2153                path: $(nested.path)
2154            "#
2155            )
2156            .apply_to(&json!({
2157                "nested": {
2158                    "path": "nested path value"
2159                }
2160            })),
2161            (
2162                Some(json!({
2163                    "one": 1,
2164                    "two": 2,
2165                    "negativeThree": -3,
2166                    "true": true,
2167                    "false": false,
2168                    "null": null,
2169                    "string": "string",
2170                    "array": [1, 2, 3],
2171                    "object": { "key": "value" },
2172                    "path": "nested path value",
2173                })),
2174                vec![],
2175            ),
2176        );
2177
2178        assert_eq!(
2179            selection!(
2180                r#"
2181                one: $(1)->typeof
2182                two: $(2)->typeof
2183                negativeThree: $(-3)->typeof
2184                true: $(true)->typeof
2185                false: $(false)->typeof
2186                null: $(null)->typeof
2187                string: $("string")->typeof
2188                array: $([1, 2, 3])->typeof
2189                object: $({ "key": "value" })->typeof
2190                path: $(nested.path)->typeof
2191            "#
2192            )
2193            .apply_to(&json!({
2194                "nested": {
2195                    "path": 12345
2196                }
2197            })),
2198            (
2199                Some(json!({
2200                    "one": "number",
2201                    "two": "number",
2202                    "negativeThree": "number",
2203                    "true": "boolean",
2204                    "false": "boolean",
2205                    "null": "null",
2206                    "string": "string",
2207                    "array": "array",
2208                    "object": "object",
2209                    "path": "number",
2210                })),
2211                vec![],
2212            ),
2213        );
2214
2215        assert_eq!(
2216            selection!(
2217                r#"
2218                items: $([
2219                    1,
2220                    -2.0,
2221                    true,
2222                    false,
2223                    null,
2224                    "string",
2225                    [1, 2, 3],
2226                    { "key": "value" },
2227                    nested.path,
2228                ])->map(@->typeof)
2229            "#
2230            )
2231            .apply_to(&json!({
2232                "nested": {
2233                    "path": { "deeply": "nested" }
2234                }
2235            })),
2236            (
2237                Some(json!({
2238                    "items": [
2239                        "number",
2240                        "number",
2241                        "boolean",
2242                        "boolean",
2243                        "null",
2244                        "string",
2245                        "array",
2246                        "object",
2247                        "object",
2248                    ],
2249                })),
2250                vec![],
2251            ),
2252        );
2253
2254        assert_eq!(
2255            selection!(
2256                r#"
2257                $({
2258                    one: 1,
2259                    two: 2,
2260                    negativeThree: -3,
2261                    true: true,
2262                    false: false,
2263                    null: null,
2264                    string: "string",
2265                    array: [1, 2, 3],
2266                    object: { "key": "value" },
2267                    path: $ . nested . path ,
2268                })->entries
2269            "#
2270            )
2271            .apply_to(&json!({
2272                "nested": {
2273                    "path": "nested path value"
2274                }
2275            })),
2276            (
2277                Some(json!([
2278                    { "key": "one", "value": 1 },
2279                    { "key": "two", "value": 2 },
2280                    { "key": "negativeThree", "value": -3 },
2281                    { "key": "true", "value": true },
2282                    { "key": "false", "value": false },
2283                    { "key": "null", "value": null },
2284                    { "key": "string", "value": "string" },
2285                    { "key": "array", "value": [1, 2, 3] },
2286                    { "key": "object", "value": { "key": "value" } },
2287                    { "key": "path", "value": "nested path value" },
2288                ])),
2289                vec![],
2290            ),
2291        );
2292
2293        assert_eq!(
2294            selection!(
2295                r#"
2296                $({
2297                    string: $("string")->slice(1, 4),
2298                    array: $([1, 2, 3])->map(@->add(10)),
2299                    object: $({ "key": "value" })->get("key"),
2300                    path: nested.path->slice($("nested ")->size),
2301                    needlessParens: $("oyez"),
2302                    withoutParens: "oyez",
2303                })
2304            "#
2305            )
2306            .apply_to(&json!({
2307                "nested": {
2308                    "path": "nested path value"
2309                }
2310            })),
2311            (
2312                Some(json!({
2313                    "string": "tri",
2314                    "array": [11, 12, 13],
2315                    "object": "value",
2316                    "path": "path value",
2317                    "needlessParens": "oyez",
2318                    "withoutParens": "oyez",
2319                })),
2320                vec![],
2321            ),
2322        );
2323
2324        assert_eq!(
2325            selection!(
2326                r#"
2327                string: $("string")->slice(1, 4)
2328                array: $([1, 2, 3])->map(@->add(10))
2329                object: $({ "key": "value" })->get("key")
2330                path: nested.path->slice($("nested ")->size)
2331            "#
2332            )
2333            .apply_to(&json!({
2334                "nested": {
2335                    "path": "nested path value"
2336                }
2337            })),
2338            (
2339                Some(json!({
2340                    "string": "tri",
2341                    "array": [11, 12, 13],
2342                    "object": "value",
2343                    "path": "path value",
2344                })),
2345                vec![],
2346            ),
2347        );
2348    }
2349
2350    #[rstest]
2351    #[case::v0_2(ConnectSpec::V0_2)]
2352    #[case::v0_3(ConnectSpec::V0_3)]
2353    fn test_inline_paths_with_subselections(#[case] spec: ConnectSpec) {
2354        let data = json!({
2355            "id": 123,
2356            "created": "2021-01-01T00:00:00Z",
2357            "model": "gpt-4o",
2358            "choices": [{
2359                "index": 0,
2360                "message": {
2361                    "role": "assistant",
2362                    "content": "The capital of Australia is Canberra.",
2363                },
2364            }, {
2365                "index": 1,
2366                "message": {
2367                    "role": "assistant",
2368                    "content": "The capital of Australia is Sydney.",
2369                },
2370            }],
2371        });
2372
2373        {
2374            let expected = (
2375                Some(json!({
2376                    "id": 123,
2377                    "created": "2021-01-01T00:00:00Z",
2378                    "model": "gpt-4o",
2379                    "role": "assistant",
2380                    "content": "The capital of Australia is Canberra.",
2381                })),
2382                vec![],
2383            );
2384
2385            assert_eq!(
2386                selection!(
2387                    r#"
2388                    id
2389                    created
2390                    model
2391                    role: choices->first.message.role
2392                    content: choices->first.message.content
2393                "#,
2394                    spec
2395                )
2396                .apply_to(&data),
2397                expected,
2398            );
2399
2400            assert_eq!(
2401                selection!(
2402                    r#"
2403                    id
2404                    created
2405                    model
2406                    choices->first.message {
2407                        role
2408                        content
2409                    }
2410                "#,
2411                    spec
2412                )
2413                .apply_to(&data),
2414                expected,
2415            );
2416
2417            assert_eq!(
2418                selection!(
2419                    r#"
2420                    id
2421                    choices->first.message {
2422                        role
2423                        content
2424                    }
2425                    created
2426                    model
2427                "#,
2428                    spec
2429                )
2430                .apply_to(&data),
2431                expected,
2432            );
2433        }
2434
2435        {
2436            let expected = (
2437                Some(json!({
2438                    "id": 123,
2439                    "created": "2021-01-01T00:00:00Z",
2440                    "model": "gpt-4o",
2441                    "role": "assistant",
2442                    "message": "The capital of Australia is Sydney.",
2443                })),
2444                vec![],
2445            );
2446
2447            assert_eq!(
2448                selection!(
2449                    r#"
2450                    id
2451                    created
2452                    model
2453                    role: choices->last.message.role
2454                    message: choices->last.message.content
2455                "#,
2456                    spec
2457                )
2458                .apply_to(&data),
2459                expected,
2460            );
2461
2462            assert_eq!(
2463                selection!(
2464                    r#"
2465                    id
2466                    created
2467                    model
2468                    choices->last.message {
2469                        role
2470                        message: content
2471                    }
2472                "#,
2473                    spec
2474                )
2475                .apply_to(&data),
2476                expected,
2477            );
2478
2479            assert_eq!(
2480                selection!(
2481                    r#"
2482                    created
2483                    choices->last.message {
2484                        message: content
2485                        role
2486                    }
2487                    model
2488                    id
2489                "#,
2490                    spec
2491                )
2492                .apply_to(&data),
2493                expected,
2494            );
2495        }
2496
2497        {
2498            let expected = (
2499                Some(json!({
2500                    "id": 123,
2501                    "created": "2021-01-01T00:00:00Z",
2502                    "model": "gpt-4o",
2503                    "role": "assistant",
2504                    "correct": "The capital of Australia is Canberra.",
2505                    "incorrect": "The capital of Australia is Sydney.",
2506                })),
2507                vec![],
2508            );
2509
2510            assert_eq!(
2511                selection!(
2512                    r#"
2513                    id
2514                    created
2515                    model
2516                    role: choices->first.message.role
2517                    correct: choices->first.message.content
2518                    incorrect: choices->last.message.content
2519                "#,
2520                    spec
2521                )
2522                .apply_to(&data),
2523                expected,
2524            );
2525
2526            assert_eq!(
2527                selection!(
2528                    r#"
2529                    id
2530                    created
2531                    model
2532                    choices->first.message {
2533                        role
2534                        correct: content
2535                    }
2536                    choices->last.message {
2537                        incorrect: content
2538                    }
2539                "#,
2540                    spec
2541                )
2542                .apply_to(&data),
2543                expected,
2544            );
2545
2546            assert_eq!(
2547                selection!(
2548                    r#"
2549                    id
2550                    created
2551                    model
2552                    choices->first.message {
2553                        role
2554                        correct: content
2555                    }
2556                    incorrect: choices->last.message.content
2557                "#,
2558                    spec
2559                )
2560                .apply_to(&data),
2561                expected,
2562            );
2563
2564            assert_eq!(
2565                selection!(
2566                    r#"
2567                    id
2568                    created
2569                    model
2570                    choices->first.message {
2571                        correct: content
2572                    }
2573                    choices->last.message {
2574                        role
2575                        incorrect: content
2576                    }
2577                "#,
2578                    spec
2579                )
2580                .apply_to(&data),
2581                expected,
2582            );
2583
2584            assert_eq!(
2585                selection!(
2586                    r#"
2587                    id
2588                    created
2589                    correct: choices->first.message.content
2590                    choices->last.message {
2591                        role
2592                        incorrect: content
2593                    }
2594                    model
2595                "#,
2596                    spec
2597                )
2598                .apply_to(&data),
2599                expected,
2600            );
2601        }
2602
2603        {
2604            let data = json!({
2605                "from": "data",
2606            });
2607
2608            let vars = {
2609                let mut vars = IndexMap::default();
2610                vars.insert(
2611                    "$this".to_string(),
2612                    json!({
2613                        "id": 1234,
2614                    }),
2615                );
2616                vars.insert(
2617                    "$args".to_string(),
2618                    json!({
2619                        "input": {
2620                            "title": "The capital of Australia",
2621                            "body": "Canberra",
2622                        },
2623                        "extra": "extra",
2624                    }),
2625                );
2626                vars
2627            };
2628
2629            let expected = (
2630                Some(json!({
2631                    "id": 1234,
2632                    "title": "The capital of Australia",
2633                    "body": "Canberra",
2634                    "from": "data",
2635                })),
2636                vec![],
2637            );
2638
2639            assert_eq!(
2640                selection!(
2641                    r#"
2642                    id: $this.id
2643                    $args.input {
2644                        title
2645                        body
2646                    }
2647                    from
2648                "#,
2649                    spec
2650                )
2651                .apply_with_vars(&data, &vars),
2652                expected,
2653            );
2654
2655            assert_eq!(
2656                selection!(
2657                    r#"
2658                    from
2659                    $args.input { title body }
2660                    id: $this.id
2661                "#,
2662                    spec
2663                )
2664                .apply_with_vars(&data, &vars),
2665                expected,
2666            );
2667
2668            assert_eq!(
2669                selection!(
2670                    r#"
2671                    $args.input { body title }
2672                    from
2673                    id: $this.id
2674                "#,
2675                    spec
2676                )
2677                .apply_with_vars(&data, &vars),
2678                expected,
2679            );
2680
2681            assert_eq!(
2682                selection!(
2683                    r#"
2684                    id: $this.id
2685                    $args { $.input { title body } }
2686                    from
2687                "#,
2688                    spec
2689                )
2690                .apply_with_vars(&data, &vars),
2691                expected,
2692            );
2693
2694            assert_eq!(
2695                selection!(
2696                    r#"
2697                    id: $this.id
2698                    $args { $.input { title body } extra }
2699                    from: $.from
2700                "#,
2701                    spec
2702                )
2703                .apply_with_vars(&data, &vars),
2704                (
2705                    Some(json!({
2706                        "id": 1234,
2707                        "title": "The capital of Australia",
2708                        "body": "Canberra",
2709                        "extra": "extra",
2710                        "from": "data",
2711                    })),
2712                    vec![],
2713                ),
2714            );
2715
2716            assert_eq!(
2717                selection!(
2718                    r#"
2719                    # Equivalent to id: $this.id
2720                    $this { id }
2721
2722                    $args {
2723                        __typename: $("Args")
2724
2725                        # Requiring $. instead of just . prevents .input from
2726                        # parsing as a key applied to the $("Args") string.
2727                        $.input { title body }
2728
2729                        extra
2730                    }
2731
2732                    from: $.from
2733                "#,
2734                    spec
2735                )
2736                .apply_with_vars(&data, &vars),
2737                (
2738                    Some(json!({
2739                        "id": 1234,
2740                        "title": "The capital of Australia",
2741                        "body": "Canberra",
2742                        "__typename": "Args",
2743                        "extra": "extra",
2744                        "from": "data",
2745                    })),
2746                    vec![],
2747                ),
2748            );
2749        }
2750    }
2751
2752    #[test]
2753    fn test_inline_path_errors() {
2754        {
2755            let data = json!({
2756                "id": 123,
2757                "created": "2021-01-01T00:00:00Z",
2758                "model": "gpt-4o",
2759                "choices": [{
2760                    "message": "The capital of Australia is Canberra.",
2761                }, {
2762                    "message": "The capital of Australia is Sydney.",
2763                }],
2764            });
2765
2766            let expected = (
2767                Some(json!({
2768                    "id": 123,
2769                    "created": "2021-01-01T00:00:00Z",
2770                    "model": "gpt-4o",
2771                })),
2772                vec![
2773                    ApplyToError::new(
2774                        "Property .role not found in string".to_string(),
2775                        vec![
2776                            json!("choices"),
2777                            json!("->first"),
2778                            json!("message"),
2779                            json!("role"),
2780                        ],
2781                        Some(123..127),
2782                        ConnectSpec::latest(),
2783                    ),
2784                    ApplyToError::new(
2785                        "Property .content not found in string".to_string(),
2786                        vec![
2787                            json!("choices"),
2788                            json!("->first"),
2789                            json!("message"),
2790                            json!("content"),
2791                        ],
2792                        Some(128..135),
2793                        ConnectSpec::latest(),
2794                    ),
2795                    ApplyToError::new(
2796                        "Expected object or null, not string".to_string(),
2797                        vec![],
2798                        // This is the range of the whole
2799                        // `choices->first.message { role content }`
2800                        // subselection.
2801                        Some(98..137),
2802                        ConnectSpec::latest(),
2803                    ),
2804                ],
2805            );
2806
2807            assert_eq!(
2808                selection!(
2809                    r#"
2810                    id
2811                    created
2812                    model
2813                    choices->first.message { role content }
2814                "#
2815                )
2816                .apply_to(&data),
2817                expected,
2818            );
2819        }
2820
2821        assert_eq!(
2822            selection!("id nested.path.nonexistent { name }").apply_to(&json!({
2823                "id": 2345,
2824                "nested": {
2825                    "path": "nested path value",
2826                },
2827            })),
2828            (
2829                Some(json!({
2830                    "id": 2345,
2831                })),
2832                vec![
2833                    ApplyToError::new(
2834                        "Property .nonexistent not found in string".to_string(),
2835                        vec![json!("nested"), json!("path"), json!("nonexistent")],
2836                        Some(15..26),
2837                        ConnectSpec::latest(),
2838                    ),
2839                    ApplyToError::new(
2840                        "Inlined path produced no value".to_string(),
2841                        vec![],
2842                        // This is the range of the whole
2843                        // `nested.path.nonexistent { name }` path selection.
2844                        Some(3..35),
2845                        ConnectSpec::latest(),
2846                    ),
2847                ],
2848            ),
2849        );
2850
2851        let valid_inline_path_selection = JSONSelection::named(SubSelection {
2852            selections: vec![NamedSelection {
2853                prefix: NamingPrefix::None,
2854                path: PathSelection {
2855                    path: PathList::Key(
2856                        Key::field("some").into_with_range(),
2857                        PathList::Key(
2858                            Key::field("object").into_with_range(),
2859                            PathList::Empty.into_with_range(),
2860                        )
2861                        .into_with_range(),
2862                    )
2863                    .into_with_range(),
2864                },
2865            }],
2866            ..Default::default()
2867        });
2868
2869        assert_eq!(
2870            valid_inline_path_selection.apply_to(&json!({
2871                "some": {
2872                    "object": {
2873                        "key": "value",
2874                    },
2875                },
2876            })),
2877            (
2878                Some(json!({
2879                    "key": "value",
2880                })),
2881                vec![],
2882            ),
2883        );
2884    }
2885
2886    #[test]
2887    fn test_apply_to_non_identifier_properties() {
2888        let data = json!({
2889            "not an identifier": [
2890                { "also.not.an.identifier": 0 },
2891                { "also.not.an.identifier": 1 },
2892                { "also.not.an.identifier": 2 },
2893            ],
2894            "another": {
2895                "pesky string literal!": {
2896                    "identifier": 123,
2897                    "{ evil braces }": true,
2898                },
2899            },
2900        });
2901
2902        assert_eq!(
2903            // The grammar enforces that we must always provide identifier aliases
2904            // for non-identifier properties, so the data we get back will always be
2905            // GraphQL-safe.
2906            selection!("alias: 'not an identifier' { safe: 'also.not.an.identifier' }")
2907                .apply_to(&data),
2908            (
2909                Some(json!({
2910                    "alias": [
2911                        { "safe": 0 },
2912                        { "safe": 1 },
2913                        { "safe": 2 },
2914                    ],
2915                })),
2916                vec![],
2917            ),
2918        );
2919
2920        assert_eq!(
2921            selection!("'not an identifier'.'also.not.an.identifier'").apply_to(&data),
2922            (Some(json!([0, 1, 2])), vec![],),
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\" { safe: \"also.not.an.identifier\" }")
2932                .apply_to(&data),
2933            (
2934                Some(json!([
2935                    { "safe": 0 },
2936                    { "safe": 1 },
2937                    { "safe": 2 },
2938                ])),
2939                vec![],
2940            ),
2941        );
2942
2943        assert_eq!(
2944            selection!(
2945                "another {
2946                pesky: 'pesky string literal!' {
2947                    identifier
2948                    evil: '{ evil braces }'
2949                }
2950            }"
2951            )
2952            .apply_to(&data),
2953            (
2954                Some(json!({
2955                    "another": {
2956                        "pesky": {
2957                            "identifier": 123,
2958                            "evil": true,
2959                        },
2960                    },
2961                })),
2962                vec![],
2963            ),
2964        );
2965
2966        assert_eq!(
2967            selection!("another.'pesky string literal!'.'{ evil braces }'").apply_to(&data),
2968            (Some(json!(true)), vec![],),
2969        );
2970
2971        assert_eq!(
2972            selection!("another.'pesky string literal!'.\"identifier\"").apply_to(&data),
2973            (Some(json!(123)), vec![],),
2974        );
2975
2976        assert_eq!(
2977            selection!("$.another.'pesky string literal!'.\"identifier\"").apply_to(&data),
2978            (Some(json!(123)), vec![],),
2979        );
2980    }
2981
2982    #[rstest]
2983    #[case::latest(ConnectSpec::V0_2)]
2984    #[case::next(ConnectSpec::V0_3)]
2985    fn test_left_associative_path_evaluation(#[case] spec: ConnectSpec) {
2986        assert_eq!(
2987            selection!("batch.id->first", spec).apply_to(&json!({
2988                "batch": [
2989                    { "id": 1 },
2990                    { "id": 2 },
2991                    { "id": 3 },
2992                ],
2993            })),
2994            (Some(json!(1)), vec![]),
2995        );
2996
2997        assert_eq!(
2998            selection!("batch.id->last", spec).apply_to(&json!({
2999                "batch": [
3000                    { "id": 1 },
3001                    { "id": 2 },
3002                    { "id": 3 },
3003                ],
3004            })),
3005            (Some(json!(3)), vec![]),
3006        );
3007
3008        assert_eq!(
3009            selection!("batch.id->size", spec).apply_to(&json!({
3010                "batch": [
3011                    { "id": 1 },
3012                    { "id": 2 },
3013                    { "id": 3 },
3014                ],
3015            })),
3016            (Some(json!(3)), vec![]),
3017        );
3018
3019        assert_eq!(
3020            selection!("batch.id->slice(1)->first", spec).apply_to(&json!({
3021                "batch": [
3022                    { "id": 1 },
3023                    { "id": 2 },
3024                    { "id": 3 },
3025                ],
3026            })),
3027            (Some(json!(2)), vec![]),
3028        );
3029
3030        assert_eq!(
3031            selection!("batch.id->map({ batchId: @ })", spec).apply_to(&json!({
3032                "batch": [
3033                    { "id": 1 },
3034                    { "id": 2 },
3035                    { "id": 3 },
3036                ],
3037            })),
3038            (
3039                Some(json!([
3040                    { "batchId": 1 },
3041                    { "batchId": 2 },
3042                    { "batchId": 3 },
3043                ])),
3044                vec![],
3045            ),
3046        );
3047
3048        let mut vars = IndexMap::default();
3049        vars.insert(
3050            "$batch".to_string(),
3051            json!([
3052                { "id": 4 },
3053                { "id": 5 },
3054                { "id": 6 },
3055            ]),
3056        );
3057        assert_eq!(
3058            selection!("$batch.id->map({ batchId: @ })", spec).apply_with_vars(
3059                &json!({
3060                    "batch": "ignored",
3061                }),
3062                &vars
3063            ),
3064            (
3065                Some(json!([
3066                    { "batchId": 4 },
3067                    { "batchId": 5 },
3068                    { "batchId": 6 },
3069                ])),
3070                vec![],
3071            ),
3072        );
3073
3074        assert_eq!(
3075            selection!("batch.id->map({ batchId: @ })->first", spec).apply_to(&json!({
3076                "batch": [
3077                    { "id": 7 },
3078                    { "id": 8 },
3079                    { "id": 9 },
3080                ],
3081            })),
3082            (Some(json!({ "batchId": 7 })), vec![]),
3083        );
3084
3085        assert_eq!(
3086            selection!("batch.id->map({ batchId: @ })->last", spec).apply_to(&json!({
3087                "batch": [
3088                    { "id": 7 },
3089                    { "id": 8 },
3090                    { "id": 9 },
3091                ],
3092            })),
3093            (Some(json!({ "batchId": 9 })), vec![]),
3094        );
3095
3096        assert_eq!(
3097            selection!("$batch.id->map({ batchId: @ })->first", spec).apply_with_vars(
3098                &json!({
3099                    "batch": "ignored",
3100                }),
3101                &vars
3102            ),
3103            (Some(json!({ "batchId": 4 })), vec![]),
3104        );
3105
3106        assert_eq!(
3107            selection!("$batch.id->map({ batchId: @ })->last", spec).apply_with_vars(
3108                &json!({
3109                    "batch": "ignored",
3110                }),
3111                &vars
3112            ),
3113            (Some(json!({ "batchId": 6 })), vec![]),
3114        );
3115
3116        assert_eq!(
3117            selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3118                "arrays": [
3119                    { "as": { "bs": [10, 20, 30] } },
3120                    { "as": { "bs": [40, 50, 60] } },
3121                    { "as": { "bs": [70, 80, 90] } },
3122                ],
3123            })),
3124            (
3125                Some(json!({
3126                    "echoed": [
3127                        [10, 20, 30],
3128                        [40, 50, 60],
3129                        [70, 80, 90],
3130                    ],
3131                })),
3132                vec![],
3133            ),
3134        );
3135
3136        assert_eq!(
3137            selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3138                "arrays": [
3139                    { "as": { "bs": [10, 20, 30] } },
3140                    { "as": [
3141                        { "bs": [40, 50, 60] },
3142                        { "bs": [70, 80, 90] },
3143                    ] },
3144                    { "as": { "bs": [100, 110, 120] } },
3145                ],
3146            })),
3147            (
3148                Some(json!({
3149                    "echoed": [
3150                        [10, 20, 30],
3151                        [
3152                            [40, 50, 60],
3153                            [70, 80, 90],
3154                        ],
3155                        [100, 110, 120],
3156                    ],
3157                })),
3158                vec![],
3159            ),
3160        );
3161
3162        assert_eq!(
3163            selection!("batch.id->jsonStringify", spec).apply_to(&json!({
3164                "batch": [
3165                    { "id": 1 },
3166                    { "id": 2 },
3167                    { "id": 3 },
3168                ],
3169            })),
3170            (Some(json!("[1,2,3]")), vec![]),
3171        );
3172
3173        assert_eq!(
3174            selection!("batch.id->map([@])->echo([@])->jsonStringify", spec).apply_to(&json!({
3175                "batch": [
3176                    { "id": 1 },
3177                    { "id": 2 },
3178                    { "id": 3 },
3179                ],
3180            })),
3181            (Some(json!("[[[1],[2],[3]]]")), vec![]),
3182        );
3183
3184        assert_eq!(
3185            selection!("batch.id->map([@])->echo([@])->jsonStringify->typeof", spec).apply_to(
3186                &json!({
3187                    "batch": [
3188                        { "id": 1 },
3189                        { "id": 2 },
3190                        { "id": 3 },
3191                    ],
3192                })
3193            ),
3194            (Some(json!("string")), vec![]),
3195        );
3196    }
3197
3198    #[test]
3199    fn test_left_associative_output_shapes_v0_2() {
3200        let spec = ConnectSpec::V0_2;
3201
3202        assert_eq!(
3203            selection!("$batch.id", spec).shape().pretty_print(),
3204            "$batch.id"
3205        );
3206
3207        assert_eq!(
3208            selection!("$batch.id->first", spec).shape().pretty_print(),
3209            "Unknown",
3210        );
3211
3212        assert_eq!(
3213            selection!("$batch.id->last", spec).shape().pretty_print(),
3214            "Unknown",
3215        );
3216
3217        let mut named_shapes = IndexMap::default();
3218        named_shapes.insert(
3219            "$batch".to_string(),
3220            Shape::list(
3221                Shape::record(
3222                    {
3223                        let mut map = Shape::empty_map();
3224                        map.insert("id".to_string(), Shape::int([]));
3225                        map
3226                    },
3227                    [],
3228                ),
3229                [],
3230            ),
3231        );
3232
3233        let root_shape = Shape::name("$root", []);
3234        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3235            .with_spec(spec)
3236            .with_named_shapes(named_shapes);
3237
3238        let computed_batch_id =
3239            selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3240        assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3241
3242        let computed_first = selection!("$batch.id->first", spec)
3243            .compute_output_shape(&shape_context, root_shape.clone());
3244        assert_eq!(computed_first.pretty_print(), "Unknown");
3245
3246        let computed_last = selection!("$batch.id->last", spec)
3247            .compute_output_shape(&shape_context, root_shape.clone());
3248        assert_eq!(computed_last.pretty_print(), "Unknown");
3249
3250        assert_eq!(
3251            selection!("$batch.id->jsonStringify", spec)
3252                .shape()
3253                .pretty_print(),
3254            "Unknown",
3255        );
3256
3257        assert_eq!(
3258            selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3259                .shape()
3260                .pretty_print(),
3261            "Unknown",
3262        );
3263
3264        assert_eq!(
3265            selection!("$batch.id->map(@)->echo(@)", spec)
3266                .shape()
3267                .pretty_print(),
3268            "Unknown",
3269        );
3270
3271        assert_eq!(
3272            selection!("$batch.id->map(@)->echo([@])", spec)
3273                .shape()
3274                .pretty_print(),
3275            "Unknown",
3276        );
3277
3278        assert_eq!(
3279            selection!("$batch.id->map([@])->echo(@)", spec)
3280                .shape()
3281                .pretty_print(),
3282            "Unknown",
3283        );
3284
3285        assert_eq!(
3286            selection!("$batch.id->map([@])->echo([@])", spec)
3287                .shape()
3288                .pretty_print(),
3289            "Unknown",
3290        );
3291
3292        assert_eq!(
3293            selection!("$batch.id->map([@])->echo([@])", spec)
3294                .compute_output_shape(&shape_context, root_shape,)
3295                .pretty_print(),
3296            "Unknown",
3297        );
3298    }
3299
3300    #[test]
3301    fn test_left_associative_output_shapes_v0_3() {
3302        let spec = ConnectSpec::V0_3;
3303
3304        assert_eq!(
3305            selection!("$batch.id", spec).shape().pretty_print(),
3306            "$batch.id"
3307        );
3308
3309        assert_eq!(
3310            selection!("$batch.id->first", spec).shape().pretty_print(),
3311            "$batch.id.0",
3312        );
3313
3314        assert_eq!(
3315            selection!("$batch.id->last", spec).shape().pretty_print(),
3316            "$batch.id.*",
3317        );
3318
3319        let mut named_shapes = IndexMap::default();
3320        named_shapes.insert(
3321            "$batch".to_string(),
3322            Shape::list(
3323                Shape::record(
3324                    {
3325                        let mut map = Shape::empty_map();
3326                        map.insert("id".to_string(), Shape::int([]));
3327                        map
3328                    },
3329                    [],
3330                ),
3331                [],
3332            ),
3333        );
3334
3335        let root_shape = Shape::name("$root", []);
3336        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3337            .with_spec(spec)
3338            .with_named_shapes(named_shapes.clone());
3339
3340        let computed_batch_id =
3341            selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3342        assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3343
3344        let computed_first = selection!("$batch.id->first", spec)
3345            .compute_output_shape(&shape_context, root_shape.clone());
3346        assert_eq!(computed_first.pretty_print(), "One<Int, None>");
3347
3348        let computed_last = selection!("$batch.id->last", spec)
3349            .compute_output_shape(&shape_context, root_shape.clone());
3350        assert_eq!(computed_last.pretty_print(), "One<Int, None>");
3351
3352        assert_eq!(
3353            selection!("$batch.id->jsonStringify", spec)
3354                .shape()
3355                .pretty_print(),
3356            "String",
3357        );
3358
3359        assert_eq!(
3360            selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3361                .shape()
3362                .pretty_print(),
3363            "String",
3364        );
3365
3366        assert_eq!(
3367            selection!("$batch.id->map(@)->echo(@)", spec)
3368                .shape()
3369                .pretty_print(),
3370            "List<$batch.id.*>",
3371        );
3372
3373        assert_eq!(
3374            selection!("$batch.id->map(@)->echo([@])", spec)
3375                .shape()
3376                .pretty_print(),
3377            "[List<$batch.id.*>]",
3378        );
3379
3380        assert_eq!(
3381            selection!("$batch.id->map([@])->echo(@)", spec)
3382                .shape()
3383                .pretty_print(),
3384            "List<[$batch.id.*]>",
3385        );
3386
3387        assert_eq!(
3388            selection!("$batch.id->map([@])->echo([@])", spec)
3389                .shape()
3390                .pretty_print(),
3391            "[List<[$batch.id.*]>]",
3392        );
3393
3394        assert_eq!(
3395            selection!("$batch.id->map([@])->echo([@])", spec)
3396                .compute_output_shape(&shape_context, root_shape,)
3397                .pretty_print(),
3398            "[List<[Int]>]",
3399        );
3400    }
3401
3402    #[test]
3403    fn test_lit_paths() {
3404        let data = json!({
3405            "value": {
3406                "key": 123,
3407            },
3408        });
3409
3410        assert_eq!(
3411            selection!("$(\"a\")->first").apply_to(&data),
3412            (Some(json!("a")), vec![]),
3413        );
3414
3415        assert_eq!(
3416            selection!("$('asdf'->last)").apply_to(&data),
3417            (Some(json!("f")), vec![]),
3418        );
3419
3420        assert_eq!(
3421            selection!("$(1234)->add(1111)").apply_to(&data),
3422            (Some(json!(2345)), vec![]),
3423        );
3424
3425        assert_eq!(
3426            selection!("$(1234->add(1111))").apply_to(&data),
3427            (Some(json!(2345)), vec![]),
3428        );
3429
3430        assert_eq!(
3431            selection!("$(value.key->mul(10))").apply_to(&data),
3432            (Some(json!(1230)), vec![]),
3433        );
3434
3435        assert_eq!(
3436            selection!("$(value.key)->mul(10)").apply_to(&data),
3437            (Some(json!(1230)), vec![]),
3438        );
3439
3440        assert_eq!(
3441            selection!("$(value.key->typeof)").apply_to(&data),
3442            (Some(json!("number")), vec![]),
3443        );
3444
3445        assert_eq!(
3446            selection!("$(value.key)->typeof").apply_to(&data),
3447            (Some(json!("number")), vec![]),
3448        );
3449
3450        assert_eq!(
3451            selection!("$([1, 2, 3])->last").apply_to(&data),
3452            (Some(json!(3)), vec![]),
3453        );
3454
3455        assert_eq!(
3456            selection!("$([1, 2, 3]->first)").apply_to(&data),
3457            (Some(json!(1)), vec![]),
3458        );
3459
3460        assert_eq!(
3461            selection!("$({ a: 'ay', b: 1 }).a").apply_to(&data),
3462            (Some(json!("ay")), vec![]),
3463        );
3464
3465        assert_eq!(
3466            selection!("$({ a: 'ay', b: 2 }.a)").apply_to(&data),
3467            (Some(json!("ay")), vec![]),
3468        );
3469
3470        assert_eq!(
3471            // Note that the -> has lower precedence than the -, so -1 is parsed
3472            // as a completed expression before applying the ->add(10) method,
3473            // giving 9 instead of -11.
3474            selection!("$(-1->add(10))").apply_to(&data),
3475            (Some(json!(9)), vec![]),
3476        );
3477    }
3478
3479    #[test]
3480    fn test_compute_output_shape() {
3481        assert_eq!(selection!("").shape().pretty_print(), "{}");
3482
3483        assert_eq!(
3484            selection!("id name").shape().pretty_print(),
3485            "{ id: $root.*.id, name: $root.*.name }",
3486        );
3487
3488        // // On hold until variadic $(...) is merged (PR #6456).
3489        // assert_eq!(
3490        //     selection!("$.data { thisOrThat: $(maybe.this, maybe.that) }")
3491        //         .shape()
3492        //         .pretty_print(),
3493        //     // Technically $.data could be an array, so this should be a union
3494        //     // of this shape and a list of this shape, except with
3495        //     // $root.data.0.maybe.{this,that} shape references.
3496        //     //
3497        //     // We could try to say that any { ... } shape represents either an
3498        //     // object or a list of objects, by policy, to avoid having to write
3499        //     // One<{...}, List<{...}>> everywhere a SubSelection appears.
3500        //     //
3501        //     // But then we don't know where the array indexes should go...
3502        //     "{ thisOrThat: One<$root.data.*.maybe.this, $root.data.*.maybe.that> }",
3503        // );
3504
3505        assert_eq!(
3506            selection!(
3507                r#"
3508                id
3509                name
3510                friends: friend_ids { id: @ }
3511                alias: arrayOfArrays { x y }
3512                ys: arrayOfArrays.y xs: arrayOfArrays.x
3513            "#
3514            )
3515            .shape()
3516            .pretty_print(),
3517            // This output shape is wrong if $root.friend_ids turns out to be an
3518            // array, and it's tricky to see how to transform the shape to what
3519            // it would have been if we knew that, where friends: List<{ id:
3520            // $root.friend_ids.* }> (note the * meaning any array index),
3521            // because who's to say it's not the id field that should become the
3522            // List, rather than the friends field?
3523            "{ alias: { x: $root.*.arrayOfArrays.*.x, y: $root.*.arrayOfArrays.*.y }, friends: { id: $root.*.friend_ids.* }, id: $root.*.id, name: $root.*.name, xs: $root.*.arrayOfArrays.x, ys: $root.*.arrayOfArrays.y }",
3524        );
3525
3526        // TODO: re-test when method type checking is re-enabled
3527        // assert_eq!(
3528        //     selection!(r#"
3529        //         id
3530        //         name
3531        //         friends: friend_ids->map({ id: @ })
3532        //         alias: arrayOfArrays { x y }
3533        //         ys: arrayOfArrays.y xs: arrayOfArrays.x
3534        //     "#).shape().pretty_print(),
3535        //     "{ alias: { x: $root.*.arrayOfArrays.*.x, y: $root.*.arrayOfArrays.*.y }, friends: List<{ id: $root.*.friend_ids.* }>, id: $root.*.id, name: $root.*.name, xs: $root.*.arrayOfArrays.x, ys: $root.*.arrayOfArrays.y }",
3536        // );
3537        //
3538        // assert_eq!(
3539        //     selection!("$->echo({ thrice: [@, @, @] })")
3540        //         .shape()
3541        //         .pretty_print(),
3542        //     "{ thrice: [$root, $root, $root] }",
3543        // );
3544        //
3545        // assert_eq!(
3546        //     selection!("$->echo({ thrice: [@, @, @] })->entries")
3547        //         .shape()
3548        //         .pretty_print(),
3549        //     "[{ key: \"thrice\", value: [$root, $root, $root] }]",
3550        // );
3551        //
3552        // assert_eq!(
3553        //     selection!("$->echo({ thrice: [@, @, @] })->entries.key")
3554        //         .shape()
3555        //         .pretty_print(),
3556        //     "[\"thrice\"]",
3557        // );
3558        //
3559        // assert_eq!(
3560        //     selection!("$->echo({ thrice: [@, @, @] })->entries.value")
3561        //         .shape()
3562        //         .pretty_print(),
3563        //     "[[$root, $root, $root]]",
3564        // );
3565        //
3566        // assert_eq!(
3567        //     selection!("$->echo({ wrapped: @ })->entries { k: key v: value }")
3568        //         .shape()
3569        //         .pretty_print(),
3570        //     "[{ k: \"wrapped\", v: $root }]",
3571        // );
3572    }
3573
3574    #[rstest]
3575    #[case::v0_3(ConnectSpec::V0_3)]
3576    fn test_optional_key_access_with_existing_property(#[case] spec: ConnectSpec) {
3577        use serde_json_bytes::json;
3578
3579        let data = json!({
3580            "user": {
3581                "profile": {
3582                    "name": "Alice"
3583                }
3584            }
3585        });
3586
3587        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3588            .unwrap()
3589            .apply_to(&data);
3590        assert!(errors.is_empty());
3591        assert_eq!(result, Some(json!("Alice")));
3592    }
3593
3594    #[rstest]
3595    #[case::v0_3(ConnectSpec::V0_3)]
3596    fn test_optional_key_access_with_null_value(#[case] spec: ConnectSpec) {
3597        use serde_json_bytes::json;
3598
3599        let data_null = json!({
3600            "user": null
3601        });
3602
3603        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3604            .unwrap()
3605            .apply_to(&data_null);
3606        assert!(errors.is_empty());
3607        assert_eq!(result, None);
3608    }
3609
3610    #[rstest]
3611    #[case::v0_3(ConnectSpec::V0_3)]
3612    fn test_optional_key_access_on_non_object(#[case] spec: ConnectSpec) {
3613        use serde_json_bytes::json;
3614
3615        let data_non_obj = json!({
3616            "user": "not an object"
3617        });
3618
3619        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3620            .unwrap()
3621            .apply_to(&data_non_obj);
3622        assert_eq!(errors.len(), 1);
3623        assert!(
3624            errors[0]
3625                .message()
3626                .contains("Property .profile not found in string")
3627        );
3628        assert_eq!(result, None);
3629    }
3630
3631    #[rstest]
3632    #[case::v0_3(ConnectSpec::V0_3)]
3633    fn test_optional_key_access_with_missing_property(#[case] spec: ConnectSpec) {
3634        use serde_json_bytes::json;
3635
3636        let data = json!({
3637            "user": {
3638                "other": "value"
3639            }
3640        });
3641
3642        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3643            .unwrap()
3644            .apply_to(&data);
3645        assert_eq!(errors.len(), 1);
3646        assert!(
3647            errors[0]
3648                .message()
3649                .contains("Property .profile not found in object")
3650        );
3651        assert_eq!(result, None);
3652    }
3653
3654    #[rstest]
3655    #[case::v0_3(ConnectSpec::V0_3)]
3656    fn test_chained_optional_key_access(#[case] spec: ConnectSpec) {
3657        use serde_json_bytes::json;
3658
3659        let data = json!({
3660            "user": {
3661                "profile": {
3662                    "name": "Alice"
3663                }
3664            }
3665        });
3666
3667        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
3668            .unwrap()
3669            .apply_to(&data);
3670        assert!(errors.is_empty());
3671        assert_eq!(result, Some(json!("Alice")));
3672    }
3673
3674    #[rstest]
3675    #[case::v0_3(ConnectSpec::V0_3)]
3676    fn test_chained_optional_access_with_null_in_middle(#[case] spec: ConnectSpec) {
3677        use serde_json_bytes::json;
3678
3679        let data_partial_null = json!({
3680            "user": {
3681                "profile": null
3682            }
3683        });
3684
3685        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
3686            .unwrap()
3687            .apply_to(&data_partial_null);
3688        assert!(errors.is_empty());
3689        assert_eq!(result, None);
3690    }
3691
3692    #[rstest]
3693    #[case::v0_3(ConnectSpec::V0_3)]
3694    fn test_optional_method_on_null(#[case] spec: ConnectSpec) {
3695        use serde_json_bytes::json;
3696
3697        let data = json!({
3698            "items": null
3699        });
3700
3701        let (result, errors) = JSONSelection::parse_with_spec("$.items?->first", spec)
3702            .unwrap()
3703            .apply_to(&data);
3704        assert!(errors.is_empty());
3705        assert_eq!(result, None);
3706    }
3707
3708    #[rstest]
3709    #[case::v0_3(ConnectSpec::V0_3)]
3710    fn test_optional_method_with_valid_method(#[case] spec: ConnectSpec) {
3711        use serde_json_bytes::json;
3712
3713        let data = json!({
3714            "values": [1, 2, 3]
3715        });
3716
3717        let (result, errors) = JSONSelection::parse_with_spec("$.values?->first", spec)
3718            .unwrap()
3719            .apply_to(&data);
3720        assert!(errors.is_empty());
3721        assert_eq!(result, Some(json!(1)));
3722    }
3723
3724    #[rstest]
3725    #[case::v0_3(ConnectSpec::V0_3)]
3726    fn test_optional_method_with_unknown_method(#[case] spec: ConnectSpec) {
3727        use serde_json_bytes::json;
3728
3729        let data = json!({
3730            "values": [1, 2, 3]
3731        });
3732
3733        let (result, errors) = JSONSelection::parse_with_spec("$.values?->length", spec)
3734            .unwrap()
3735            .apply_to(&data);
3736        assert_eq!(errors.len(), 1);
3737        assert!(errors[0].message().contains("Method ->length not found"));
3738        assert_eq!(result, None);
3739    }
3740
3741    #[rstest]
3742    #[case::v0_3(ConnectSpec::V0_3)]
3743    fn test_optional_chaining_with_subselection_on_valid_data(#[case] spec: ConnectSpec) {
3744        use serde_json_bytes::json;
3745
3746        let data = json!({
3747            "user": {
3748                "profile": {
3749                    "name": "Alice",
3750                    "age": 30,
3751                    "email": "alice@example.com"
3752                }
3753            }
3754        });
3755
3756        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
3757            .unwrap()
3758            .apply_to(&data);
3759        assert!(errors.is_empty());
3760        assert_eq!(
3761            result,
3762            Some(json!({
3763                "name": "Alice",
3764                "age": 30
3765            }))
3766        );
3767    }
3768
3769    #[rstest]
3770    #[case::v0_3(ConnectSpec::V0_3)]
3771    fn test_optional_chaining_with_subselection_on_null_data(#[case] spec: ConnectSpec) {
3772        use serde_json_bytes::json;
3773
3774        let data_null = json!({
3775            "user": null
3776        });
3777
3778        let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
3779            .unwrap()
3780            .apply_to(&data_null);
3781        assert!(errors.is_empty());
3782        assert_eq!(result, None);
3783    }
3784
3785    #[rstest]
3786    #[case::v0_3(ConnectSpec::V0_3)]
3787    fn test_mixed_regular_and_optional_chaining_working_case(#[case] spec: ConnectSpec) {
3788        use serde_json_bytes::json;
3789
3790        let data = json!({
3791            "response": {
3792                "data": {
3793                    "user": {
3794                        "profile": {
3795                            "name": "Bob"
3796                        }
3797                    }
3798                }
3799            }
3800        });
3801
3802        let (result, errors) =
3803            JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
3804                .unwrap()
3805                .apply_to(&data);
3806        assert!(errors.is_empty());
3807        assert_eq!(result, Some(json!("Bob")));
3808    }
3809
3810    #[rstest]
3811    #[case::v0_3(ConnectSpec::V0_3)]
3812    fn test_mixed_regular_and_optional_chaining_with_null(#[case] spec: ConnectSpec) {
3813        use serde_json_bytes::json;
3814
3815        let data_null_data = json!({
3816            "response": {
3817                "data": null
3818            }
3819        });
3820
3821        let (result, errors) =
3822            JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
3823                .unwrap()
3824                .apply_to(&data_null_data);
3825        assert!(errors.is_empty());
3826        assert_eq!(result, None);
3827    }
3828
3829    #[rstest]
3830    #[case::v0_3(ConnectSpec::V0_3)]
3831    fn test_optional_selection_set_with_valid_data(#[case] spec: ConnectSpec) {
3832        use serde_json_bytes::json;
3833
3834        let data = json!({
3835            "user": {
3836                "id": 123,
3837                "name": "Alice"
3838            }
3839        });
3840
3841        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3842            .unwrap()
3843            .apply_to(&data);
3844        assert_eq!(
3845            result,
3846            Some(json!({
3847                "id": 123,
3848                "name": "Alice"
3849            }))
3850        );
3851        assert_eq!(errors, vec![]);
3852    }
3853
3854    #[rstest]
3855    #[case::v0_3(ConnectSpec::V0_3)]
3856    fn test_optional_selection_set_with_null_data(#[case] spec: ConnectSpec) {
3857        use serde_json_bytes::json;
3858
3859        let data = json!({
3860            "user": null
3861        });
3862
3863        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3864            .unwrap()
3865            .apply_to(&data);
3866        assert_eq!(result, None);
3867        assert_eq!(errors, vec![]);
3868    }
3869
3870    #[rstest]
3871    #[case::v0_3(ConnectSpec::V0_3)]
3872    fn test_optional_selection_set_with_missing_property(#[case] spec: ConnectSpec) {
3873        use serde_json_bytes::json;
3874
3875        let data = json!({
3876            "other": "value"
3877        });
3878
3879        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3880            .unwrap()
3881            .apply_to(&data);
3882        assert_eq!(result, None);
3883        assert_eq!(errors.len(), 0);
3884    }
3885
3886    #[rstest]
3887    #[case::v0_3(ConnectSpec::V0_3)]
3888    fn test_optional_selection_set_with_non_object(#[case] spec: ConnectSpec) {
3889        use serde_json_bytes::json;
3890
3891        let data = json!({
3892            "user": "not an object"
3893        });
3894
3895        let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3896            .unwrap()
3897            .apply_to(&data);
3898        // When data is not null but not an object, SubSelection still tries to access properties
3899        // This results in errors, but returns the original value since no properties were found
3900        assert_eq!(result, Some(json!("not an object")));
3901        assert_eq!(errors.len(), 2);
3902        assert!(
3903            errors[0]
3904                .message()
3905                .contains("Property .id not found in string")
3906        );
3907        assert!(
3908            errors[1]
3909                .message()
3910                .contains("Property .name not found in string")
3911        );
3912    }
3913
3914    #[test]
3915    fn test_optional_field_selections() {
3916        let spec = ConnectSpec::V0_3;
3917        let author_selection = selection!("author? { age middleName? }", spec);
3918        assert_debug_snapshot!(author_selection);
3919        assert_eq!(
3920            author_selection.pretty_print(),
3921            "author? { age middleName? }",
3922        );
3923        assert_eq!(
3924            author_selection.shape().pretty_print(),
3925            "{ author: One<{ age: $root.*.author?.*.age, middleName: $root.*.author?.*.middleName? }, None> }",
3926        );
3927    }
3928
3929    #[test]
3930    fn test_optional_input_shape_with_selection() {
3931        let spec = ConnectSpec::V0_3;
3932        let optional_author_shape_selection =
3933            selection!("unreliableAuthor { age middleName? }", spec);
3934
3935        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3936            .with_spec(spec)
3937            .with_named_shapes([(
3938                "$root".to_string(),
3939                Shape::record(
3940                    {
3941                        let mut map = Shape::empty_map();
3942                        map.insert(
3943                            "unreliableAuthor".to_string(),
3944                            Shape::one(
3945                                [
3946                                    Shape::record(
3947                                        {
3948                                            let mut map = Shape::empty_map();
3949                                            map.insert("age".to_string(), Shape::int([]));
3950                                            map.insert(
3951                                                "middleName".to_string(),
3952                                                Shape::one([Shape::string([]), Shape::none()], []),
3953                                            );
3954                                            map
3955                                        },
3956                                        [],
3957                                    ),
3958                                    Shape::none(),
3959                                ],
3960                                [],
3961                            ),
3962                        );
3963                        map
3964                    },
3965                    [],
3966                ),
3967            )]);
3968
3969        assert_eq!(
3970            optional_author_shape_selection
3971                .compute_output_shape(
3972                    &shape_context,
3973                    shape_context.named_shapes().get("$root").unwrap().clone(),
3974                )
3975                .pretty_print(),
3976            "{ unreliableAuthor: One<{ age: Int, middleName: One<String, None> }, None> }",
3977        );
3978    }
3979
3980    // TODO Reenable these tests in ConnectSpec::V0_4 when we support ... spread
3981    // syntax and abstract typs.
3982    /** #[cfg(test)]
3983    mod spread {
3984        use serde_json_bytes::Value as JSON;
3985        use serde_json_bytes::json;
3986        use shape::Shape;
3987        use shape::location::SourceId;
3988
3989        use crate::connectors::ConnectSpec;
3990        use crate::connectors::json_selection::ShapeContext;
3991
3992        #[derive(Debug)]
3993        pub(super) struct SetupItems {
3994            pub data: JSON,
3995            pub shape_context: ShapeContext,
3996            pub root_shape: Shape,
3997        }
3998
3999        pub(super) fn setup(spec: ConnectSpec) -> SetupItems {
4000            let a_b_data = json!({
4001                "a": { "phonetic": "ay" },
4002                "b": { "phonetic": "bee" },
4003            });
4004
4005            let a_b_data_shape = Shape::from_json_bytes(&a_b_data);
4006
4007            let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
4008                .with_spec(spec)
4009                .with_named_shapes([("$root".to_string(), a_b_data_shape)]);
4010
4011            let root_shape = shape_context.named_shapes().get("$root").unwrap().clone();
4012
4013            SetupItems {
4014                data: a_b_data,
4015                shape_context,
4016                root_shape,
4017            }
4018        }
4019    }
4020
4021    #[test]
4022    fn test_spread_syntax_spread_a() {
4023        let spec = ConnectSpec::V0_4;
4024        let spread::SetupItems {
4025            data: a_b_data,
4026            shape_context,
4027            root_shape,
4028        } = spread::setup(spec);
4029
4030        let spread_a = selection!("...a", spec);
4031        assert_eq!(
4032            spread_a.apply_to(&a_b_data),
4033            (Some(json!({"phonetic": "ay"})), vec![]),
4034        );
4035        assert_eq!(spread_a.shape().pretty_print(), "$root.*.a",);
4036        assert_eq!(
4037            spread_a
4038                .compute_output_shape(&shape_context, root_shape)
4039                .pretty_print(),
4040            "{ phonetic: \"ay\" }",
4041        );
4042    }
4043    **/
4044
4045    #[rstest]
4046    #[case::v0_3(ConnectSpec::V0_3)]
4047    fn test_nested_optional_selection_sets(#[case] spec: ConnectSpec) {
4048        use serde_json_bytes::json;
4049
4050        let data = json!({
4051            "user": {
4052                "profile": {
4053                    "name": "Alice",
4054                    "email": "alice@example.com"
4055                }
4056            }
4057        });
4058
4059        let (result, errors) =
4060            JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4061                .unwrap()
4062                .apply_to(&data);
4063        assert_eq!(
4064            result,
4065            Some(json!({
4066                "name": "Alice",
4067                "email": "alice@example.com"
4068            }))
4069        );
4070        assert_eq!(errors, vec![]);
4071
4072        // Test with null nested data
4073        let data_with_null_profile = json!({
4074            "user": {
4075                "profile": null
4076            }
4077        });
4078
4079        let (result, errors) =
4080            JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4081                .unwrap()
4082                .apply_to(&data_with_null_profile);
4083        assert_eq!(result, None);
4084        assert_eq!(errors, vec![]);
4085    }
4086
4087    #[rstest]
4088    #[case::v0_3(ConnectSpec::V0_3)]
4089    fn test_mixed_optional_selection_and_optional_chaining(#[case] spec: ConnectSpec) {
4090        use serde_json_bytes::json;
4091
4092        let data = json!({
4093            "user": {
4094                "id": 123,
4095                "profile": null
4096            }
4097        });
4098
4099        let (result, errors) =
4100            JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4101                .unwrap()
4102                .apply_to(&data);
4103        assert_eq!(
4104            result,
4105            Some(json!({
4106                "id": 123
4107            }))
4108        );
4109        assert_eq!(errors, vec![]);
4110
4111        // Test with missing user
4112        let data_no_user = json!({
4113            "other": "value"
4114        });
4115
4116        let (result, errors) =
4117            JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4118                .unwrap()
4119                .apply_to(&data_no_user);
4120        assert_eq!(result, None);
4121        assert_eq!(errors.len(), 0);
4122    }
4123
4124    #[rstest]
4125    #[case::v0_3(ConnectSpec::V0_3)]
4126    fn test_optional_selection_set_parsing(#[case] spec: ConnectSpec) {
4127        // Test that the parser correctly handles optional selection sets
4128        let selection = JSONSelection::parse_with_spec("$.user? { id name }", spec).unwrap();
4129        assert_eq!(selection.pretty_print(), "$.user? { id name }");
4130
4131        // Test with nested optional selection sets
4132        let selection = JSONSelection::parse_with_spec("$.user.profile? { name }", spec).unwrap();
4133        assert_eq!(selection.pretty_print(), "$.user.profile? { name }");
4134
4135        // Test mixed with regular selection sets
4136        let selection =
4137            JSONSelection::parse_with_spec("$.user? { id profile { name } }", spec).unwrap();
4138        assert_eq!(selection.pretty_print(), "$.user? { id profile { name } }");
4139    }
4140
4141    #[rstest]
4142    #[case::v0_3(ConnectSpec::V0_3)]
4143    fn test_optional_selection_set_with_arrays(#[case] spec: ConnectSpec) {
4144        use serde_json_bytes::json;
4145
4146        let data = json!({
4147            "users": [
4148                {
4149                    "id": 1,
4150                    "name": "Alice"
4151                },
4152                null,
4153                {
4154                    "id": 3,
4155                    "name": "Charlie"
4156                }
4157            ]
4158        });
4159
4160        let (result, errors) = JSONSelection::parse_with_spec("$.users ?{ id name }", spec)
4161            .unwrap()
4162            .apply_to(&data);
4163        assert_eq!(
4164            result,
4165            Some(json!([
4166                {
4167                    "id": 1,
4168                    "name": "Alice"
4169                },
4170                null,
4171                {
4172                    "id": 3,
4173                    "name": "Charlie"
4174                }
4175            ]))
4176        );
4177
4178        assert_eq!(errors.len(), 2);
4179        assert!(
4180            errors[0]
4181                .message()
4182                .contains("Property .id not found in null")
4183        );
4184        assert!(
4185            errors[1]
4186                .message()
4187                .contains("Property .name not found in null")
4188        );
4189    }
4190
4191    // TODO Reenable these tests in ConnectSpec::V0_4 when we support ... spread
4192    // syntax and abstract types.
4193    /**
4194    #[test]
4195    fn test_spread_syntax_a_spread_b() {
4196        let spec = ConnectSpec::V0_4;
4197        let spread::SetupItems {
4198            data: a_b_data,
4199            shape_context,
4200            root_shape,
4201        } = spread::setup(spec);
4202
4203        let a_spread_b = selection!("a...b", spec);
4204        assert_eq!(
4205            a_spread_b.apply_to(&a_b_data),
4206            (
4207                Some(json!({"a": { "phonetic": "ay" }, "phonetic": "bee" })),
4208                vec![]
4209            ),
4210        );
4211        assert_eq!(
4212            a_spread_b.shape().pretty_print(),
4213            "All<$root.*.b, { a: $root.*.a }>",
4214        );
4215        assert_eq!(
4216            a_spread_b
4217                .compute_output_shape(&shape_context, root_shape)
4218                .pretty_print(),
4219            "{ a: { phonetic: \"ay\" }, phonetic: \"bee\" }",
4220        );
4221    }
4222
4223    #[test]
4224    fn test_spread_syntax_spread_a_b() {
4225        let spec = ConnectSpec::V0_4;
4226        let spread::SetupItems {
4227            data: a_b_data,
4228            shape_context,
4229            root_shape,
4230        } = spread::setup(spec);
4231
4232        let spread_a_b = selection!("...a b", spec);
4233        assert_eq!(
4234            spread_a_b.apply_to(&a_b_data),
4235            (
4236                Some(json!({"phonetic": "ay", "b": { "phonetic": "bee" }})),
4237                vec![]
4238            ),
4239        );
4240        assert_eq!(
4241            spread_a_b.shape().pretty_print(),
4242            "All<$root.*.a, { b: $root.*.b }>",
4243        );
4244        assert_eq!(
4245            spread_a_b
4246                .compute_output_shape(&shape_context, root_shape)
4247                .pretty_print(),
4248            "{ b: { phonetic: \"bee\" }, phonetic: \"ay\" }",
4249        );
4250    }
4251
4252    #[test]
4253    fn test_spread_match_none() {
4254        let spec = ConnectSpec::V0_4;
4255
4256        let sel = selection!(
4257            "before ...condition->match([true, { matched: true }]) after",
4258            spec
4259        );
4260        assert_eq!(
4261            sel.shape().pretty_print(),
4262            "One<{ after: $root.*.after, before: $root.*.before, matched: true }, { after: $root.*.after, before: $root.*.before }>",
4263        );
4264
4265        assert_eq!(
4266            sel.apply_to(&json!({
4267                "before": "before value",
4268                "after": "after value",
4269                "condition": true,
4270            })),
4271            (
4272                Some(json!({
4273                    "before": "before value",
4274                    "after": "after value",
4275                    "matched": true,
4276                })),
4277                vec![],
4278            ),
4279        );
4280
4281        assert_eq!(
4282            sel.apply_to(&json!({
4283                "before": "before value",
4284                "after": "after value",
4285                "condition": false,
4286            })),
4287            (
4288                Some(json!({
4289                    "before": "before value",
4290                    "after": "after value",
4291                })),
4292                vec![
4293                    ApplyToError::new(
4294                        "Method ->match did not match any [candidate, value] pair".to_string(),
4295                        vec![json!("condition"), json!("->match")],
4296                        Some(21..53),
4297                        spec,
4298                    ),
4299                    ApplyToError::new(
4300                        "Inlined path produced no value".to_string(),
4301                        vec![],
4302                        Some(10..53),
4303                        spec,
4304                    )
4305                ],
4306            ),
4307        );
4308    }
4309
4310    #[cfg(test)]
4311    mod spread_with_match {
4312        use crate::connectors::ConnectSpec;
4313        use crate::connectors::JSONSelection;
4314        use crate::selection;
4315
4316        pub(super) fn get_selection(spec: ConnectSpec) -> JSONSelection {
4317            let sel = selection!(
4318                r#"
4319                upc
4320                ... type->match(
4321                    ["book", {
4322                        __typename: "Book",
4323                        title: title,
4324                        author: { name: author.name },
4325                    }],
4326                    ["movie", {
4327                        __typename: "Movie",
4328                        title: title,
4329                        director: director.name,
4330                    }],
4331                    ["magazine", {
4332                        __typename: "Magazine",
4333                        title: title,
4334                        editor: editor.name,
4335                    }],
4336                    ["dummy", {}],
4337                    [@, null],
4338                )
4339                "#,
4340                spec
4341            );
4342
4343            assert_eq!(
4344                sel.shape().pretty_print(),
4345                // An upcoming Shape library update should improve the readability
4346                // of this pretty printing considerably.
4347                "One<{ __typename: \"Book\", author: { name: $root.*.author.name }, title: $root.*.title, upc: $root.*.upc }, { __typename: \"Movie\", director: $root.*.director.name, title: $root.*.title, upc: $root.*.upc }, { __typename: \"Magazine\", editor: $root.*.editor.name, title: $root.*.title, upc: $root.*.upc }, { upc: $root.*.upc }, null>"
4348            );
4349
4350            sel
4351        }
4352    }
4353
4354    #[test]
4355    fn test_spread_with_match_book() {
4356        let spec = ConnectSpec::V0_4;
4357        let sel = spread_with_match::get_selection(spec);
4358
4359        let book_data = json!({
4360            "upc": "1234567890",
4361            "type": "book",
4362            "title": "The Great Gatsby",
4363            "author": { "name": "F. Scott Fitzgerald" },
4364        });
4365        assert_eq!(
4366            sel.apply_to(&book_data),
4367            (
4368                Some(json!({
4369                    "__typename": "Book",
4370                    "upc": "1234567890",
4371                    "title": "The Great Gatsby",
4372                    "author": { "name": "F. Scott Fitzgerald" },
4373                })),
4374                vec![],
4375            ),
4376        );
4377    }
4378
4379    #[test]
4380    fn test_spread_with_match_movie() {
4381        let spec = ConnectSpec::V0_4;
4382        let sel = spread_with_match::get_selection(spec);
4383
4384        let movie_data = json!({
4385            "upc": "0987654321",
4386            "type": "movie",
4387            "title": "Inception",
4388            "director": { "name": "Christopher Nolan" },
4389        });
4390        assert_eq!(
4391            sel.apply_to(&movie_data),
4392            (
4393                Some(json!({
4394                    "__typename": "Movie",
4395                    "upc": "0987654321",
4396                    "title": "Inception",
4397                    "director": "Christopher Nolan",
4398                })),
4399                vec![],
4400            ),
4401        );
4402    }
4403
4404    #[test]
4405    fn test_spread_with_match_magazine() {
4406        let spec = ConnectSpec::V0_4;
4407        let sel = spread_with_match::get_selection(spec);
4408
4409        let magazine_data = json!({
4410            "upc": "1122334455",
4411            "type": "magazine",
4412            "title": "National Geographic",
4413            "editor": { "name": "Susan Goldberg" },
4414        });
4415        assert_eq!(
4416            sel.apply_to(&magazine_data),
4417            (
4418                Some(json!({
4419                    "__typename": "Magazine",
4420                    "upc": "1122334455",
4421                    "title": "National Geographic",
4422                    "editor": "Susan Goldberg",
4423                })),
4424                vec![],
4425            ),
4426        );
4427    }
4428
4429    #[test]
4430    fn test_spread_with_match_dummy() {
4431        let spec = ConnectSpec::V0_4;
4432        let sel = spread_with_match::get_selection(spec);
4433
4434        let dummy_data = json!({
4435            "upc": "5566778899",
4436            "type": "dummy",
4437        });
4438        assert_eq!(
4439            sel.apply_to(&dummy_data),
4440            (
4441                Some(json!({
4442                    "upc": "5566778899",
4443                })),
4444                vec![],
4445            ),
4446        );
4447    }
4448
4449    #[test]
4450    fn test_spread_with_match_unknown() {
4451        let spec = ConnectSpec::V0_4;
4452        let sel = spread_with_match::get_selection(spec);
4453
4454        let unknown_data = json!({
4455            "upc": "9988776655",
4456            "type": "music",
4457            "title": "The White Stripes",
4458            "artist": { "name": "Jack White" },
4459        });
4460        assert_eq!(sel.apply_to(&unknown_data), (Some(json!(null)), vec![]));
4461    }
4462
4463    #[test]
4464    fn test_spread_null() {
4465        let spec = ConnectSpec::V0_4;
4466        assert_eq!(
4467            selection!("...$(null)", spec).apply_to(&json!({ "ignored": "data" })),
4468            (Some(json!(null)), vec![]),
4469        );
4470        assert_eq!(
4471            selection!("ignored ...$(null)", spec).apply_to(&json!({ "ignored": "data" })),
4472            (Some(json!(null)), vec![]),
4473        );
4474        assert_eq!(
4475            selection!("...$(null) ignored", spec).apply_to(&json!({ "ignored": "data" })),
4476            (Some(json!(null)), vec![]),
4477        );
4478        assert_eq!(
4479            selection!("group: { a ...b }", spec).apply_to(&json!({ "a": "ay", "b": null })),
4480            (Some(json!({ "group": null })), vec![]),
4481        );
4482    }
4483
4484    #[test]
4485    fn test_spread_missing() {
4486        let spec = ConnectSpec::V0_4;
4487
4488        assert_eq!(
4489            selection!("a ...missing z", spec).apply_to(&json!({ "a": "ay", "z": "zee" })),
4490            (
4491                Some(json!({
4492                    "a": "ay",
4493                    "z": "zee",
4494                })),
4495                vec![
4496                    ApplyToError::new(
4497                        "Property .missing not found in object".to_string(),
4498                        vec![json!("missing")],
4499                        Some(5..12),
4500                        spec,
4501                    ),
4502                    ApplyToError::new(
4503                        "Inlined path produced no value".to_string(),
4504                        vec![],
4505                        Some(5..12),
4506                        spec,
4507                    ),
4508                ],
4509            ),
4510        );
4511
4512        assert_eq!(
4513            selection!("a ...$(missing) z", spec).apply_to(&json!({ "a": "ay", "z": "zee" })),
4514            (
4515                Some(json!({
4516                    "a": "ay",
4517                    "z": "zee",
4518                })),
4519                vec![
4520                    ApplyToError::new(
4521                        "Property .missing not found in object".to_string(),
4522                        vec![json!("missing")],
4523                        Some(7..14),
4524                        spec,
4525                    ),
4526                    ApplyToError::new(
4527                        "Inlined path produced no value".to_string(),
4528                        vec![],
4529                        Some(5..15),
4530                        spec,
4531                    ),
4532                ],
4533            ),
4534        );
4535    }
4536
4537    #[test]
4538    fn test_spread_invalid_numbers() {
4539        let spec = ConnectSpec::V0_4;
4540
4541        assert_eq!(
4542            selection!("...invalid", spec).apply_to(&json!({ "invalid": 123 })),
4543            (
4544                Some(json!({})),
4545                vec![ApplyToError::new(
4546                    "Expected object or null, not number".to_string(),
4547                    vec![],
4548                    Some(3..10),
4549                    spec,
4550                )],
4551            ),
4552        );
4553
4554        assert_eq!(
4555            selection!(" ... $( invalid ) ", spec).apply_to(&json!({ "invalid": 234 })),
4556            (
4557                Some(json!({})),
4558                vec![ApplyToError::new(
4559                    "Expected object or null, not number".to_string(),
4560                    vec![],
4561                    Some(5..17),
4562                    spec,
4563                )],
4564            ),
4565        );
4566    }
4567
4568    #[test]
4569    fn test_spread_invalid_bools() {
4570        let spec = ConnectSpec::V0_4;
4571
4572        assert_eq!(
4573            selection!("...invalid", spec).apply_to(&json!({ "invalid": true })),
4574            (
4575                Some(json!({})),
4576                vec![ApplyToError::new(
4577                    "Expected object or null, not boolean".to_string(),
4578                    vec![],
4579                    Some(3..10),
4580                    spec,
4581                )],
4582            ),
4583        );
4584
4585        assert_eq!(
4586            selection!("...$(invalid)", spec).apply_to(&json!({ "invalid": false })),
4587            (
4588                Some(json!({})),
4589                vec![ApplyToError::new(
4590                    "Expected object or null, not boolean".to_string(),
4591                    vec![],
4592                    Some(3..13),
4593                    spec,
4594                )],
4595            ),
4596        );
4597    }
4598
4599    #[test]
4600    fn test_spread_invalid_strings() {
4601        let spec = ConnectSpec::V0_4;
4602
4603        assert_eq!(
4604            selection!("...invalid", spec).apply_to(&json!({ "invalid": "string" })),
4605            (
4606                Some(json!({})),
4607                vec![ApplyToError::new(
4608                    "Expected object or null, not string".to_string(),
4609                    vec![],
4610                    Some(3..10),
4611                    spec,
4612                )],
4613            ),
4614        );
4615
4616        assert_eq!(
4617            selection!("...$(invalid)", spec).apply_to(&json!({ "invalid": "string" })),
4618            (
4619                Some(json!({})),
4620                vec![ApplyToError::new(
4621                    "Expected object or null, not string".to_string(),
4622                    vec![],
4623                    Some(3..13),
4624                    spec,
4625                )],
4626            ),
4627        );
4628    }
4629
4630    #[test]
4631    fn test_spread_invalid_arrays() {
4632        let spec = ConnectSpec::V0_4;
4633
4634        // The ... operator only works for objects for now, as it spreads their
4635        // keys into some larger object. We may support array spreading in the
4636        // future, but it will probably work somewhat differently (it may be
4637        // available only within literal expressions, for example).
4638        assert_eq!(
4639            selection!("...invalid", spec).apply_to(&json!({ "invalid": [1, 2, 3] })),
4640            (
4641                Some(json!({})),
4642                vec![ApplyToError::new(
4643                    "Expected object or null, not array".to_string(),
4644                    vec![],
4645                    Some(3..10),
4646                    spec,
4647                )],
4648            ),
4649        );
4650
4651        assert_eq!(
4652            selection!("...$(invalid)", spec).apply_to(&json!({ "invalid": [] })),
4653            (
4654                Some(json!({})),
4655                vec![ApplyToError::new(
4656                    "Expected object or null, not array".to_string(),
4657                    vec![],
4658                    Some(3..13),
4659                    spec,
4660                )],
4661            ),
4662        );
4663    }
4664
4665    #[test]
4666    fn test_spread_output_shapes() {
4667        let spec = ConnectSpec::V0_4;
4668
4669        assert_eq!(selection!("...a", spec).shape().pretty_print(), "$root.*.a");
4670        assert_eq!(
4671            selection!("...$(a)", spec).shape().pretty_print(),
4672            "$root.*.a",
4673        );
4674
4675        assert_eq!(
4676            selection!("a ...b", spec).shape().pretty_print(),
4677            "All<$root.*.b, { a: $root.*.a }>",
4678        );
4679        assert_eq!(
4680            selection!("a ...$(b)", spec).shape().pretty_print(),
4681            "All<$root.*.b, { a: $root.*.a }>",
4682        );
4683
4684        assert_eq!(
4685            selection!("a ...b c", spec).shape().pretty_print(),
4686            "All<$root.*.b, { a: $root.*.a, c: $root.*.c }>",
4687        );
4688        assert_eq!(
4689            selection!("a ...$(b) c", spec).shape().pretty_print(),
4690            "All<$root.*.b, { a: $root.*.a, c: $root.*.c }>",
4691        );
4692    }
4693    **/
4694
4695    #[test]
4696    fn null_coalescing_should_return_left_when_left_not_null() {
4697        let spec = ConnectSpec::V0_3;
4698        assert_eq!(
4699            selection!("$('Foo' ?? 'Bar')", spec).apply_to(&json!({})),
4700            (Some(json!("Foo")), vec![]),
4701        );
4702    }
4703
4704    #[test]
4705    fn null_coalescing_should_return_right_when_left_is_null() {
4706        let spec = ConnectSpec::V0_3;
4707        assert_eq!(
4708            selection!("$(null ?? 'Bar')", spec).apply_to(&json!({})),
4709            (Some(json!("Bar")), vec![]),
4710        );
4711    }
4712
4713    #[test]
4714    fn none_coalescing_should_return_left_when_left_not_none() {
4715        let spec = ConnectSpec::V0_3;
4716        assert_eq!(
4717            selection!("$('Foo' ?! 'Bar')", spec).apply_to(&json!({})),
4718            (Some(json!("Foo")), vec![]),
4719        );
4720    }
4721
4722    #[test]
4723    fn none_coalescing_should_preserve_null_when_left_is_null() {
4724        let spec = ConnectSpec::V0_3;
4725        assert_eq!(
4726            selection!("$(null ?! 'Bar')", spec).apply_to(&json!({})),
4727            (Some(json!(null)), vec![]),
4728        );
4729    }
4730
4731    #[test]
4732    fn nullish_coalescing_should_return_final_null() {
4733        let spec = ConnectSpec::V0_3;
4734        assert_eq!(
4735            selection!("$(missing ?? null)", spec).apply_to(&json!({})),
4736            (Some(json!(null)), vec![]),
4737        );
4738        assert_eq!(
4739            selection!("$(missing ?! null)", spec).apply_to(&json!({})),
4740            (Some(json!(null)), vec![]),
4741        );
4742    }
4743
4744    #[test]
4745    fn nullish_coalescing_should_return_final_none() {
4746        let spec = ConnectSpec::V0_3;
4747        assert_eq!(
4748            selection!("$(missing ?? also_missing)", spec).apply_to(&json!({})),
4749            (
4750                None,
4751                vec![
4752                    ApplyToError::new(
4753                        "Property .missing not found in object".to_string(),
4754                        vec![json!("missing")],
4755                        Some(2..9),
4756                        spec,
4757                    ),
4758                    ApplyToError::new(
4759                        "Property .also_missing not found in object".to_string(),
4760                        vec![json!("also_missing")],
4761                        Some(13..25),
4762                        spec,
4763                    ),
4764                ]
4765            ),
4766        );
4767        assert_eq!(
4768            selection!("maybe: $(missing ?! also_missing)", spec).apply_to(&json!({})),
4769            (
4770                Some(json!({})),
4771                vec![
4772                    ApplyToError::new(
4773                        "Property .missing not found in object".to_string(),
4774                        vec![json!("missing")],
4775                        Some(9..16),
4776                        spec,
4777                    ),
4778                    ApplyToError::new(
4779                        "Property .also_missing not found in object".to_string(),
4780                        vec![json!("also_missing")],
4781                        Some(20..32),
4782                        spec,
4783                    ),
4784                ]
4785            ),
4786        );
4787    }
4788
4789    #[test]
4790    fn coalescing_operators_should_return_earlier_values_if_later_missing() {
4791        let spec = ConnectSpec::V0_3;
4792        assert_eq!(
4793            selection!("$(1234 ?? missing)", spec).apply_to(&json!({})),
4794            (Some(json!(1234)), vec![]),
4795        );
4796        assert_eq!(
4797            selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": 1234 })),
4798            (Some(json!(1234)), vec![]),
4799        );
4800        assert_eq!(
4801            selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": null })),
4802            (
4803                None,
4804                vec![ApplyToError::new(
4805                    "Property .missing not found in object".to_string(),
4806                    vec![json!("missing")],
4807                    Some(10..17),
4808                    spec,
4809                )]
4810            ),
4811        );
4812        assert_eq!(
4813            selection!("$(null ?! missing)", spec).apply_to(&json!({})),
4814            (Some(json!(null)), vec![]),
4815        );
4816        assert_eq!(
4817            selection!("$(item ?! missing)", spec).apply_to(&json!({ "item": null })),
4818            (Some(json!(null)), vec![]),
4819        );
4820    }
4821
4822    #[test]
4823    fn null_coalescing_should_chain_left_to_right_when_multiple_nulls() {
4824        // TODO: TEST HERE
4825        let spec = ConnectSpec::V0_3;
4826        assert_eq!(
4827            selection!("$(null ?? null ?? 'Bar')", spec).apply_to(&json!({})),
4828            (Some(json!("Bar")), vec![]),
4829        );
4830    }
4831
4832    #[test]
4833    fn null_coalescing_should_stop_at_first_non_null_when_chaining() {
4834        let spec = ConnectSpec::V0_3;
4835        assert_eq!(
4836            selection!("$('Foo' ?? null ?? 'Bar')", spec).apply_to(&json!({})),
4837            (Some(json!("Foo")), vec![]),
4838        );
4839    }
4840
4841    #[test]
4842    fn null_coalescing_should_fallback_when_field_is_null() {
4843        let spec = ConnectSpec::V0_3;
4844        let data = json!({"field1": null, "field2": "value2"});
4845        assert_eq!(
4846            selection!("$($.field1 ?? $.field2)", spec).apply_to(&data),
4847            (Some(json!("value2")), vec![]),
4848        );
4849    }
4850
4851    #[test]
4852    fn null_coalescing_should_use_literal_fallback_when_all_fields_null() {
4853        let spec = ConnectSpec::V0_3;
4854        let data = json!({"field1": null, "field3": null});
4855        assert_eq!(
4856            selection!("$($.field1 ?? $.field3 ?? 'fallback')", spec).apply_to(&data),
4857            (Some(json!("fallback")), vec![]),
4858        );
4859    }
4860
4861    #[test]
4862    fn none_coalescing_should_preserve_null_field() {
4863        let spec = ConnectSpec::V0_3;
4864        let data = json!({"nullField": null});
4865        assert_eq!(
4866            selection!("$($.nullField ?! 'fallback')", spec).apply_to(&data),
4867            (Some(json!(null)), vec![]),
4868        );
4869    }
4870
4871    #[test]
4872    fn none_coalescing_should_replace_missing_field() {
4873        let spec = ConnectSpec::V0_3;
4874        let data = json!({"nullField": null});
4875        assert_eq!(
4876            selection!("$($.missingField ?! 'fallback')", spec).apply_to(&data),
4877            (Some(json!("fallback")), vec![]),
4878        );
4879    }
4880
4881    #[test]
4882    fn null_coalescing_should_replace_null_field() {
4883        let spec = ConnectSpec::V0_3;
4884        let data = json!({"nullField": null});
4885        assert_eq!(
4886            selection!("$($.nullField ?? 'fallback')", spec).apply_to(&data),
4887            (Some(json!("fallback")), vec![]),
4888        );
4889    }
4890
4891    #[test]
4892    fn null_coalescing_should_replace_missing_field() {
4893        let spec = ConnectSpec::V0_3;
4894        let data = json!({"nullField": null});
4895        assert_eq!(
4896            selection!("$($.missingField ?? 'fallback')", spec).apply_to(&data),
4897            (Some(json!("fallback")), vec![]),
4898        );
4899    }
4900
4901    #[test]
4902    fn null_coalescing_should_preserve_number_type() {
4903        let spec = ConnectSpec::V0_3;
4904        assert_eq!(
4905            selection!("$(null ?? 42)", spec).apply_to(&json!({})),
4906            (Some(json!(42)), vec![]),
4907        );
4908    }
4909
4910    #[test]
4911    fn null_coalescing_should_preserve_boolean_type() {
4912        let spec = ConnectSpec::V0_3;
4913        assert_eq!(
4914            selection!("$(null ?? true)", spec).apply_to(&json!({})),
4915            (Some(json!(true)), vec![]),
4916        );
4917    }
4918
4919    #[test]
4920    fn null_coalescing_should_preserve_object_type() {
4921        let spec = ConnectSpec::V0_3;
4922        assert_eq!(
4923            selection!("$(null ?? {'key': 'value'})", spec).apply_to(&json!({})),
4924            (Some(json!({"key": "value"})), vec![]),
4925        );
4926    }
4927
4928    #[test]
4929    fn null_coalescing_should_preserve_array_type() {
4930        let spec = ConnectSpec::V0_3;
4931        assert_eq!(
4932            selection!("$(null ?? [1, 2, 3])", spec).apply_to(&json!({})),
4933            (Some(json!([1, 2, 3])), vec![]),
4934        );
4935    }
4936
4937    #[test]
4938    fn null_coalescing_should_fallback_when_null_used_as_method_arg() {
4939        let spec = ConnectSpec::V0_3;
4940        assert_eq!(
4941            selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": null, "c": 5})),
4942            (Some(json!(10)), vec![]),
4943        );
4944    }
4945
4946    #[test]
4947    fn null_coalescing_should_fallback_when_none_used_as_method_arg() {
4948        let spec = ConnectSpec::V0_3;
4949        assert_eq!(
4950            selection!("$.a->add(missing ?? c)", spec)
4951                .apply_to(&json!({"a": 5, "b": null, "c": 5})),
4952            (Some(json!(10)), vec![]),
4953        );
4954    }
4955
4956    #[test]
4957    fn null_coalescing_should_not_fallback_when_not_null_used_as_method_arg() {
4958        let spec = ConnectSpec::V0_3;
4959        assert_eq!(
4960            selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": 3, "c": 5})),
4961            (Some(json!(8)), vec![]),
4962        );
4963    }
4964
4965    #[test]
4966    fn null_coalescing_should_allow_multiple_method_args() {
4967        let spec = ConnectSpec::V0_3;
4968        let add_selection = selection!("a->add(b ?? c, missing ?! c)", spec);
4969        assert_eq!(
4970            add_selection.apply_to(&json!({ "a": 5, "b": 3, "c": 7 })),
4971            (Some(json!(15)), vec![]),
4972        );
4973        assert_eq!(
4974            add_selection.apply_to(&json!({ "a": 5, "b": null, "c": 7 })),
4975            (Some(json!(19)), vec![]),
4976        );
4977    }
4978
4979    // TODO Reenable this test in ConnectSpec::V0_4 when we support ... spread
4980    // syntax and abstract types.
4981    /**
4982    #[test]
4983    fn none_coalescing_should_allow_defaulting_match() {
4984        let spec = ConnectSpec::V0_4;
4985
4986        assert_eq!(
4987            selection!("a ...b->match(['match', { b: 'world' }])", spec)
4988                .apply_to(&json!({ "a": "hello", "b": "match" })),
4989            (Some(json!({ "a": "hello", "b": "world" })), vec![]),
4990        );
4991
4992        assert_eq!(
4993            selection!("a ...$(b->match(['match', { b: 'world' }]) ?? {})", spec)
4994                .apply_to(&json!({ "a": "hello", "b": "match" })),
4995            (Some(json!({ "a": "hello", "b": "world" })), vec![]),
4996        );
4997
4998        assert_eq!(
4999            selection!("a ...$(b->match(['match', { b: 'world' }]) ?? {})", spec)
5000                .apply_to(&json!({ "a": "hello", "b": "bogus" })),
5001            (Some(json!({ "a": "hello" })), vec![]),
5002        );
5003
5004        assert_eq!(
5005            selection!("a ...$(b->match(['match', { b: 'world' }]) ?! null)", spec)
5006                .apply_to(&json!({ "a": "hello", "b": "bogus" })),
5007            (Some(json!(null)), vec![]),
5008        );
5009    }
5010
5011    #[test]
5012    fn nullish_coalescing_chains_should_have_predictable_shape() {
5013        let spec = ConnectSpec::V0_4;
5014
5015        let chain = selection!("$(1 ?? true ?? null)", spec);
5016        assert_eq!(chain.shape().pretty_print(), "One<1, true, null>",);
5017
5018        let complex_chain = selection!(
5019            r#"
5020            ... $(
5021                message?->echo({ __typename: "Good", message: @ }) ??
5022                error?->echo({ __typename: "Bad", error: @ }) ??
5023                null
5024            )
5025        "#,
5026            spec
5027        );
5028        assert_eq!(
5029            complex_chain.shape().pretty_print(),
5030            "One<{ __typename: \"Good\", message: $root.*.message }, { __typename: \"Bad\", error: $root.*.error }, null>",
5031        );
5032
5033        let complex_chain_no_fallback = selection!(
5034            r#"
5035            ... $(
5036                message?->echo({ __typename: "Good", message: @ }) ??
5037                error?->echo({ __typename: "Bad", error: @ })
5038            )
5039        "#,
5040            spec
5041        );
5042        assert_eq!(
5043            complex_chain_no_fallback.shape().pretty_print(),
5044            // None should not be an option here, even though both message and
5045            // error might not exist, because the ... spread operator spreads
5046            // nothing in that case.
5047            // "One<{ __typename: \"Good\", message: $root.*.message }, { __typename: \"Bad\", error: $root.*.error }, None>",
5048            "One<{ __typename: \"Good\", message: $root.*.message }, { __typename: \"Bad\", error: $root.*.error }, {}>",
5049        );
5050        assert_eq!(
5051            complex_chain_no_fallback.apply_to(&json!({})),
5052            (Some(json!({})), vec![
5053                ApplyToError::new(
5054                    "Inlined path produced no value".to_string(),
5055                    vec![],
5056                    Some(17..165),
5057                    spec,
5058                ),
5059            ]),
5060        );
5061    }
5062    **/
5063
5064    #[test]
5065    fn wtf_operator_should_not_exclude_null_from_nullable_union_shape() {
5066        let spec = ConnectSpec::V0_3;
5067
5068        // We're also testing the ?? operator here, to show the difference.
5069        let nullish_selection = selection!("$($value ?? 'fallback')", spec);
5070        let wtf_selection = selection!("$($value ?! 'fallback')", spec);
5071
5072        let mut vars = IndexMap::default();
5073        vars.insert("$value".to_string(), json!(null));
5074
5075        assert_eq!(
5076            nullish_selection.apply_with_vars(&json!({}), &vars),
5077            (Some(json!("fallback")), vec![]),
5078        );
5079
5080        assert_eq!(
5081            wtf_selection.apply_with_vars(&json!({}), &vars),
5082            (Some(json!(null)), vec![]),
5083        );
5084
5085        let mut vars_with_string_value = IndexMap::default();
5086        vars_with_string_value.insert("$value".to_string(), json!("fine"));
5087
5088        assert_eq!(
5089            nullish_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5090            (Some(json!("fine")), vec![]),
5091        );
5092
5093        assert_eq!(
5094            wtf_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5095            (Some(json!("fine")), vec![]),
5096        );
5097
5098        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5099            .with_spec(spec)
5100            .with_named_shapes([(
5101                "$value".to_string(),
5102                Shape::one([Shape::string([]), Shape::null([]), Shape::none()], []),
5103            )]);
5104
5105        assert_eq!(
5106            nullish_selection
5107                // Since we're not using the input shape $root here, it should
5108                // not be a problem to pass Shape::none() as $root.
5109                .compute_output_shape(&shape_context, Shape::none())
5110                .pretty_print(),
5111            "String",
5112        );
5113
5114        assert_eq!(
5115            wtf_selection
5116                // Since we're not using the input shape $root here, it should
5117                // not be a problem to pass Shape::none() as $root.
5118                .compute_output_shape(&shape_context, Shape::none())
5119                .pretty_print(),
5120            "One<String, null>",
5121        );
5122    }
5123
5124    #[test]
5125    fn question_operator_should_map_null_to_none() {
5126        let spec = ConnectSpec::V0_3;
5127
5128        let nullish_string_selection = selection!("$(stringOrNull?)", spec);
5129        assert_eq!(
5130            nullish_string_selection.apply_to(&json!({"stringOrNull": "a string"})),
5131            (Some(json!("a string")), vec![]),
5132        );
5133        assert_eq!(
5134            nullish_string_selection.apply_to(&json!({"stringOrNull": null})),
5135            (None, vec![]),
5136        );
5137        assert_eq!(
5138            nullish_string_selection.apply_to(&json!({})),
5139            (None, vec![]),
5140        );
5141
5142        let shape_context = {
5143            let mut named_shapes = IndexMap::default();
5144
5145            named_shapes.insert(
5146                "$root".to_string(),
5147                Shape::record(
5148                    {
5149                        let mut map = Shape::empty_map();
5150                        map.insert(
5151                            "stringOrNull".to_string(),
5152                            Shape::one([Shape::string([]), Shape::null([])], []),
5153                        );
5154                        map
5155                    },
5156                    [],
5157                ),
5158            );
5159
5160            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5161                .with_spec(spec)
5162                .with_named_shapes(named_shapes)
5163        };
5164
5165        let root_shape = shape_context.named_shapes().get("$root").unwrap().clone();
5166
5167        assert_eq!(
5168            root_shape.pretty_print(),
5169            "{ stringOrNull: One<String, null> }",
5170        );
5171
5172        assert_eq!(
5173            nullish_string_selection
5174                .compute_output_shape(
5175                    &shape_context,
5176                    shape_context.named_shapes().get("$root").unwrap().clone(),
5177                )
5178                .pretty_print(),
5179            // Note that null has been replaced with None.
5180            "One<String, None>",
5181        );
5182    }
5183
5184    #[test]
5185    fn question_operator_should_add_none_to_named_shapes() {
5186        let spec = ConnectSpec::V0_3;
5187
5188        let string_or_null_expr = selection!("$(stringOrNull?)", spec);
5189
5190        assert_eq!(
5191            string_or_null_expr.shape().pretty_print(),
5192            "$root.stringOrNull?",
5193        );
5194    }
5195
5196    #[test]
5197    fn question_operator_with_nested_objects() {
5198        let spec = ConnectSpec::V0_3;
5199
5200        let nested_selection = selection!("$(user?.profile?.name)", spec);
5201        assert_eq!(
5202            nested_selection.apply_to(&json!({"user": {"profile": {"name": "Alice"}}})),
5203            (Some(json!("Alice")), vec![]),
5204        );
5205        assert_eq!(
5206            nested_selection.apply_to(&json!({"user": null})),
5207            (None, vec![]),
5208        );
5209        assert_eq!(
5210            nested_selection.apply_to(&json!({"user": {"profile": null}})),
5211            (None, vec![]),
5212        );
5213        assert_eq!(nested_selection.apply_to(&json!({})), (None, vec![]));
5214    }
5215
5216    #[test]
5217    fn question_operator_with_array_access() {
5218        let spec = ConnectSpec::V0_3;
5219
5220        let array_selection = selection!("$(items?->first?.name)", spec);
5221        assert_eq!(
5222            array_selection.apply_to(&json!({"items": [{"name": "first"}]})),
5223            (Some(json!("first")), vec![]),
5224        );
5225        assert_eq!(
5226            array_selection.apply_to(&json!({"items": []})),
5227            (None, vec![]),
5228        );
5229        assert_eq!(
5230            array_selection.apply_to(&json!({"items": null})),
5231            (None, vec![]),
5232        );
5233        assert_eq!(array_selection.apply_to(&json!({})), (None, vec![]));
5234    }
5235
5236    #[test]
5237    fn question_operator_with_union_shapes() {
5238        let spec = ConnectSpec::V0_3;
5239
5240        let shape_context = {
5241            let mut named_shapes = IndexMap::default();
5242
5243            named_shapes.insert(
5244                "$root".to_string(),
5245                Shape::record(
5246                    {
5247                        let mut map = Shape::empty_map();
5248                        map.insert(
5249                            "unionField".to_string(),
5250                            Shape::one([Shape::string([]), Shape::int([]), Shape::null([])], []),
5251                        );
5252                        map
5253                    },
5254                    [],
5255                ),
5256            );
5257
5258            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5259                .with_spec(spec)
5260                .with_named_shapes(named_shapes)
5261        };
5262
5263        let union_selection = selection!("$(unionField?)", spec);
5264
5265        assert_eq!(
5266            union_selection
5267                .compute_output_shape(
5268                    &shape_context,
5269                    shape_context.named_shapes().get("$root").unwrap().clone(),
5270                )
5271                .pretty_print(),
5272            "One<String, Int, None>",
5273        );
5274    }
5275
5276    #[test]
5277    fn question_operator_with_error_shapes() {
5278        let spec = ConnectSpec::V0_3;
5279
5280        let shape_context = {
5281            let mut named_shapes = IndexMap::default();
5282
5283            named_shapes.insert(
5284                "$root".to_string(),
5285                Shape::record(
5286                    {
5287                        let mut map = Shape::empty_map();
5288                        map.insert(
5289                            "errorField".to_string(),
5290                            Shape::error_with_partial(
5291                                "Test error".to_string(),
5292                                Shape::one([Shape::string([]), Shape::null([])], []),
5293                                [],
5294                            ),
5295                        );
5296                        map
5297                    },
5298                    [],
5299                ),
5300            );
5301
5302            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5303                .with_spec(spec)
5304                .with_named_shapes(named_shapes)
5305        };
5306
5307        let error_selection = selection!("$(errorField?)", spec);
5308
5309        let result_shape = error_selection.compute_output_shape(
5310            &shape_context,
5311            shape_context.named_shapes().get("$root").unwrap().clone(),
5312        );
5313
5314        // The question mark should be applied recursively to the partial shape within the error
5315        assert!(result_shape.pretty_print().contains("Error"));
5316        assert!(result_shape.pretty_print().contains("None"));
5317    }
5318
5319    #[test]
5320    fn question_operator_with_all_shapes() {
5321        let spec = ConnectSpec::V0_3;
5322
5323        let shape_context = {
5324            let mut named_shapes = IndexMap::default();
5325
5326            named_shapes.insert(
5327                "$root".to_string(),
5328                Shape::record(
5329                    {
5330                        let mut map = Shape::empty_map();
5331                        map.insert(
5332                            "allField".to_string(),
5333                            Shape::all(
5334                                [
5335                                    Shape::string([]),
5336                                    Shape::one([Shape::string([]), Shape::null([])], []),
5337                                ],
5338                                [],
5339                            ),
5340                        );
5341                        map
5342                    },
5343                    [],
5344                ),
5345            );
5346
5347            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5348                .with_spec(spec)
5349                .with_named_shapes(named_shapes)
5350        };
5351
5352        let all_selection = selection!("$(allField?)", spec);
5353
5354        assert_eq!(
5355            all_selection
5356                .compute_output_shape(
5357                    &shape_context,
5358                    shape_context.named_shapes().get("$root").unwrap().clone(),
5359                )
5360                .pretty_print(),
5361            "One<String, None>",
5362        );
5363    }
5364
5365    #[test]
5366    fn question_operator_preserves_non_null_shapes() {
5367        let spec = ConnectSpec::V0_3;
5368
5369        let shape_context = {
5370            let mut named_shapes = IndexMap::default();
5371
5372            named_shapes.insert(
5373                "$root".to_string(),
5374                Shape::record(
5375                    {
5376                        let mut map = Shape::empty_map();
5377                        map.insert("nonNullString".to_string(), Shape::string([]));
5378                        map
5379                    },
5380                    [],
5381                ),
5382            );
5383
5384            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5385                .with_spec(spec)
5386                .with_named_shapes(named_shapes)
5387        };
5388
5389        let non_null_selection = selection!("$(nonNullString?)", spec);
5390
5391        assert_eq!(
5392            non_null_selection
5393                .compute_output_shape(
5394                    &shape_context,
5395                    shape_context.named_shapes().get("$root").unwrap().clone(),
5396                )
5397                .pretty_print(),
5398            "String",
5399        );
5400    }
5401
5402    #[test]
5403    fn question_operator_with_multiple_operators_in_chain() {
5404        let spec = ConnectSpec::V0_3;
5405
5406        // Test combining ? with other operators
5407        let mixed_chain_selection = selection!("$(field? ?? 'default')", spec);
5408        assert_eq!(
5409            mixed_chain_selection.apply_to(&json!({"field": "value"})),
5410            (Some(json!("value")), vec![]),
5411        );
5412        assert_eq!(
5413            mixed_chain_selection.apply_to(&json!({"field": null})),
5414            (Some(json!("default")), vec![]),
5415        );
5416        assert_eq!(
5417            mixed_chain_selection.apply_to(&json!({})),
5418            (Some(json!("default")), vec![]),
5419        );
5420    }
5421
5422    #[test]
5423    fn question_operator_direct_null_input_shape() {
5424        let spec = ConnectSpec::V0_3;
5425
5426        let shape_context = {
5427            let mut named_shapes = IndexMap::default();
5428
5429            named_shapes.insert("$root".to_string(), Shape::null([]));
5430
5431            ShapeContext::new(SourceId::Other("JSONSelection".into()))
5432                .with_spec(spec)
5433                .with_named_shapes(named_shapes)
5434        };
5435
5436        let null_selection = selection!("$root?", spec);
5437
5438        assert_eq!(
5439            null_selection
5440                .compute_output_shape(
5441                    &shape_context,
5442                    shape_context.named_shapes().get("$root").unwrap().clone(),
5443                )
5444                .pretty_print(),
5445            "None",
5446        );
5447    }
5448
5449    #[test]
5450    fn test_unknown_name() {
5451        let spec = ConnectSpec::V0_3;
5452        let sel = selection!("book.author? { name age? }", spec);
5453        assert_eq!(
5454            sel.shape().pretty_print(),
5455            "One<{ age: $root.book.author?.*.age?, name: $root.book.author?.*.name }, None>",
5456        );
5457    }
5458
5459    #[test]
5460    fn test_nullish_coalescing_shape() {
5461        let spec = ConnectSpec::V0_3;
5462        let sel = selection!("$(a ?? b ?? c)", spec);
5463        assert_eq!(
5464            sel.shape().pretty_print(),
5465            "One<$root.a?!, $root.b?!, $root.c>",
5466        );
5467
5468        let mut named_shapes = IndexMap::default();
5469        named_shapes.insert(
5470            "$root".to_string(),
5471            Shape::record(
5472                {
5473                    let mut map = Shape::empty_map();
5474                    map.insert(
5475                        "a".to_string(),
5476                        Shape::one([Shape::string([]), Shape::null([])], []),
5477                    );
5478                    map.insert("b".to_string(), Shape::string([]));
5479                    map.insert("c".to_string(), Shape::int([]));
5480                    map
5481                },
5482                [],
5483            ),
5484        );
5485
5486        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5487            .with_spec(spec)
5488            .with_named_shapes(named_shapes);
5489
5490        assert_eq!(
5491            sel.compute_output_shape(
5492                &shape_context,
5493                shape_context.named_shapes().get("$root").unwrap().clone(),
5494            )
5495            .pretty_print(),
5496            "One<String, Int>",
5497        );
5498    }
5499
5500    #[test]
5501    fn test_none_coalescing_shape() {
5502        let spec = ConnectSpec::V0_3;
5503        let sel = selection!("$(a ?! b ?! c)", spec);
5504        assert_eq!(
5505            sel.shape().pretty_print(),
5506            "One<$root.a!, $root.b!, $root.c>",
5507        );
5508
5509        let mut named_shapes = IndexMap::default();
5510        named_shapes.insert(
5511            "$root".to_string(),
5512            Shape::record(
5513                {
5514                    let mut map = Shape::empty_map();
5515                    map.insert(
5516                        "a".to_string(),
5517                        Shape::one([Shape::string([]), Shape::null([])], []),
5518                    );
5519                    map.insert(
5520                        "b".to_string(),
5521                        Shape::one([Shape::string([]), Shape::none()], []),
5522                    );
5523                    map.insert("c".to_string(), Shape::null([]));
5524                    map
5525                },
5526                [],
5527            ),
5528        );
5529
5530        let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5531            .with_spec(spec)
5532            .with_named_shapes(named_shapes);
5533
5534        assert_eq!(
5535            sel.compute_output_shape(
5536                &shape_context,
5537                shape_context.named_shapes().get("$root").unwrap().clone(),
5538            )
5539            .pretty_print(),
5540            "One<String, null>",
5541        );
5542    }
5543}