1use std::cell::RefCell;
4use std::hash::Hash;
5
6use apollo_compiler::collections::IndexMap;
7use apollo_compiler::collections::IndexSet;
8use serde_json_bytes::Map as JSONMap;
9use serde_json_bytes::Value as JSON;
10use serde_json_bytes::json;
11use shape::Shape;
12use shape::ShapeCase;
13use shape::location::Location;
14use shape::location::SourceId;
15
16use super::Ref;
17use super::helpers::json_merge;
18use super::helpers::json_type_name;
19use super::immutable::InputPath;
20use super::known_var::KnownVariable;
21use super::lit_expr::LitExpr;
22use super::lit_expr::LitOp;
23use super::location::OffsetRange;
24use super::location::Ranged;
25use super::location::WithRange;
26use super::methods::ArrowMethod;
27use super::parser::*;
28use super::selection_trie::SelectionTrie;
29use crate::connectors::spec::ConnectSpec;
30
31pub(super) type VarsWithPathsMap<'a> = IndexMap<KnownVariable, (&'a JSON, InputPath<JSON>)>;
32
33impl JSONSelection {
34 pub fn apply_to(&self, data: &JSON) -> (Option<JSON>, Vec<ApplyToError>) {
40 self.apply_with_vars(data, &IndexMap::default())
41 }
42
43 pub fn apply_with_vars(
44 &self,
45 data: &JSON,
46 vars: &IndexMap<String, JSON>,
47 ) -> (Option<JSON>, Vec<ApplyToError>) {
48 let mut errors = IndexSet::default();
50
51 let mut vars_with_paths: VarsWithPathsMap = IndexMap::default();
52 for (var_name, var_data) in vars {
53 vars_with_paths.insert(
54 KnownVariable::External(var_name.as_str().to_string()),
55 (var_data, InputPath::empty().append(json!(var_name))),
56 );
57 }
58 vars_with_paths.insert(KnownVariable::Dollar, (data, InputPath::empty()));
62
63 let spec = self.spec();
64 let (value, apply_errors) =
65 self.apply_to_path(data, &vars_with_paths, &InputPath::empty(), spec);
66
67 errors.extend(apply_errors);
73
74 (value, errors.into_iter().collect())
75 }
76
77 pub fn shape(&self) -> Shape {
78 let context =
79 ShapeContext::new(SourceId::Other("JSONSelection".into())).with_spec(self.spec());
80
81 self.compute_output_shape(
82 &context,
85 Shape::name("$root", Vec::new()),
96 )
97 }
98
99 pub(crate) fn compute_output_shape(&self, context: &ShapeContext, input_shape: Shape) -> Shape {
100 debug_assert_eq!(context.spec(), self.spec());
101
102 let computable: &dyn ApplyToInternal = match &self.inner {
103 TopLevelSelection::Named(selection) => selection,
104 TopLevelSelection::Value(lit) => lit,
105 };
106
107 let dollar_shape = input_shape.clone();
108
109 if Some(&input_shape) == context.named_shapes().get("$root") {
110 computable.compute_output_shape(context, input_shape, dollar_shape)
113 } else {
114 let cloned_context = context
117 .clone()
118 .with_named_shapes([("$root".to_string(), input_shape.clone())]);
119 computable.compute_output_shape(&cloned_context, input_shape, dollar_shape)
120 }
121 }
122}
123
124fn lookup_variable<'a>(
125 vars: &'a VarsWithPathsMap,
126 var_name: &str,
127) -> Option<(&'a JSON, &'a InputPath<JSON>)> {
128 let entry = if var_name == "$" {
129 vars.get(&KnownVariable::Dollar)
130 } else {
131 vars.get(&KnownVariable::Local(var_name.to_string()))
135 .or_else(|| vars.get(&KnownVariable::External(var_name.to_string())))
136 };
137 entry.map(|(data, path)| (*data, path))
138}
139
140impl Ranged for JSONSelection {
141 fn range(&self) -> OffsetRange {
142 match &self.inner {
143 TopLevelSelection::Named(selection) => selection.range(),
144 TopLevelSelection::Value(lit) => lit.range(),
145 }
146 }
147
148 fn shape_location(&self, source_id: &SourceId) -> Option<Location> {
149 self.range().map(|range| source_id.location(range))
150 }
151}
152
153pub(super) trait ApplyToInternal {
154 fn apply_to_path(
157 &self,
158 data: &JSON,
159 vars: &VarsWithPathsMap,
160 input_path: &InputPath<JSON>,
161 spec: ConnectSpec,
162 ) -> (Option<JSON>, Vec<ApplyToError>);
163
164 fn apply_to_array(
167 &self,
168 data_array: &[JSON],
169 vars: &VarsWithPathsMap,
170 input_path: &InputPath<JSON>,
171 spec: ConnectSpec,
172 ) -> (Option<JSON>, Vec<ApplyToError>) {
173 let mut output = Vec::with_capacity(data_array.len());
174 let mut errors = Vec::new();
175
176 for (i, element) in data_array.iter().enumerate() {
177 let input_path_with_index = input_path.append(json!(i));
178 let (applied, apply_errors) =
179 self.apply_to_path(element, vars, &input_path_with_index, spec);
180 errors.extend(apply_errors);
181 output.push(applied.unwrap_or(JSON::Null));
185 }
186
187 (Some(JSON::Array(output)), errors)
188 }
189
190 fn compute_output_shape(
194 &self,
195 context: &ShapeContext,
196 input_shape: Shape,
199 dollar_shape: Shape,
203 ) -> Shape;
204}
205
206#[derive(Debug, Clone)]
207pub(crate) struct ShapeContext {
208 #[allow(dead_code)]
211 spec: ConnectSpec,
212
213 named_shapes: IndexMap<String, Shape>,
218
219 source_id: SourceId,
222
223 consumption: Ref<RefCell<SelectionTrie>>,
229}
230
231impl ShapeContext {
232 pub(crate) fn new(source_id: SourceId) -> Self {
233 #[allow(clippy::arc_with_non_send_sync)]
238 let consumption = Ref::new(RefCell::new(SelectionTrie::new()));
239 Self {
240 spec: ConnectSpec::latest(),
241 named_shapes: IndexMap::default(),
242 source_id,
243 consumption,
244 }
245 }
246
247 #[allow(dead_code)]
248 pub(crate) fn spec(&self) -> ConnectSpec {
249 self.spec
250 }
251
252 pub(crate) fn with_spec(mut self, spec: ConnectSpec) -> Self {
253 self.spec = spec;
254 self
255 }
256
257 pub(crate) fn named_shapes(&self) -> &IndexMap<String, Shape> {
258 &self.named_shapes
259 }
260
261 pub(crate) fn with_named_shapes(
262 mut self,
263 named_shapes: impl IntoIterator<Item = (String, Shape)>,
264 ) -> Self {
265 for (name, shape) in named_shapes {
266 self.named_shapes.insert(name.clone(), shape.clone());
267 }
268 self
269 }
270
271 pub(crate) fn source_id(&self) -> &SourceId {
272 &self.source_id
273 }
274
275 #[allow(dead_code)]
280 pub(crate) fn consumption(&self) -> &Ref<RefCell<SelectionTrie>> {
281 &self.consumption
282 }
283
284 #[allow(dead_code)]
314 pub(crate) fn record_consumption(&self, shape: &Shape, terminal: bool) {
315 let mut trie = self.consumption.borrow_mut();
316 if let ShapeCase::Name(name, _weak) = shape.case() {
317 let leaf = trie.add_name(name);
318 if terminal {
319 leaf.set_leaf();
320 }
321 }
322 for name in shape.names() {
323 trie.add_name(name);
325 }
326 }
327}
328
329#[derive(Debug, Eq, PartialEq, Clone, Hash)]
330pub struct ApplyToError {
331 message: String,
332 path: Vec<JSON>,
333 range: OffsetRange,
334 spec: ConnectSpec,
335}
336
337impl ApplyToError {
338 pub(crate) const fn new(
339 message: String,
340 path: Vec<JSON>,
341 range: OffsetRange,
342 spec: ConnectSpec,
343 ) -> Self {
344 Self {
345 message,
346 path,
347 range,
348 spec,
349 }
350 }
351
352 #[cfg(test)]
355 pub(crate) fn from_json(json: &JSON) -> Self {
356 use crate::link::spec::Version;
357
358 let error = json.as_object().unwrap();
359 let message = error.get("message").unwrap().as_str().unwrap().to_string();
360 let path = error.get("path").unwrap().as_array().unwrap().clone();
361 let range = error.get("range").unwrap().as_array().unwrap();
362 let spec = error
363 .get("spec")
364 .and_then(|s| s.as_str())
365 .and_then(|s| match s.parse::<Version>() {
366 Ok(version) => ConnectSpec::try_from(&version).ok(),
367 Err(_) => None,
368 })
369 .unwrap_or_else(ConnectSpec::latest);
370
371 Self {
372 message,
373 path,
374 range: if range.len() == 2 {
375 let start = range[0].as_u64().unwrap() as usize;
376 let end = range[1].as_u64().unwrap() as usize;
377 Some(start..end)
378 } else {
379 None
380 },
381 spec,
382 }
383 }
384
385 pub fn message(&self) -> &str {
386 self.message.as_str()
387 }
388
389 pub fn path(&self) -> &[JSON] {
390 self.path.as_slice()
391 }
392
393 pub fn range(&self) -> OffsetRange {
394 self.range.clone()
395 }
396
397 pub fn spec(&self) -> ConnectSpec {
398 self.spec
399 }
400}
401
402pub(super) trait ApplyToResultMethods {
406 fn prepend_errors(self, errors: Vec<ApplyToError>) -> Self;
407
408 fn and_then_collecting_errors(
409 self,
410 f: impl FnOnce(&JSON) -> (Option<JSON>, Vec<ApplyToError>),
411 ) -> (Option<JSON>, Vec<ApplyToError>);
412}
413
414impl ApplyToResultMethods for (Option<JSON>, Vec<ApplyToError>) {
415 fn prepend_errors(self, mut errors: Vec<ApplyToError>) -> Self {
419 if errors.is_empty() {
420 self
421 } else {
422 let (value_opt, apply_errors) = self;
423 errors.extend(apply_errors);
424 (value_opt, errors)
425 }
426 }
427
428 fn and_then_collecting_errors(
431 self,
432 f: impl FnOnce(&JSON) -> (Option<JSON>, Vec<ApplyToError>),
433 ) -> (Option<JSON>, Vec<ApplyToError>) {
434 match self {
435 (Some(data), errors) => f(&data).prepend_errors(errors),
436 (None, errors) => (None, errors),
437 }
438 }
439}
440
441impl ApplyToInternal for JSONSelection {
442 fn apply_to_path(
443 &self,
444 data: &JSON,
445 vars: &VarsWithPathsMap,
446 input_path: &InputPath<JSON>,
447 _spec: ConnectSpec,
448 ) -> (Option<JSON>, Vec<ApplyToError>) {
449 match &self.inner {
450 TopLevelSelection::Named(named_selections) => {
457 named_selections.apply_to_path(data, vars, input_path, self.spec)
458 }
459 TopLevelSelection::Value(lit) => lit.apply_to_path(data, vars, input_path, self.spec),
460 }
461 }
462
463 fn compute_output_shape(
464 &self,
465 context: &ShapeContext,
466 input_shape: Shape,
467 dollar_shape: Shape,
468 ) -> Shape {
469 debug_assert_eq!(context.spec(), self.spec());
470
471 match &self.inner {
472 TopLevelSelection::Named(selection) => {
473 selection.compute_output_shape(context, input_shape, dollar_shape)
474 }
475 TopLevelSelection::Value(lit) => {
476 lit.compute_output_shape(context, input_shape, dollar_shape)
477 }
478 }
479 }
480}
481
482impl ApplyToInternal for NamedSelection {
483 fn apply_to_path(
484 &self,
485 data: &JSON,
486 vars: &VarsWithPathsMap,
487 input_path: &InputPath<JSON>,
488 spec: ConnectSpec,
489 ) -> (Option<JSON>, Vec<ApplyToError>) {
490 let mut output: Option<JSON> = None;
491 let mut errors = Vec::new();
492
493 let (value_opt, apply_errors) = self.path.apply_to_path(data, vars, input_path, spec);
494 errors.extend(apply_errors);
495
496 match &self.prefix {
497 NamingPrefix::Alias(alias) => {
498 if let Some(value) = value_opt {
499 output = Some(json!({ alias.name.as_str(): value }));
500 }
501 }
502
503 NamingPrefix::Spread(_spread_range) => {
504 match value_opt {
505 Some(JSON::Object(_) | JSON::Null) => {
506 output = value_opt;
509 }
510 Some(value) => {
511 errors.push(ApplyToError::new(
512 format!("Expected object or null, not {}", json_type_name(&value)),
513 input_path.to_vec(),
514 self.path.range(),
515 spec,
516 ));
517 }
518 None => {
519 errors.push(ApplyToError::new(
520 "Inlined path produced no value".to_string(),
521 input_path.to_vec(),
522 self.path.range(),
523 spec,
524 ));
525 }
526 };
527 }
528
529 NamingPrefix::None => {
530 let single_key = if let LitExpr::Path(path) = self.path.as_ref() {
535 path.get_single_key()
536 } else {
537 None
538 };
539 if let Some(single_key) = single_key {
540 if let Some(value) = value_opt {
541 output = Some(json!({ single_key.as_str(): value }));
542 }
543 } else {
544 output = value_opt;
545 }
546 }
547 }
548
549 (output, errors)
550 }
551
552 fn compute_output_shape(
553 &self,
554 context: &ShapeContext,
555 input_shape: Shape,
556 dollar_shape: Shape,
557 ) -> Shape {
558 let path_shape = self
559 .path
560 .compute_output_shape(context, input_shape, dollar_shape);
561
562 if let Some(single_output_key) = self.get_single_key() {
563 let mut map = Shape::empty_map();
564 map.insert(single_output_key.as_string(), path_shape);
565 Shape::record(map, self.shape_location(context.source_id()))
566 } else {
567 path_shape
568 }
569 }
570}
571
572impl ApplyToInternal for PathSelection {
573 fn apply_to_path(
574 &self,
575 data: &JSON,
576 vars: &VarsWithPathsMap,
577 input_path: &InputPath<JSON>,
578 spec: ConnectSpec,
579 ) -> (Option<JSON>, Vec<ApplyToError>) {
580 match (self.path.as_ref(), vars.get(&KnownVariable::Dollar)) {
581 (PathList::Key(_, _), Some((dollar_data, dollar_path))) => {
587 self.path
588 .apply_to_path(dollar_data, vars, dollar_path, spec)
589 }
590
591 _ => self.path.apply_to_path(data, vars, input_path, spec),
596 }
597 }
598
599 fn compute_output_shape(
600 &self,
601 context: &ShapeContext,
602 input_shape: Shape,
603 dollar_shape: Shape,
604 ) -> Shape {
605 match self.path.as_ref() {
606 PathList::Key(_, _) => {
607 self.path
611 .compute_output_shape(context, dollar_shape.clone(), dollar_shape)
612 }
613 _ => self
616 .path
617 .compute_output_shape(context, input_shape, dollar_shape),
618 }
619 }
620}
621
622impl ApplyToInternal for WithRange<PathList> {
623 fn apply_to_path(
624 &self,
625 data: &JSON,
626 vars: &VarsWithPathsMap,
627 input_path: &InputPath<JSON>,
628 spec: ConnectSpec,
629 ) -> (Option<JSON>, Vec<ApplyToError>) {
630 match self.as_ref() {
631 PathList::Var(ranged_var_name, tail) => {
632 let var_name = ranged_var_name.as_ref();
633 if var_name == &KnownVariable::AtSign {
634 tail.apply_to_path(data, vars, input_path, spec)
638 } else if let Some((var_data, var_path)) = lookup_variable(vars, var_name.as_str())
639 {
640 tail.apply_to_path(var_data, vars, var_path, spec)
645 } else {
646 (
647 None,
648 vec![ApplyToError::new(
649 format!("Variable {} not found", var_name.as_str()),
650 input_path.to_vec(),
651 ranged_var_name.range(),
652 spec,
653 )],
654 )
655 }
656 }
657 PathList::Key(key, tail) => {
658 let input_path_with_key = input_path.append(key.to_json());
659
660 if let JSON::Array(array) = data {
661 let empty_tail = WithRange::new(PathList::Empty, tail.range());
667 let self_with_empty_tail =
668 WithRange::new(PathList::Key(key.clone(), empty_tail), key.range());
669
670 self_with_empty_tail
671 .apply_to_array(array, vars, input_path, spec)
672 .and_then_collecting_errors(|shallow_mapped_array| {
673 tail.apply_to_path(
677 shallow_mapped_array,
678 vars,
679 &input_path_with_key,
680 spec,
681 )
682 })
683 } else {
684 let not_found = || {
685 (
686 None,
687 vec![ApplyToError::new(
688 format!(
689 "Property {} not found in {}",
690 key.dotted(),
691 json_type_name(data),
692 ),
693 input_path_with_key.to_vec(),
694 key.range(),
695 spec,
696 )],
697 )
698 };
699
700 if !matches!(data, JSON::Object(_)) {
701 return not_found();
702 }
703
704 if let Some(child) = data.get(key.as_str()) {
705 tail.apply_to_path(child, vars, &input_path_with_key, spec)
706 } else if tail.is_question() {
707 (None, vec![])
708 } else {
709 not_found()
710 }
711 }
712 }
713 PathList::Expr(expr, tail) => expr
714 .apply_to_path(data, vars, input_path, spec)
715 .and_then_collecting_errors(|value| {
716 tail.apply_to_path(value, vars, input_path, spec)
717 }),
718 PathList::Method(method_name, method_args, tail) => {
719 let method_path =
720 input_path.append(JSON::String(format!("->{}", method_name.as_ref()).into()));
721
722 ArrowMethod::lookup(method_name).map_or_else(
723 || {
724 (
725 None,
726 vec![ApplyToError::new(
727 format!("Method ->{} not found", method_name.as_ref()),
728 method_path.to_vec(),
729 method_name.range(),
730 spec,
731 )],
732 )
733 },
734 |method| {
735 let (result_opt, errors) = method.apply(
736 method_name,
737 method_args.as_ref(),
738 data,
739 vars,
740 &method_path,
741 spec,
742 );
743
744 if let (ArrowMethod::As, Some(JSON::Object(bindings))) =
751 (method, result_opt.as_ref())
752 {
753 let mut updated_vars = vars.clone();
754
755 for (var_name, var_value) in bindings {
756 updated_vars.insert(
757 KnownVariable::Local(var_name.as_str().to_string()),
758 (var_value, InputPath::empty().append(json!(var_name))),
760 );
761 }
762
763 return tail
764 .apply_to_path(data, &updated_vars, &method_path, spec)
767 .prepend_errors(errors);
768 }
769
770 if let Some(result) = result_opt {
771 tail.apply_to_path(&result, vars, &method_path, spec)
772 .prepend_errors(errors)
773 } else {
774 (None, errors)
780 }
781 },
782 )
783 }
784 PathList::Selection(selection) => selection.apply_to_path(data, vars, input_path, spec),
785 PathList::Question(tail) => {
786 if data.is_null() {
788 (None, vec![])
789 } else {
790 tail.apply_to_path(data, vars, input_path, spec)
791 }
792 }
793 PathList::Empty => {
794 (Some(data.clone()), vec![])
797 }
798 }
799 }
800
801 fn compute_output_shape(
802 &self,
803 context: &ShapeContext,
804 input_shape: Shape,
805 dollar_shape: Shape,
806 ) -> Shape {
807 match input_shape.case() {
808 ShapeCase::One(shapes) => {
809 return Shape::one(
810 shapes.iter().map(|shape| {
811 self.compute_output_shape(context, shape.clone(), dollar_shape.clone())
812 }),
813 input_shape.locations().cloned(),
814 );
815 }
816 ShapeCase::All(shapes) => {
817 return Shape::all(
818 shapes.iter().map(|shape| {
819 self.compute_output_shape(context, shape.clone(), dollar_shape.clone())
820 }),
821 input_shape.locations().cloned(),
822 );
823 }
824 ShapeCase::Error(error) => {
825 return match error.partial.as_ref() {
826 Some(partial) => Shape::error_with_partial(
827 error.message.clone(),
828 self.compute_output_shape(context, partial.clone(), dollar_shape),
829 input_shape.locations().cloned(),
830 ),
831 None => input_shape.clone(),
832 };
833 }
834 _ => {}
835 };
836
837 let mut extra_vars_opt: Option<Shape> = None;
841 let (current_shape, tail_opt) = match self.as_ref() {
842 PathList::Var(ranged_var_name, tail) => {
843 let var_name = ranged_var_name.as_ref();
844 let var_shape = if var_name == &KnownVariable::AtSign {
845 input_shape
846 } else if var_name == &KnownVariable::Dollar {
847 dollar_shape.clone()
848 } else if let Some(shape) = context.named_shapes().get(var_name.as_str()) {
849 shape.clone()
850 } else {
851 Shape::name(
852 var_name.as_str(),
853 ranged_var_name.shape_location(context.source_id()),
854 )
855 };
856 (var_shape, Some(tail))
857 }
858
859 PathList::Key(key, tail) => {
864 if input_shape.is_none() {
865 return input_shape;
877 }
878
879 let child_shape = field(&input_shape, key, context.source_id());
880
881 if child_shape.is_none() {
892 return Shape::error(
893 format!(
894 "Property {} not found in {}",
895 key.dotted(),
896 input_shape.pretty_print()
897 ),
898 key.shape_location(context.source_id()),
899 );
900 }
901
902 context.record_consumption(&child_shape, false);
912
913 (child_shape, Some(tail))
914 }
915
916 PathList::Expr(expr, tail) => (
917 expr.compute_output_shape(context, input_shape, dollar_shape.clone()),
918 Some(tail),
919 ),
920
921 PathList::Method(method_name, method_args, tail) => {
922 if input_shape.is_none() {
923 return input_shape;
931 }
932
933 context.record_consumption(&input_shape, true);
938
939 if let Some(method) = ArrowMethod::lookup(method_name) {
940 if context.spec() < ConnectSpec::V0_3 {
945 (
946 Shape::unknown(method_name.shape_location(context.source_id())),
947 None,
948 )
949 } else {
950 let result_shape = method.shape(
951 context,
952 method_name,
953 method_args.as_ref(),
954 input_shape.clone(),
955 dollar_shape.clone(),
956 );
957
958 if method == ArrowMethod::As {
961 extra_vars_opt = Some(result_shape);
966 (
967 input_shape,
971 Some(tail),
972 )
973 } else {
974 (result_shape, Some(tail))
975 }
976 }
977 } else {
978 (
979 Shape::error(
980 format!("Method ->{} not found", method_name.as_str()),
981 method_name.shape_location(context.source_id()),
982 ),
983 None,
984 )
985 }
986 }
987
988 PathList::Question(tail) => {
989 let q_shape = input_shape.question(self.shape_location(context.source_id()));
990 (
991 if tail.is_empty() {
992 q_shape
997 } else {
998 Shape::one([q_shape, Shape::none()], [])
1005 },
1006 Some(tail),
1007 )
1008 }
1009
1010 PathList::Selection(selection) => {
1011 if input_shape.is_none() {
1012 return input_shape;
1018 }
1019
1020 context.record_consumption(&input_shape, true);
1029
1030 (
1031 selection.compute_output_shape(context, input_shape, dollar_shape.clone()),
1032 None,
1033 )
1034 }
1035
1036 PathList::Empty => {
1037 context.record_consumption(&input_shape, true);
1041 (input_shape, None)
1042 }
1043 };
1044
1045 if let Some(tail) = tail_opt {
1046 fn compute_tail_shape(
1050 tail: &WithRange<PathList>,
1051 extra_vars_opt: &Option<Shape>,
1052 context: &ShapeContext,
1053 input_shape: Shape,
1054 dollar_shape: Shape,
1055 ) -> Shape {
1056 match extra_vars_opt.as_ref().map(|s| s.case()) {
1057 Some(ShapeCase::Object { fields, .. }) => {
1058 let new_context = context.clone().with_named_shapes(
1062 fields
1063 .iter()
1064 .map(|(name, shape)| (name.clone(), shape.clone())),
1065 );
1066 tail.compute_output_shape(&new_context, input_shape, dollar_shape)
1067 }
1068
1069 Some(ShapeCase::Error(shape::Error { message, partial })) => {
1070 if partial.is_some() {
1071 let tail_shape = compute_tail_shape(
1072 tail,
1073 partial,
1074 context,
1075 input_shape,
1076 dollar_shape,
1077 );
1078
1079 Shape::error_with_partial(
1080 message.clone(),
1081 tail_shape,
1082 tail.shape_location(context.source_id()),
1083 )
1084 } else {
1085 Shape::error(message.clone(), tail.shape_location(context.source_id()))
1086 }
1087 }
1088
1089 _ => tail.compute_output_shape(context, input_shape, dollar_shape),
1090 }
1091 }
1092
1093 compute_tail_shape(
1094 tail,
1095 &extra_vars_opt,
1096 context,
1097 current_shape,
1100 dollar_shape,
1101 )
1102 } else {
1103 current_shape
1104 }
1105 }
1106}
1107
1108impl ApplyToInternal for WithRange<LitExpr> {
1109 fn apply_to_path(
1110 &self,
1111 data: &JSON,
1112 vars: &VarsWithPathsMap,
1113 input_path: &InputPath<JSON>,
1114 spec: ConnectSpec,
1115 ) -> (Option<JSON>, Vec<ApplyToError>) {
1116 match self.as_ref() {
1117 LitExpr::String(s) => (Some(JSON::String(s.clone().into())), vec![]),
1118 LitExpr::Number(n) => (Some(JSON::Number(n.clone())), vec![]),
1119 LitExpr::Bool(b) => (Some(JSON::Bool(*b)), vec![]),
1120 LitExpr::Null => (Some(JSON::Null), vec![]),
1121 LitExpr::LegacyObject(map) => {
1122 let mut output = JSONMap::with_capacity(map.len());
1123 let mut errors = Vec::new();
1124 for (key, value) in map {
1125 let (value_opt, apply_errors) =
1126 value.apply_to_path(data, vars, input_path, spec);
1127 errors.extend(apply_errors);
1128 if let Some(value_json) = value_opt {
1129 output.insert(key.as_str(), value_json);
1130 }
1131 }
1132 (Some(JSON::Object(output)), errors)
1133 }
1134 LitExpr::Object(sub) => {
1135 let (output, errors) = sub.apply_selections_no_rebind(data, vars, input_path, spec);
1141 (Some(output), errors)
1142 }
1143 LitExpr::Array(vec) => {
1144 let mut output = Vec::with_capacity(vec.len());
1145 let mut errors = Vec::new();
1146 for value in vec {
1147 let (value_opt, apply_errors) =
1148 value.apply_to_path(data, vars, input_path, spec);
1149 errors.extend(apply_errors);
1150 output.push(value_opt.unwrap_or(JSON::Null));
1151 }
1152 (Some(JSON::Array(output)), errors)
1153 }
1154 LitExpr::Path(path) => path.apply_to_path(data, vars, input_path, spec),
1155 LitExpr::LitPath(literal, subpath) => literal
1156 .apply_to_path(data, vars, input_path, spec)
1157 .and_then_collecting_errors(|value| {
1158 subpath.apply_to_path(value, vars, input_path, spec)
1159 }),
1160 LitExpr::OpChain(op, operands) => {
1161 match op.as_ref() {
1162 LitOp::NullishCoalescing => {
1163 let mut accumulated_errors = Vec::new();
1166 let mut last_value: Option<JSON> = None;
1167
1168 for operand in operands {
1169 let (value, errors) =
1170 operand.apply_to_path(data, vars, input_path, spec);
1171
1172 match value {
1173 Some(JSON::Null) | None => {
1175 accumulated_errors.extend(errors);
1177 last_value = value;
1178 continue;
1179 }
1180 Some(value) => {
1181 return (Some(value), errors);
1183 }
1184 }
1185 }
1186
1187 if last_value.is_none() {
1193 (None, accumulated_errors)
1196 } else {
1197 (last_value, Vec::new())
1202 }
1203 }
1204
1205 LitOp::NoneCoalescing => {
1206 let mut accumulated_errors = Vec::new();
1209
1210 for operand in operands {
1211 let (value, errors) =
1212 operand.apply_to_path(data, vars, input_path, spec);
1213
1214 match value {
1215 None => {
1217 accumulated_errors.extend(errors);
1218 continue;
1219 }
1220 Some(value) => {
1222 return (Some(value), errors);
1223 }
1224 }
1225 }
1226
1227 (None, accumulated_errors)
1229 }
1230 }
1231 }
1232 }
1233 }
1234
1235 fn compute_output_shape(
1236 &self,
1237 context: &ShapeContext,
1238 input_shape: Shape,
1239 dollar_shape: Shape,
1240 ) -> Shape {
1241 let locations = self.shape_location(context.source_id());
1242
1243 match self.as_ref() {
1244 LitExpr::Null => Shape::null(locations),
1245 LitExpr::Bool(value) => Shape::bool_value(*value, locations),
1246 LitExpr::String(value) => Shape::string_value(value.as_str(), locations),
1247
1248 LitExpr::Number(value) => {
1249 if let Some(n) = value.as_i64() {
1250 Shape::int_value(n, locations)
1251 } else if value.is_f64() {
1252 Shape::float(locations)
1253 } else {
1254 Shape::error("Number neither Int nor Float", locations)
1255 }
1256 }
1257
1258 LitExpr::LegacyObject(map) => {
1259 let mut fields = Shape::empty_map();
1260 for (key, value) in map {
1261 fields.insert(
1262 key.as_string(),
1263 value.compute_output_shape(
1264 context,
1265 input_shape.clone(),
1266 dollar_shape.clone(),
1267 ),
1268 );
1269 }
1270 Shape::object(fields, Shape::none(), locations)
1271 }
1272
1273 LitExpr::Object(sub) => {
1274 sub.compute_selections_shape_no_rebind(context, input_shape, dollar_shape)
1278 }
1279
1280 LitExpr::Array(vec) => {
1281 let mut shapes = Vec::with_capacity(vec.len());
1282 for value in vec {
1283 shapes.push(value.compute_output_shape(
1284 context,
1285 input_shape.clone(),
1286 dollar_shape.clone(),
1287 ));
1288 }
1289 Shape::array(shapes, Shape::none(), locations)
1290 }
1291
1292 LitExpr::Path(path) => path.compute_output_shape(context, input_shape, dollar_shape),
1293
1294 LitExpr::LitPath(literal, subpath) => {
1295 let literal_shape =
1296 literal.compute_output_shape(context, input_shape, dollar_shape.clone());
1297 subpath.compute_output_shape(context, literal_shape, dollar_shape)
1298 }
1299
1300 LitExpr::OpChain(op, operands) => {
1301 let mut shapes: Vec<Shape> = operands
1302 .iter()
1303 .map(|operand| {
1304 operand.compute_output_shape(
1305 context,
1306 input_shape.clone(),
1307 dollar_shape.clone(),
1308 )
1309 })
1310 .collect();
1311
1312 match op.as_ref() {
1313 LitOp::NullishCoalescing => {
1314 if let Some(last_shape) = shapes.pop() {
1315 let mut new_shapes = shapes
1316 .iter()
1317 .map(|shape| {
1318 shape
1319 .question(locations.clone())
1320 .not_none(locations.clone())
1321 })
1322 .collect::<Vec<_>>();
1323 new_shapes.push(last_shape);
1324 Shape::one(new_shapes, locations)
1325 } else {
1326 Shape::one(shapes, locations)
1327 }
1328 }
1329
1330 LitOp::NoneCoalescing => {
1332 if let Some(last_shape) = shapes.pop() {
1333 let mut new_shapes = shapes
1334 .iter()
1335 .map(|shape| shape.not_none(locations.clone()))
1336 .collect::<Vec<_>>();
1337 new_shapes.push(last_shape);
1338 Shape::one(new_shapes, locations)
1339 } else {
1340 Shape::one(shapes, locations)
1341 }
1342 }
1343 }
1344 }
1345 }
1346 }
1347}
1348
1349impl SubSelection {
1350 fn apply_selections_no_rebind(
1357 &self,
1358 data: &JSON,
1359 vars: &VarsWithPathsMap,
1360 input_path: &InputPath<JSON>,
1361 spec: ConnectSpec,
1362 ) -> (JSON, Vec<ApplyToError>) {
1363 let mut output = JSON::Object(JSONMap::new());
1364 let mut errors = Vec::new();
1365
1366 for named_selection in self.selections.iter() {
1367 let (named_output_opt, apply_errors) =
1368 named_selection.apply_to_path(data, vars, input_path, spec);
1369 errors.extend(apply_errors);
1370
1371 let (merged, merge_errors) = json_merge(Some(&output), named_output_opt.as_ref());
1372
1373 errors.extend(merge_errors.into_iter().map(|message| {
1374 ApplyToError::new(message, input_path.to_vec(), self.range(), spec)
1375 }));
1376
1377 if let Some(merged) = merged {
1378 output = merged;
1379 }
1380 }
1381
1382 (output, errors)
1383 }
1384
1385 fn compute_selections_shape_no_rebind(
1389 &self,
1390 context: &ShapeContext,
1391 input_shape: Shape,
1392 dollar_shape: Shape,
1393 ) -> Shape {
1394 let locations = self.shape_location(context.source_id());
1395 let mut all_shape = Shape::unknown([]);
1396
1397 for named_selection in self.selections.iter() {
1398 all_shape = Shape::all(
1403 [
1404 all_shape,
1405 named_selection.compute_output_shape(
1406 context,
1407 input_shape.clone(),
1408 dollar_shape.clone(),
1409 ),
1410 ],
1411 locations.clone(),
1412 );
1413
1414 if all_shape.is_null() {
1418 break;
1419 }
1420 }
1421
1422 if all_shape.is_unknown() {
1423 Shape::empty_object(locations)
1424 } else {
1425 all_shape
1426 }
1427 }
1428}
1429
1430impl ApplyToInternal for SubSelection {
1431 fn apply_to_path(
1432 &self,
1433 data: &JSON,
1434 vars: &VarsWithPathsMap,
1435 input_path: &InputPath<JSON>,
1436 spec: ConnectSpec,
1437 ) -> (Option<JSON>, Vec<ApplyToError>) {
1438 if let JSON::Array(array) = data {
1439 return self.apply_to_array(array, vars, input_path, spec);
1440 }
1441
1442 let vars: VarsWithPathsMap = {
1443 let mut vars = vars.clone();
1444 vars.insert(KnownVariable::Dollar, (data, input_path.clone()));
1445 vars
1446 };
1447
1448 let (output, errors) = self.apply_selections_no_rebind(data, &vars, input_path, spec);
1449
1450 if !matches!(data, JSON::Object(_)) {
1451 let output_is_empty = match &output {
1452 JSON::Object(map) => map.is_empty(),
1453 _ => false,
1454 };
1455 if output_is_empty {
1456 return (Some(data.clone()), errors);
1460 }
1461 }
1462
1463 (Some(output), errors)
1464 }
1465
1466 fn compute_output_shape(
1467 &self,
1468 context: &ShapeContext,
1469 input_shape: Shape,
1470 _previous_dollar_shape: Shape,
1471 ) -> Shape {
1472 if let ShapeCase::Array { prefix, tail } = input_shape.case() {
1476 let new_prefix = prefix
1477 .iter()
1478 .map(|shape| self.compute_output_shape(context, shape.clone(), shape.clone()))
1479 .collect::<Vec<_>>();
1480
1481 let new_tail = if tail.is_none() {
1482 tail.clone()
1483 } else {
1484 self.compute_output_shape(context, tail.clone(), tail.clone())
1485 };
1486
1487 return Shape::array(
1488 new_prefix,
1489 new_tail,
1490 self.shape_location(context.source_id()),
1491 );
1492 }
1493
1494 let input_shape = input_shape.any_item(Vec::new());
1498
1499 let dollar_shape = input_shape.clone();
1502
1503 self.compute_selections_shape_no_rebind(context, input_shape, dollar_shape)
1504 }
1505}
1506
1507impl WithRange<PathList> {
1508 pub(crate) fn compute_consumption_trie(&self, namespace: &str) -> SelectionTrie {
1517 let context = ShapeContext::new(SourceId::Other("VariableReference".into()));
1518 let var_shape = Shape::name(namespace, Vec::new());
1519
1520 let _ = self.compute_output_shape(&context, var_shape.clone(), var_shape);
1523
1524 context
1525 .consumption()
1526 .borrow()
1527 .get(namespace)
1528 .cloned()
1529 .unwrap_or_else(SelectionTrie::new)
1530 }
1531}
1532
1533fn field(shape: &Shape, key: &WithRange<Key>, source_id: &SourceId) -> Shape {
1535 if let ShapeCase::One(inner) = shape.case() {
1536 let mut new_fields = Vec::new();
1537 for inner_field in inner.iter() {
1538 new_fields.push(field(inner_field, key, source_id));
1539 }
1540 return Shape::one(new_fields, shape.locations().cloned());
1541 }
1542 if shape.is_none() || shape.is_null() {
1543 return Shape::none();
1544 }
1545 let field_shape = shape.field(key.as_str(), key.shape_location(source_id));
1546 if field_shape.is_none() {
1547 return Shape::error(
1548 format!("field `{field}` not found", field = key.as_str()),
1549 key.shape_location(source_id),
1550 );
1551 }
1552 field_shape
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use rstest::rstest;
1558
1559 use super::*;
1560 use crate::assert_debug_snapshot;
1561 use crate::connectors::json_selection::PrettyPrintable;
1562 use crate::selection;
1563
1564 #[rstest]
1565 #[case::v0_2(ConnectSpec::V0_2)]
1566 #[case::v0_3(ConnectSpec::V0_3)]
1567 #[case::v0_4(ConnectSpec::V0_4)]
1568 fn test_apply_to_selection(#[case] spec: ConnectSpec) {
1569 let data = json!({
1570 "hello": "world",
1571 "nested": {
1572 "hello": "world",
1573 "world": "hello",
1574 },
1575 "array": [
1576 { "hello": "world 0" },
1577 { "hello": "world 1" },
1578 { "hello": "world 2" },
1579 ],
1580 });
1581
1582 #[track_caller]
1583 fn check_ok(data: &JSON, selection: JSONSelection, expected_json: JSON) {
1584 let (actual_json, errors) = selection.apply_to(data);
1585 assert_eq!(actual_json, Some(expected_json));
1586 assert_eq!(errors, vec![]);
1587 }
1588
1589 check_ok(&data, selection!("hello", spec), json!({"hello": "world"}));
1590
1591 check_ok(
1592 &data,
1593 selection!("nested", spec),
1594 json!({
1595 "nested": {
1596 "hello": "world",
1597 "world": "hello",
1598 },
1599 }),
1600 );
1601
1602 check_ok(&data, selection!("nested.hello", spec), json!("world"));
1603 check_ok(&data, selection!("$.nested.hello", spec), json!("world"));
1604
1605 check_ok(&data, selection!("nested.world", spec), json!("hello"));
1606 check_ok(&data, selection!("$.nested.world", spec), json!("hello"));
1607
1608 check_ok(
1609 &data,
1610 selection!("nested hello", spec),
1611 json!({
1612 "hello": "world",
1613 "nested": {
1614 "hello": "world",
1615 "world": "hello",
1616 },
1617 }),
1618 );
1619
1620 check_ok(
1621 &data,
1622 selection!("array { hello }", spec),
1623 json!({
1624 "array": [
1625 { "hello": "world 0" },
1626 { "hello": "world 1" },
1627 { "hello": "world 2" },
1628 ],
1629 }),
1630 );
1631
1632 check_ok(
1633 &data,
1634 selection!("greetings: array { hello }", spec),
1635 json!({
1636 "greetings": [
1637 { "hello": "world 0" },
1638 { "hello": "world 1" },
1639 { "hello": "world 2" },
1640 ],
1641 }),
1642 );
1643
1644 check_ok(
1645 &data,
1646 selection!("$.array { hello }", spec),
1647 json!([
1648 { "hello": "world 0" },
1649 { "hello": "world 1" },
1650 { "hello": "world 2" },
1651 ]),
1652 );
1653
1654 check_ok(
1655 &data,
1656 selection!("worlds: array.hello", spec),
1657 json!({
1658 "worlds": [
1659 "world 0",
1660 "world 1",
1661 "world 2",
1662 ],
1663 }),
1664 );
1665
1666 check_ok(
1667 &data,
1668 selection!("worlds: $.array.hello", spec),
1669 json!({
1670 "worlds": [
1671 "world 0",
1672 "world 1",
1673 "world 2",
1674 ],
1675 }),
1676 );
1677
1678 check_ok(
1679 &data,
1680 selection!("array.hello", spec),
1681 json!(["world 0", "world 1", "world 2",]),
1682 );
1683
1684 check_ok(
1685 &data,
1686 selection!("$.array.hello", spec),
1687 json!(["world 0", "world 1", "world 2",]),
1688 );
1689
1690 check_ok(
1691 &data,
1692 selection!("nested grouped: { hello worlds: array.hello }", spec),
1693 json!({
1694 "nested": {
1695 "hello": "world",
1696 "world": "hello",
1697 },
1698 "grouped": {
1699 "hello": "world",
1700 "worlds": [
1701 "world 0",
1702 "world 1",
1703 "world 2",
1704 ],
1705 },
1706 }),
1707 );
1708
1709 check_ok(
1710 &data,
1711 selection!("nested grouped: { hello worlds: $.array.hello }", spec),
1712 json!({
1713 "nested": {
1714 "hello": "world",
1715 "world": "hello",
1716 },
1717 "grouped": {
1718 "hello": "world",
1719 "worlds": [
1720 "world 0",
1721 "world 1",
1722 "world 2",
1723 ],
1724 },
1725 }),
1726 );
1727 }
1728
1729 #[rstest]
1730 #[case::v0_2(ConnectSpec::V0_2)]
1731 #[case::v0_3(ConnectSpec::V0_3)]
1732 #[case::v0_4(ConnectSpec::V0_4)]
1733 fn test_apply_to_errors(#[case] spec: ConnectSpec) {
1734 let data = json!({
1735 "hello": "world",
1736 "nested": {
1737 "hello": 123,
1738 "world": true,
1739 },
1740 "array": [
1741 { "hello": 1, "goodbye": "farewell" },
1742 { "hello": "two" },
1743 { "hello": 3.0, "smello": "yellow" },
1744 ],
1745 });
1746
1747 assert_eq!(
1748 selection!("hello", spec).apply_to(&data),
1749 (Some(json!({"hello": "world"})), vec![],)
1750 );
1751
1752 fn make_yellow_errors_expected(
1753 yellow_range: std::ops::Range<usize>,
1754 spec: ConnectSpec,
1755 ) -> Vec<ApplyToError> {
1756 vec![ApplyToError::new(
1757 "Property .yellow not found in object".to_string(),
1758 vec![json!("yellow")],
1759 Some(yellow_range),
1760 spec,
1761 )]
1762 }
1763 assert_eq!(
1764 selection!("yellow", spec).apply_to(&data),
1765 (Some(json!({})), make_yellow_errors_expected(0..6, spec)),
1766 );
1767 assert_eq!(
1768 selection!("$.yellow", spec).apply_to(&data),
1769 (None, make_yellow_errors_expected(2..8, spec)),
1770 );
1771
1772 assert_eq!(
1773 selection!("nested.hello", spec).apply_to(&data),
1774 (Some(json!(123)), vec![],)
1775 );
1776
1777 fn make_quoted_yellow_expected(
1778 yellow_range: std::ops::Range<usize>,
1779 spec: ConnectSpec,
1780 ) -> (Option<JSON>, Vec<ApplyToError>) {
1781 (
1782 None,
1783 vec![ApplyToError::new(
1784 "Property .\"yellow\" not found in object".to_string(),
1785 vec![json!("nested"), json!("yellow")],
1786 Some(yellow_range),
1787 spec,
1788 )],
1789 )
1790 }
1791 assert_eq!(
1792 selection!("nested.'yellow'", spec).apply_to(&data),
1793 make_quoted_yellow_expected(7..15, spec),
1794 );
1795 assert_eq!(
1796 selection!("nested.\"yellow\"", spec).apply_to(&data),
1797 make_quoted_yellow_expected(7..15, spec),
1798 );
1799 assert_eq!(
1800 selection!("$.nested.'yellow'", spec).apply_to(&data),
1801 make_quoted_yellow_expected(9..17, spec),
1802 );
1803
1804 fn make_nested_path_expected(
1805 hola_range: (usize, usize),
1806 yellow_range: (usize, usize),
1807 spec: ConnectSpec,
1808 ) -> (Option<JSON>, Vec<ApplyToError>) {
1809 (
1810 Some(json!({
1811 "world": true,
1812 })),
1813 vec![
1814 ApplyToError::from_json(&json!({
1815 "message": "Property .hola not found in object",
1816 "path": ["nested", "hola"],
1817 "range": hola_range,
1818 "spec": spec.to_string(),
1819 })),
1820 ApplyToError::from_json(&json!({
1821 "message": "Property .yellow not found in object",
1822 "path": ["nested", "yellow"],
1823 "range": yellow_range,
1824 "spec": spec.to_string(),
1825 })),
1826 ],
1827 )
1828 }
1829 assert_eq!(
1830 selection!("$.nested { hola yellow world }", spec).apply_to(&data),
1831 make_nested_path_expected((11, 15), (16, 22), spec),
1832 );
1833 assert_eq!(
1834 selection!(" $ . nested { hola yellow world } ", spec).apply_to(&data),
1835 make_nested_path_expected((14, 18), (19, 25), spec),
1836 );
1837
1838 fn make_partial_array_expected(
1839 goodbye_range: (usize, usize),
1840 spec: ConnectSpec,
1841 ) -> (Option<JSON>, Vec<ApplyToError>) {
1842 (
1843 Some(json!({
1844 "partial": [
1845 { "hello": 1, "goodbye": "farewell" },
1846 { "hello": "two" },
1847 { "hello": 3.0 },
1848 ],
1849 })),
1850 vec![
1851 ApplyToError::from_json(&json!({
1852 "message": "Property .goodbye not found in object",
1853 "path": ["array", 1, "goodbye"],
1854 "range": goodbye_range,
1855 "spec": spec.to_string(),
1856 })),
1857 ApplyToError::from_json(&json!({
1858 "message": "Property .goodbye not found in object",
1859 "path": ["array", 2, "goodbye"],
1860 "range": goodbye_range,
1861 "spec": spec.to_string(),
1862 })),
1863 ],
1864 )
1865 }
1866 assert_eq!(
1867 selection!("partial: $.array { hello goodbye }", spec).apply_to(&data),
1868 make_partial_array_expected((25, 32), spec),
1869 );
1870 assert_eq!(
1871 selection!(" partial : $ . array { hello goodbye } ", spec).apply_to(&data),
1872 make_partial_array_expected((29, 36), spec),
1873 );
1874
1875 assert_eq!(
1876 selection!("good: array.hello bad: array.smello", spec).apply_to(&data),
1877 (
1878 Some(json!({
1879 "good": [
1880 1,
1881 "two",
1882 3.0,
1883 ],
1884 "bad": [
1885 null,
1886 null,
1887 "yellow",
1888 ],
1889 })),
1890 vec![
1891 ApplyToError::from_json(&json!({
1892 "message": "Property .smello not found in object",
1893 "path": ["array", 0, "smello"],
1894 "range": [29, 35],
1895 "spec": spec.to_string(),
1896 })),
1897 ApplyToError::from_json(&json!({
1898 "message": "Property .smello not found in object",
1899 "path": ["array", 1, "smello"],
1900 "range": [29, 35],
1901 "spec": spec.to_string(),
1902 })),
1903 ],
1904 )
1905 );
1906
1907 assert_eq!(
1908 selection!("array { hello smello }", spec).apply_to(&data),
1909 (
1910 Some(json!({
1911 "array": [
1912 { "hello": 1 },
1913 { "hello": "two" },
1914 { "hello": 3.0, "smello": "yellow" },
1915 ],
1916 })),
1917 vec![
1918 ApplyToError::from_json(&json!({
1919 "message": "Property .smello not found in object",
1920 "path": ["array", 0, "smello"],
1921 "range": [14, 20],
1922 "spec": spec.to_string(),
1923 })),
1924 ApplyToError::from_json(&json!({
1925 "message": "Property .smello not found in object",
1926 "path": ["array", 1, "smello"],
1927 "range": [14, 20],
1928 "spec": spec.to_string(),
1929 })),
1930 ],
1931 )
1932 );
1933
1934 assert_eq!(
1935 selection!("$.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1936 (
1937 Some(json!({
1938 "grouped": {
1939 "hello": 123,
1940 "world": true,
1941 },
1942 })),
1943 vec![ApplyToError::from_json(&json!({
1944 "message": "Property .smelly not found in object",
1945 "path": ["nested", "smelly"],
1946 "range": [28, 34],
1947 "spec": spec.to_string(),
1948 }))],
1949 )
1950 );
1951
1952 assert_eq!(
1953 selection!("alias: $.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1954 (
1955 Some(json!({
1956 "alias": {
1957 "grouped": {
1958 "hello": 123,
1959 "world": true,
1960 },
1961 },
1962 })),
1963 vec![ApplyToError::from_json(&json!({
1964 "message": "Property .smelly not found in object",
1965 "path": ["nested", "smelly"],
1966 "range": [35, 41],
1967 "spec": spec.to_string(),
1968 }))],
1969 )
1970 );
1971 }
1972
1973 #[rstest]
1974 #[case::v0_2(ConnectSpec::V0_2)]
1975 #[case::v0_3(ConnectSpec::V0_3)]
1976 #[case::v0_4(ConnectSpec::V0_4)]
1977 fn test_apply_to_nested_arrays(#[case] spec: ConnectSpec) {
1978 let data = json!({
1979 "arrayOfArrays": [
1980 [
1981 { "x": 0, "y": 0 },
1982 ],
1983 [
1984 { "x": 1, "y": 0 },
1985 { "x": 1, "y": 1 },
1986 { "x": 1, "y": 2 },
1987 ],
1988 [
1989 { "x": 2, "y": 0 },
1990 { "x": 2, "y": 1 },
1991 ],
1992 [],
1993 [
1994 null,
1995 { "x": 4, "y": 1 },
1996 { "x": 4, "why": 2 },
1997 null,
1998 { "x": 4, "y": 4 },
1999 ]
2000 ],
2001 });
2002
2003 fn make_array_of_arrays_x_expected(
2004 x_range: (usize, usize),
2005 spec: ConnectSpec,
2006 ) -> (Option<JSON>, Vec<ApplyToError>) {
2007 (
2008 Some(json!([[0], [1, 1, 1], [2, 2], [], [null, 4, 4, null, 4]])),
2009 vec![
2010 ApplyToError::from_json(&json!({
2011 "message": "Property .x not found in null",
2012 "path": ["arrayOfArrays", 4, 0, "x"],
2013 "range": x_range,
2014 "spec": spec.to_string(),
2015 })),
2016 ApplyToError::from_json(&json!({
2017 "message": "Property .x not found in null",
2018 "path": ["arrayOfArrays", 4, 3, "x"],
2019 "range": x_range,
2020 "spec": spec.to_string(),
2021 })),
2022 ],
2023 )
2024 }
2025 assert_eq!(
2026 selection!("arrayOfArrays.x", spec).apply_to(&data),
2027 make_array_of_arrays_x_expected((14, 15), spec),
2028 );
2029 assert_eq!(
2030 selection!("$.arrayOfArrays.x", spec).apply_to(&data),
2031 make_array_of_arrays_x_expected((16, 17), spec),
2032 );
2033
2034 fn make_array_of_arrays_y_expected(
2035 y_range: (usize, usize),
2036 spec: ConnectSpec,
2037 ) -> (Option<JSON>, Vec<ApplyToError>) {
2038 (
2039 Some(json!([
2040 [0],
2041 [0, 1, 2],
2042 [0, 1],
2043 [],
2044 [null, 1, null, null, 4],
2045 ])),
2046 vec![
2047 ApplyToError::from_json(&json!({
2048 "message": "Property .y not found in null",
2049 "path": ["arrayOfArrays", 4, 0, "y"],
2050 "range": y_range,
2051 "spec": spec.to_string(),
2052 })),
2053 ApplyToError::from_json(&json!({
2054 "message": "Property .y not found in object",
2055 "path": ["arrayOfArrays", 4, 2, "y"],
2056 "range": y_range,
2057 "spec": spec.to_string(),
2058 })),
2059 ApplyToError::from_json(&json!({
2060 "message": "Property .y not found in null",
2061 "path": ["arrayOfArrays", 4, 3, "y"],
2062 "range": y_range,
2063 "spec": spec.to_string(),
2064 })),
2065 ],
2066 )
2067 }
2068 assert_eq!(
2069 selection!("arrayOfArrays.y", spec).apply_to(&data),
2070 make_array_of_arrays_y_expected((14, 15), spec),
2071 );
2072 assert_eq!(
2073 selection!("$.arrayOfArrays.y", spec).apply_to(&data),
2074 make_array_of_arrays_y_expected((16, 17), spec),
2075 );
2076
2077 assert_eq!(
2078 selection!("alias: arrayOfArrays { x y }", spec).apply_to(&data),
2079 (
2080 Some(json!({
2081 "alias": [
2082 [
2083 { "x": 0, "y": 0 },
2084 ],
2085 [
2086 { "x": 1, "y": 0 },
2087 { "x": 1, "y": 1 },
2088 { "x": 1, "y": 2 },
2089 ],
2090 [
2091 { "x": 2, "y": 0 },
2092 { "x": 2, "y": 1 },
2093 ],
2094 [],
2095 [
2096 null,
2097 { "x": 4, "y": 1 },
2098 { "x": 4 },
2099 null,
2100 { "x": 4, "y": 4 },
2101 ]
2102 ],
2103 })),
2104 vec![
2105 ApplyToError::from_json(&json!({
2106 "message": "Property .x not found in null",
2107 "path": ["arrayOfArrays", 4, 0, "x"],
2108 "range": [23, 24],
2109 "spec": spec.to_string(),
2110 })),
2111 ApplyToError::from_json(&json!({
2112 "message": "Property .y not found in null",
2113 "path": ["arrayOfArrays", 4, 0, "y"],
2114 "range": [25, 26],
2115 "spec": spec.to_string(),
2116 })),
2117 ApplyToError::from_json(&json!({
2118 "message": "Property .y not found in object",
2119 "path": ["arrayOfArrays", 4, 2, "y"],
2120 "range": [25, 26],
2121 "spec": spec.to_string(),
2122 })),
2123 ApplyToError::from_json(&json!({
2124 "message": "Property .x not found in null",
2125 "path": ["arrayOfArrays", 4, 3, "x"],
2126 "range": [23, 24],
2127 "spec": spec.to_string(),
2128 })),
2129 ApplyToError::from_json(&json!({
2130 "message": "Property .y not found in null",
2131 "path": ["arrayOfArrays", 4, 3, "y"],
2132 "range": [25, 26],
2133 "spec": spec.to_string(),
2134 })),
2135 ],
2136 ),
2137 );
2138
2139 fn make_array_of_arrays_x_y_expected(
2140 x_range: (usize, usize),
2141 y_range: (usize, usize),
2142 spec: ConnectSpec,
2143 ) -> (Option<JSON>, Vec<ApplyToError>) {
2144 (
2145 Some(json!({
2146 "ys": [
2147 [0],
2148 [0, 1, 2],
2149 [0, 1],
2150 [],
2151 [null, 1, null, null, 4],
2152 ],
2153 "xs": [
2154 [0],
2155 [1, 1, 1],
2156 [2, 2],
2157 [],
2158 [null, 4, 4, null, 4],
2159 ],
2160 })),
2161 vec![
2162 ApplyToError::from_json(&json!({
2163 "message": "Property .y not found in null",
2164 "path": ["arrayOfArrays", 4, 0, "y"],
2165 "range": y_range,
2166 "spec": spec.to_string(),
2167 })),
2168 ApplyToError::from_json(&json!({
2169 "message": "Property .y not found in object",
2170 "path": ["arrayOfArrays", 4, 2, "y"],
2171 "range": y_range,
2172 "spec": spec.to_string(),
2173 })),
2174 ApplyToError::from_json(&json!({
2175 "path": ["arrayOfArrays", 4, 3, "y"],
2178 "message": "Property .y not found in null",
2179 "range": y_range,
2180 "spec": spec.to_string(),
2181 })),
2182 ApplyToError::from_json(&json!({
2183 "message": "Property .x not found in null",
2184 "path": ["arrayOfArrays", 4, 0, "x"],
2185 "range": x_range,
2186 "spec": spec.to_string(),
2187 })),
2188 ApplyToError::from_json(&json!({
2189 "message": "Property .x not found in null",
2190 "path": ["arrayOfArrays", 4, 3, "x"],
2191 "range": x_range,
2192 "spec": spec.to_string(),
2193 })),
2194 ],
2195 )
2196 }
2197 assert_eq!(
2198 selection!("ys: arrayOfArrays.y xs: arrayOfArrays.x", spec).apply_to(&data),
2199 make_array_of_arrays_x_y_expected((38, 39), (18, 19), spec),
2200 );
2201 assert_eq!(
2202 selection!("ys: $.arrayOfArrays.y xs: $.arrayOfArrays.x", spec).apply_to(&data),
2203 make_array_of_arrays_x_y_expected((42, 43), (20, 21), spec),
2204 );
2205 }
2206
2207 #[rstest]
2208 #[case::v0_2(ConnectSpec::V0_2)]
2209 #[case::v0_3(ConnectSpec::V0_3)]
2210 #[case::v0_4(ConnectSpec::V0_4)]
2211 fn test_apply_to_variable_expressions(#[case] spec: ConnectSpec) {
2212 let id_object = selection!("id: $", spec).apply_to(&json!(123));
2213 assert_eq!(id_object, (Some(json!({"id": 123})), vec![]));
2214
2215 let data = json!({
2216 "id": 123,
2217 "name": "Ben",
2218 "friend_ids": [234, 345, 456]
2219 });
2220
2221 assert_eq!(
2222 selection!("id name friends: friend_ids { id: $ }", spec).apply_to(&data),
2223 (
2224 Some(json!({
2225 "id": 123,
2226 "name": "Ben",
2227 "friends": [
2228 { "id": 234 },
2229 { "id": 345 },
2230 { "id": 456 },
2231 ],
2232 })),
2233 vec![],
2234 ),
2235 );
2236
2237 let mut vars = IndexMap::default();
2238 vars.insert("$args".to_string(), json!({ "id": "id from args" }));
2239 assert_eq!(
2240 selection!("id: $args.id name", spec).apply_with_vars(&data, &vars),
2241 (
2242 Some(json!({
2243 "id": "id from args",
2244 "name": "Ben"
2245 })),
2246 vec![],
2247 ),
2248 );
2249 assert_eq!(
2250 selection!("nested.path { id: $args.id name }", spec).apply_to(&json!({
2251 "nested": {
2252 "path": data,
2253 },
2254 })),
2255 (
2256 Some(json!({
2257 "name": "Ben"
2258 })),
2259 vec![ApplyToError::from_json(&json!({
2260 "message": "Variable $args not found",
2261 "path": ["nested", "path"],
2262 "range": [18, 23],
2263 "spec": spec.to_string(),
2264 }))],
2265 ),
2266 );
2267 let mut vars_without_args_id = IndexMap::default();
2268 vars_without_args_id.insert("$args".to_string(), json!({ "unused": "ignored" }));
2269 assert_eq!(
2270 selection!("id: $args.id name", spec).apply_with_vars(&data, &vars_without_args_id),
2271 (
2272 Some(json!({
2273 "name": "Ben"
2274 })),
2275 vec![ApplyToError::from_json(&json!({
2276 "message": "Property .id not found in object",
2277 "path": ["$args", "id"],
2278 "range": [10, 12],
2279 "spec": spec.to_string(),
2280 }))],
2281 ),
2282 );
2283
2284 assert_eq!(
2286 selection!("$args.id", spec).apply_with_vars(&json!([1, 2, 3]), &vars),
2287 (Some(json!("id from args")), vec![]),
2288 );
2289 }
2290
2291 #[test]
2292 fn test_apply_to_variable_expressions_typename() {
2293 let typename_object =
2294 selection!("__typename: $->echo('Product') reviews { __typename: $->echo('Review') }")
2295 .apply_to(&json!({"reviews": [{}]}));
2296 assert_eq!(
2297 typename_object,
2298 (
2299 Some(json!({"__typename": "Product", "reviews": [{ "__typename": "Review" }] })),
2300 vec![]
2301 )
2302 );
2303 }
2304
2305 #[test]
2306 fn test_literal_expressions_in_parentheses() {
2307 assert_eq!(
2308 selection!("__typename: $('Product')").apply_to(&json!({})),
2309 (Some(json!({"__typename": "Product"})), vec![]),
2310 );
2311
2312 assert_eq!(
2313 selection!(" __typename : 'Product' ").apply_to(&json!({})),
2314 (
2315 Some(json!({})),
2316 vec![ApplyToError::new(
2317 "Property .\"Product\" not found in object".to_string(),
2318 vec![json!("Product")],
2319 Some(14..23),
2320 ConnectSpec::latest(),
2321 )],
2322 ),
2323 );
2324
2325 assert_eq!(
2326 selection!(
2327 r#"
2328 one: $(1)
2329 two: $(2)
2330 negativeThree: $(- 3)
2331 true: $(true )
2332 false: $( false)
2333 null: $(null)
2334 string: $("string")
2335 array: $( [ 1 , 2 , 3 ] )
2336 object: $( { "key" : "value" } )
2337 path: $(nested.path)
2338 "#
2339 )
2340 .apply_to(&json!({
2341 "nested": {
2342 "path": "nested path value"
2343 }
2344 })),
2345 (
2346 Some(json!({
2347 "one": 1,
2348 "two": 2,
2349 "negativeThree": -3,
2350 "true": true,
2351 "false": false,
2352 "null": null,
2353 "string": "string",
2354 "array": [1, 2, 3],
2355 "object": { "key": "value" },
2356 "path": "nested path value",
2357 })),
2358 vec![],
2359 ),
2360 );
2361
2362 assert_eq!(
2363 selection!(
2364 r#"
2365 one: $(1)->typeof
2366 two: $(2)->typeof
2367 negativeThree: $(-3)->typeof
2368 true: $(true)->typeof
2369 false: $(false)->typeof
2370 null: $(null)->typeof
2371 string: $("string")->typeof
2372 array: $([1, 2, 3])->typeof
2373 object: $({ "key": "value" })->typeof
2374 path: $(nested.path)->typeof
2375 "#
2376 )
2377 .apply_to(&json!({
2378 "nested": {
2379 "path": 12345
2380 }
2381 })),
2382 (
2383 Some(json!({
2384 "one": "number",
2385 "two": "number",
2386 "negativeThree": "number",
2387 "true": "boolean",
2388 "false": "boolean",
2389 "null": "null",
2390 "string": "string",
2391 "array": "array",
2392 "object": "object",
2393 "path": "number",
2394 })),
2395 vec![],
2396 ),
2397 );
2398
2399 assert_eq!(
2400 selection!(
2401 r#"
2402 items: $([
2403 1,
2404 -2.0,
2405 true,
2406 false,
2407 null,
2408 "string",
2409 [1, 2, 3],
2410 { "key": "value" },
2411 nested.path,
2412 ])->map(@->typeof)
2413 "#
2414 )
2415 .apply_to(&json!({
2416 "nested": {
2417 "path": { "deeply": "nested" }
2418 }
2419 })),
2420 (
2421 Some(json!({
2422 "items": [
2423 "number",
2424 "number",
2425 "boolean",
2426 "boolean",
2427 "null",
2428 "string",
2429 "array",
2430 "object",
2431 "object",
2432 ],
2433 })),
2434 vec![],
2435 ),
2436 );
2437
2438 assert_eq!(
2439 selection!(
2440 r#"
2441 $({
2442 one: 1,
2443 two: 2,
2444 negativeThree: -3,
2445 true: true,
2446 false: false,
2447 null: null,
2448 string: "string",
2449 array: [1, 2, 3],
2450 object: { "key": "value" },
2451 path: $ . nested . path ,
2452 })->entries
2453 "#
2454 )
2455 .apply_to(&json!({
2456 "nested": {
2457 "path": "nested path value"
2458 }
2459 })),
2460 (
2461 Some(json!([
2462 { "key": "one", "value": 1 },
2463 { "key": "two", "value": 2 },
2464 { "key": "negativeThree", "value": -3 },
2465 { "key": "true", "value": true },
2466 { "key": "false", "value": false },
2467 { "key": "null", "value": null },
2468 { "key": "string", "value": "string" },
2469 { "key": "array", "value": [1, 2, 3] },
2470 { "key": "object", "value": { "key": "value" } },
2471 { "key": "path", "value": "nested path value" },
2472 ])),
2473 vec![],
2474 ),
2475 );
2476
2477 assert_eq!(
2478 selection!(
2479 r#"
2480 $({
2481 string: $("string")->slice(1, 4),
2482 array: $([1, 2, 3])->map(@->add(10)),
2483 object: $({ "key": "value" })->get("key"),
2484 path: nested.path->slice($("nested ")->size),
2485 needlessParens: $("oyez"),
2486 withoutParens: "oyez",
2487 })
2488 "#
2489 )
2490 .apply_to(&json!({
2491 "nested": {
2492 "path": "nested path value"
2493 }
2494 })),
2495 (
2496 Some(json!({
2497 "string": "tri",
2498 "array": [11, 12, 13],
2499 "object": "value",
2500 "path": "path value",
2501 "needlessParens": "oyez",
2502 "withoutParens": "oyez",
2503 })),
2504 vec![],
2505 ),
2506 );
2507
2508 assert_eq!(
2509 selection!(
2510 r#"
2511 string: $("string")->slice(1, 4)
2512 array: $([1, 2, 3])->map(@->add(10))
2513 object: $({ "key": "value" })->get("key")
2514 path: nested.path->slice($("nested ")->size)
2515 "#
2516 )
2517 .apply_to(&json!({
2518 "nested": {
2519 "path": "nested path value"
2520 }
2521 })),
2522 (
2523 Some(json!({
2524 "string": "tri",
2525 "array": [11, 12, 13],
2526 "object": "value",
2527 "path": "path value",
2528 })),
2529 vec![],
2530 ),
2531 );
2532 }
2533
2534 #[rstest]
2535 #[case::v0_2(ConnectSpec::V0_2)]
2536 #[case::v0_3(ConnectSpec::V0_3)]
2537 #[case::v0_4(ConnectSpec::V0_4)]
2538 fn test_inline_paths_with_subselections(#[case] spec: ConnectSpec) {
2539 let data = json!({
2540 "id": 123,
2541 "created": "2021-01-01T00:00:00Z",
2542 "model": "gpt-4o",
2543 "choices": [{
2544 "index": 0,
2545 "message": {
2546 "role": "assistant",
2547 "content": "The capital of Australia is Canberra.",
2548 },
2549 }, {
2550 "index": 1,
2551 "message": {
2552 "role": "assistant",
2553 "content": "The capital of Australia is Sydney.",
2554 },
2555 }],
2556 });
2557
2558 {
2559 let expected = (
2560 Some(json!({
2561 "id": 123,
2562 "created": "2021-01-01T00:00:00Z",
2563 "model": "gpt-4o",
2564 "role": "assistant",
2565 "content": "The capital of Australia is Canberra.",
2566 })),
2567 vec![],
2568 );
2569
2570 assert_eq!(
2571 selection!(
2572 r#"
2573 id
2574 created
2575 model
2576 role: choices->first.message.role
2577 content: choices->first.message.content
2578 "#,
2579 spec
2580 )
2581 .apply_to(&data),
2582 expected,
2583 );
2584
2585 assert_eq!(
2586 selection!(
2587 r#"
2588 id
2589 created
2590 model
2591 choices->first.message {
2592 role
2593 content
2594 }
2595 "#,
2596 spec
2597 )
2598 .apply_to(&data),
2599 expected,
2600 );
2601
2602 assert_eq!(
2603 selection!(
2604 r#"
2605 id
2606 choices->first.message {
2607 role
2608 content
2609 }
2610 created
2611 model
2612 "#,
2613 spec
2614 )
2615 .apply_to(&data),
2616 expected,
2617 );
2618 }
2619
2620 {
2621 let expected = (
2622 Some(json!({
2623 "id": 123,
2624 "created": "2021-01-01T00:00:00Z",
2625 "model": "gpt-4o",
2626 "role": "assistant",
2627 "message": "The capital of Australia is Sydney.",
2628 })),
2629 vec![],
2630 );
2631
2632 assert_eq!(
2633 selection!(
2634 r#"
2635 id
2636 created
2637 model
2638 role: choices->last.message.role
2639 message: choices->last.message.content
2640 "#,
2641 spec
2642 )
2643 .apply_to(&data),
2644 expected,
2645 );
2646
2647 assert_eq!(
2648 selection!(
2649 r#"
2650 id
2651 created
2652 model
2653 choices->last.message {
2654 role
2655 message: content
2656 }
2657 "#,
2658 spec
2659 )
2660 .apply_to(&data),
2661 expected,
2662 );
2663
2664 assert_eq!(
2665 selection!(
2666 r#"
2667 created
2668 choices->last.message {
2669 message: content
2670 role
2671 }
2672 model
2673 id
2674 "#,
2675 spec
2676 )
2677 .apply_to(&data),
2678 expected,
2679 );
2680 }
2681
2682 {
2683 let expected = (
2684 Some(json!({
2685 "id": 123,
2686 "created": "2021-01-01T00:00:00Z",
2687 "model": "gpt-4o",
2688 "role": "assistant",
2689 "correct": "The capital of Australia is Canberra.",
2690 "incorrect": "The capital of Australia is Sydney.",
2691 })),
2692 vec![],
2693 );
2694
2695 assert_eq!(
2696 selection!(
2697 r#"
2698 id
2699 created
2700 model
2701 role: choices->first.message.role
2702 correct: choices->first.message.content
2703 incorrect: choices->last.message.content
2704 "#,
2705 spec
2706 )
2707 .apply_to(&data),
2708 expected,
2709 );
2710
2711 assert_eq!(
2712 selection!(
2713 r#"
2714 id
2715 created
2716 model
2717 choices->first.message {
2718 role
2719 correct: content
2720 }
2721 choices->last.message {
2722 incorrect: content
2723 }
2724 "#,
2725 spec
2726 )
2727 .apply_to(&data),
2728 expected,
2729 );
2730
2731 assert_eq!(
2732 selection!(
2733 r#"
2734 id
2735 created
2736 model
2737 choices->first.message {
2738 role
2739 correct: content
2740 }
2741 incorrect: choices->last.message.content
2742 "#,
2743 spec
2744 )
2745 .apply_to(&data),
2746 expected,
2747 );
2748
2749 assert_eq!(
2750 selection!(
2751 r#"
2752 id
2753 created
2754 model
2755 choices->first.message {
2756 correct: content
2757 }
2758 choices->last.message {
2759 role
2760 incorrect: content
2761 }
2762 "#,
2763 spec
2764 )
2765 .apply_to(&data),
2766 expected,
2767 );
2768
2769 assert_eq!(
2770 selection!(
2771 r#"
2772 id
2773 created
2774 correct: choices->first.message.content
2775 choices->last.message {
2776 role
2777 incorrect: content
2778 }
2779 model
2780 "#,
2781 spec
2782 )
2783 .apply_to(&data),
2784 expected,
2785 );
2786 }
2787
2788 {
2789 let data = json!({
2790 "from": "data",
2791 });
2792
2793 let vars = {
2794 let mut vars = IndexMap::default();
2795 vars.insert(
2796 "$this".to_string(),
2797 json!({
2798 "id": 1234,
2799 }),
2800 );
2801 vars.insert(
2802 "$args".to_string(),
2803 json!({
2804 "input": {
2805 "title": "The capital of Australia",
2806 "body": "Canberra",
2807 },
2808 "extra": "extra",
2809 }),
2810 );
2811 vars
2812 };
2813
2814 let expected = (
2815 Some(json!({
2816 "id": 1234,
2817 "title": "The capital of Australia",
2818 "body": "Canberra",
2819 "from": "data",
2820 })),
2821 vec![],
2822 );
2823
2824 assert_eq!(
2825 selection!(
2826 r#"
2827 id: $this.id
2828 $args.input {
2829 title
2830 body
2831 }
2832 from
2833 "#,
2834 spec
2835 )
2836 .apply_with_vars(&data, &vars),
2837 expected,
2838 );
2839
2840 assert_eq!(
2841 selection!(
2842 r#"
2843 from
2844 $args.input { title body }
2845 id: $this.id
2846 "#,
2847 spec
2848 )
2849 .apply_with_vars(&data, &vars),
2850 expected,
2851 );
2852
2853 assert_eq!(
2854 selection!(
2855 r#"
2856 $args.input { body title }
2857 from
2858 id: $this.id
2859 "#,
2860 spec
2861 )
2862 .apply_with_vars(&data, &vars),
2863 expected,
2864 );
2865
2866 assert_eq!(
2867 selection!(
2868 r#"
2869 id: $this.id
2870 $args { $.input { title body } }
2871 from
2872 "#,
2873 spec
2874 )
2875 .apply_with_vars(&data, &vars),
2876 expected,
2877 );
2878
2879 assert_eq!(
2880 selection!(
2881 r#"
2882 id: $this.id
2883 $args { $.input { title body } extra }
2884 from: $.from
2885 "#,
2886 spec
2887 )
2888 .apply_with_vars(&data, &vars),
2889 (
2890 Some(json!({
2891 "id": 1234,
2892 "title": "The capital of Australia",
2893 "body": "Canberra",
2894 "extra": "extra",
2895 "from": "data",
2896 })),
2897 vec![],
2898 ),
2899 );
2900
2901 assert_eq!(
2902 selection!(
2903 r#"
2904 # Equivalent to id: $this.id
2905 $this { id }
2906
2907 $args {
2908 __typename: $("Args")
2909
2910 # Requiring $. instead of just . prevents .input from
2911 # parsing as a key applied to the $("Args") string.
2912 $.input { title body }
2913
2914 extra
2915 }
2916
2917 from: $.from
2918 "#,
2919 spec
2920 )
2921 .apply_with_vars(&data, &vars),
2922 (
2923 Some(json!({
2924 "id": 1234,
2925 "title": "The capital of Australia",
2926 "body": "Canberra",
2927 "__typename": "Args",
2928 "extra": "extra",
2929 "from": "data",
2930 })),
2931 vec![],
2932 ),
2933 );
2934 }
2935 }
2936
2937 #[test]
2938 fn test_inline_path_errors() {
2939 {
2940 let data = json!({
2941 "id": 123,
2942 "created": "2021-01-01T00:00:00Z",
2943 "model": "gpt-4o",
2944 "choices": [{
2945 "message": "The capital of Australia is Canberra.",
2946 }, {
2947 "message": "The capital of Australia is Sydney.",
2948 }],
2949 });
2950
2951 let expected = (
2952 Some(json!({
2953 "id": 123,
2954 "created": "2021-01-01T00:00:00Z",
2955 "model": "gpt-4o",
2956 })),
2957 vec![
2958 ApplyToError::new(
2959 "Property .role not found in string".to_string(),
2960 vec![
2961 json!("choices"),
2962 json!("->first"),
2963 json!("message"),
2964 json!("role"),
2965 ],
2966 Some(123..127),
2967 ConnectSpec::latest(),
2968 ),
2969 ApplyToError::new(
2970 "Property .content not found in string".to_string(),
2971 vec![
2972 json!("choices"),
2973 json!("->first"),
2974 json!("message"),
2975 json!("content"),
2976 ],
2977 Some(128..135),
2978 ConnectSpec::latest(),
2979 ),
2980 ApplyToError::new(
2981 "Expected object or null, not string".to_string(),
2982 vec![],
2983 Some(98..137),
2987 ConnectSpec::latest(),
2988 ),
2989 ],
2990 );
2991
2992 assert_eq!(
2993 selection!(
2994 r#"
2995 id
2996 created
2997 model
2998 choices->first.message { role content }
2999 "#
3000 )
3001 .apply_to(&data),
3002 expected,
3003 );
3004 }
3005
3006 assert_eq!(
3007 selection!("id nested.path.nonexistent { name }").apply_to(&json!({
3008 "id": 2345,
3009 "nested": {
3010 "path": "nested path value",
3011 },
3012 })),
3013 (
3014 Some(json!({
3015 "id": 2345,
3016 })),
3017 vec![
3018 ApplyToError::new(
3019 "Property .nonexistent not found in string".to_string(),
3020 vec![json!("nested"), json!("path"), json!("nonexistent")],
3021 Some(15..26),
3022 ConnectSpec::latest(),
3023 ),
3024 ApplyToError::new(
3025 "Inlined path produced no value".to_string(),
3026 vec![],
3027 Some(3..35),
3030 ConnectSpec::latest(),
3031 ),
3032 ],
3033 ),
3034 );
3035
3036 let valid_inline_path_selection = JSONSelection::named(SubSelection {
3037 selections: vec![NamedSelection {
3038 prefix: NamingPrefix::None,
3039 path: WithRange::new(
3040 LitExpr::Path(PathSelection {
3041 path: PathList::Key(
3042 Key::field("some").into_with_range(),
3043 PathList::Key(
3044 Key::field("object").into_with_range(),
3045 PathList::Empty.into_with_range(),
3046 )
3047 .into_with_range(),
3048 )
3049 .into_with_range(),
3050 }),
3051 None,
3052 ),
3053 }],
3054 ..Default::default()
3055 });
3056
3057 assert_eq!(
3058 valid_inline_path_selection.apply_to(&json!({
3059 "some": {
3060 "object": {
3061 "key": "value",
3062 },
3063 },
3064 })),
3065 (
3066 Some(json!({
3067 "key": "value",
3068 })),
3069 vec![],
3070 ),
3071 );
3072 }
3073
3074 #[test]
3075 fn test_apply_to_non_identifier_properties() {
3076 let data = json!({
3077 "not an identifier": [
3078 { "also.not.an.identifier": 0 },
3079 { "also.not.an.identifier": 1 },
3080 { "also.not.an.identifier": 2 },
3081 ],
3082 "another": {
3083 "pesky string literal!": {
3084 "identifier": 123,
3085 "{ evil braces }": true,
3086 },
3087 },
3088 });
3089
3090 assert_eq!(
3091 selection!("alias: 'not an identifier' { safe: 'also.not.an.identifier' }")
3095 .apply_to(&data),
3096 (
3097 Some(json!({
3098 "alias": [
3099 { "safe": 0 },
3100 { "safe": 1 },
3101 { "safe": 2 },
3102 ],
3103 })),
3104 vec![],
3105 ),
3106 );
3107
3108 assert_eq!(
3109 selection!("'not an identifier'.'also.not.an.identifier'").apply_to(&data),
3110 (Some(json!([0, 1, 2])), vec![],),
3111 );
3112
3113 assert_eq!(
3114 selection!("$.'not an identifier'.'also.not.an.identifier'").apply_to(&data),
3115 (Some(json!([0, 1, 2])), vec![],),
3116 );
3117
3118 assert_eq!(
3119 selection!("$.\"not an identifier\" { safe: \"also.not.an.identifier\" }")
3120 .apply_to(&data),
3121 (
3122 Some(json!([
3123 { "safe": 0 },
3124 { "safe": 1 },
3125 { "safe": 2 },
3126 ])),
3127 vec![],
3128 ),
3129 );
3130
3131 assert_eq!(
3132 selection!(
3133 "another {
3134 pesky: 'pesky string literal!' {
3135 identifier
3136 evil: '{ evil braces }'
3137 }
3138 }"
3139 )
3140 .apply_to(&data),
3141 (
3142 Some(json!({
3143 "another": {
3144 "pesky": {
3145 "identifier": 123,
3146 "evil": true,
3147 },
3148 },
3149 })),
3150 vec![],
3151 ),
3152 );
3153
3154 assert_eq!(
3155 selection!("another.'pesky string literal!'.'{ evil braces }'").apply_to(&data),
3156 (Some(json!(true)), vec![],),
3157 );
3158
3159 assert_eq!(
3160 selection!("another.'pesky string literal!'.\"identifier\"").apply_to(&data),
3161 (Some(json!(123)), vec![],),
3162 );
3163
3164 assert_eq!(
3165 selection!("$.another.'pesky string literal!'.\"identifier\"").apply_to(&data),
3166 (Some(json!(123)), vec![],),
3167 );
3168 }
3169
3170 #[rstest]
3171 #[case::latest(ConnectSpec::V0_2)]
3172 #[case::next(ConnectSpec::V0_3)]
3173 #[case::v0_4(ConnectSpec::V0_4)]
3174 fn test_left_associative_path_evaluation(#[case] spec: ConnectSpec) {
3175 assert_eq!(
3176 selection!("batch.id->first", spec).apply_to(&json!({
3177 "batch": [
3178 { "id": 1 },
3179 { "id": 2 },
3180 { "id": 3 },
3181 ],
3182 })),
3183 (Some(json!(1)), vec![]),
3184 );
3185
3186 assert_eq!(
3187 selection!("batch.id->last", spec).apply_to(&json!({
3188 "batch": [
3189 { "id": 1 },
3190 { "id": 2 },
3191 { "id": 3 },
3192 ],
3193 })),
3194 (Some(json!(3)), vec![]),
3195 );
3196
3197 assert_eq!(
3198 selection!("batch.id->size", spec).apply_to(&json!({
3199 "batch": [
3200 { "id": 1 },
3201 { "id": 2 },
3202 { "id": 3 },
3203 ],
3204 })),
3205 (Some(json!(3)), vec![]),
3206 );
3207
3208 assert_eq!(
3209 selection!("batch.id->slice(1)->first", spec).apply_to(&json!({
3210 "batch": [
3211 { "id": 1 },
3212 { "id": 2 },
3213 { "id": 3 },
3214 ],
3215 })),
3216 (Some(json!(2)), vec![]),
3217 );
3218
3219 assert_eq!(
3220 selection!("batch.id->map({ batchId: @ })", spec).apply_to(&json!({
3221 "batch": [
3222 { "id": 1 },
3223 { "id": 2 },
3224 { "id": 3 },
3225 ],
3226 })),
3227 (
3228 Some(json!([
3229 { "batchId": 1 },
3230 { "batchId": 2 },
3231 { "batchId": 3 },
3232 ])),
3233 vec![],
3234 ),
3235 );
3236
3237 let mut vars = IndexMap::default();
3238 vars.insert(
3239 "$batch".to_string(),
3240 json!([
3241 { "id": 4 },
3242 { "id": 5 },
3243 { "id": 6 },
3244 ]),
3245 );
3246 assert_eq!(
3247 selection!("$batch.id->map({ batchId: @ })", spec).apply_with_vars(
3248 &json!({
3249 "batch": "ignored",
3250 }),
3251 &vars
3252 ),
3253 (
3254 Some(json!([
3255 { "batchId": 4 },
3256 { "batchId": 5 },
3257 { "batchId": 6 },
3258 ])),
3259 vec![],
3260 ),
3261 );
3262
3263 assert_eq!(
3264 selection!("batch.id->map({ batchId: @ })->first", spec).apply_to(&json!({
3265 "batch": [
3266 { "id": 7 },
3267 { "id": 8 },
3268 { "id": 9 },
3269 ],
3270 })),
3271 (Some(json!({ "batchId": 7 })), vec![]),
3272 );
3273
3274 assert_eq!(
3275 selection!("batch.id->map({ batchId: @ })->last", spec).apply_to(&json!({
3276 "batch": [
3277 { "id": 7 },
3278 { "id": 8 },
3279 { "id": 9 },
3280 ],
3281 })),
3282 (Some(json!({ "batchId": 9 })), vec![]),
3283 );
3284
3285 assert_eq!(
3286 selection!("$batch.id->map({ batchId: @ })->first", spec).apply_with_vars(
3287 &json!({
3288 "batch": "ignored",
3289 }),
3290 &vars
3291 ),
3292 (Some(json!({ "batchId": 4 })), vec![]),
3293 );
3294
3295 assert_eq!(
3296 selection!("$batch.id->map({ batchId: @ })->last", spec).apply_with_vars(
3297 &json!({
3298 "batch": "ignored",
3299 }),
3300 &vars
3301 ),
3302 (Some(json!({ "batchId": 6 })), vec![]),
3303 );
3304
3305 assert_eq!(
3306 selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3307 "arrays": [
3308 { "as": { "bs": [10, 20, 30] } },
3309 { "as": { "bs": [40, 50, 60] } },
3310 { "as": { "bs": [70, 80, 90] } },
3311 ],
3312 })),
3313 (
3314 Some(json!({
3315 "echoed": [
3316 [10, 20, 30],
3317 [40, 50, 60],
3318 [70, 80, 90],
3319 ],
3320 })),
3321 vec![],
3322 ),
3323 );
3324
3325 assert_eq!(
3326 selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3327 "arrays": [
3328 { "as": { "bs": [10, 20, 30] } },
3329 { "as": [
3330 { "bs": [40, 50, 60] },
3331 { "bs": [70, 80, 90] },
3332 ] },
3333 { "as": { "bs": [100, 110, 120] } },
3334 ],
3335 })),
3336 (
3337 Some(json!({
3338 "echoed": [
3339 [10, 20, 30],
3340 [
3341 [40, 50, 60],
3342 [70, 80, 90],
3343 ],
3344 [100, 110, 120],
3345 ],
3346 })),
3347 vec![],
3348 ),
3349 );
3350
3351 assert_eq!(
3352 selection!("batch.id->jsonStringify", spec).apply_to(&json!({
3353 "batch": [
3354 { "id": 1 },
3355 { "id": 2 },
3356 { "id": 3 },
3357 ],
3358 })),
3359 (Some(json!("[1,2,3]")), vec![]),
3360 );
3361
3362 assert_eq!(
3363 selection!("batch.id->map([@])->echo([@])->jsonStringify", spec).apply_to(&json!({
3364 "batch": [
3365 { "id": 1 },
3366 { "id": 2 },
3367 { "id": 3 },
3368 ],
3369 })),
3370 (Some(json!("[[[1],[2],[3]]]")), vec![]),
3371 );
3372
3373 assert_eq!(
3374 selection!("batch.id->map([@])->echo([@])->jsonStringify->typeof", spec).apply_to(
3375 &json!({
3376 "batch": [
3377 { "id": 1 },
3378 { "id": 2 },
3379 { "id": 3 },
3380 ],
3381 })
3382 ),
3383 (Some(json!("string")), vec![]),
3384 );
3385 }
3386
3387 #[test]
3388 fn test_left_associative_output_shapes_v0_2() {
3389 let spec = ConnectSpec::V0_2;
3390
3391 assert_eq!(
3392 selection!("$batch.id", spec).shape().pretty_print(),
3393 "$batch.id"
3394 );
3395
3396 assert_eq!(
3397 selection!("$batch.id->first", spec).shape().pretty_print(),
3398 "Unknown",
3399 );
3400
3401 assert_eq!(
3402 selection!("$batch.id->last", spec).shape().pretty_print(),
3403 "Unknown",
3404 );
3405
3406 let mut named_shapes = IndexMap::default();
3407 named_shapes.insert(
3408 "$batch".to_string(),
3409 Shape::list(
3410 Shape::record(
3411 {
3412 let mut map = Shape::empty_map();
3413 map.insert("id".to_string(), Shape::int([]));
3414 map
3415 },
3416 [],
3417 ),
3418 [],
3419 ),
3420 );
3421
3422 let root_shape = Shape::name("$root", []);
3423 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3424 .with_spec(spec)
3425 .with_named_shapes(named_shapes);
3426
3427 let computed_batch_id =
3428 selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3429 assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3430
3431 let computed_first = selection!("$batch.id->first", spec)
3432 .compute_output_shape(&shape_context, root_shape.clone());
3433 assert_eq!(computed_first.pretty_print(), "Unknown");
3434
3435 let computed_last = selection!("$batch.id->last", spec)
3436 .compute_output_shape(&shape_context, root_shape.clone());
3437 assert_eq!(computed_last.pretty_print(), "Unknown");
3438
3439 assert_eq!(
3440 selection!("$batch.id->jsonStringify", spec)
3441 .shape()
3442 .pretty_print(),
3443 "Unknown",
3444 );
3445
3446 assert_eq!(
3447 selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3448 .shape()
3449 .pretty_print(),
3450 "Unknown",
3451 );
3452
3453 assert_eq!(
3454 selection!("$batch.id->map(@)->echo(@)", spec)
3455 .shape()
3456 .pretty_print(),
3457 "Unknown",
3458 );
3459
3460 assert_eq!(
3461 selection!("$batch.id->map(@)->echo([@])", spec)
3462 .shape()
3463 .pretty_print(),
3464 "Unknown",
3465 );
3466
3467 assert_eq!(
3468 selection!("$batch.id->map([@])->echo(@)", spec)
3469 .shape()
3470 .pretty_print(),
3471 "Unknown",
3472 );
3473
3474 assert_eq!(
3475 selection!("$batch.id->map([@])->echo([@])", spec)
3476 .shape()
3477 .pretty_print(),
3478 "Unknown",
3479 );
3480
3481 assert_eq!(
3482 selection!("$batch.id->map([@])->echo([@])", spec)
3483 .compute_output_shape(&shape_context, root_shape,)
3484 .pretty_print(),
3485 "Unknown",
3486 );
3487 }
3488
3489 #[test]
3490 fn test_left_associative_output_shapes_v0_3() {
3491 let spec = ConnectSpec::V0_3;
3492
3493 assert_eq!(
3494 selection!("$batch.id", spec).shape().pretty_print(),
3495 "$batch.id"
3496 );
3497
3498 assert_eq!(
3499 selection!("$batch.id->first", spec).shape().pretty_print(),
3500 "$batch.id.0",
3501 );
3502
3503 assert_eq!(
3504 selection!("$batch.id->last", spec).shape().pretty_print(),
3505 "$batch.id.*",
3506 );
3507
3508 let mut named_shapes = IndexMap::default();
3509 named_shapes.insert(
3510 "$batch".to_string(),
3511 Shape::list(
3512 Shape::record(
3513 {
3514 let mut map = Shape::empty_map();
3515 map.insert("id".to_string(), Shape::int([]));
3516 map
3517 },
3518 [],
3519 ),
3520 [],
3521 ),
3522 );
3523
3524 let root_shape = Shape::name("$root", []);
3525 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3526 .with_spec(spec)
3527 .with_named_shapes(named_shapes.clone());
3528
3529 let computed_batch_id =
3530 selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3531 assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3532
3533 let computed_first = selection!("$batch.id->first", spec)
3534 .compute_output_shape(&shape_context, root_shape.clone());
3535 assert_eq!(computed_first.pretty_print(), "One<Int, None>");
3536
3537 let computed_last = selection!("$batch.id->last", spec)
3538 .compute_output_shape(&shape_context, root_shape.clone());
3539 assert_eq!(computed_last.pretty_print(), "One<Int, None>");
3540
3541 assert_eq!(
3542 selection!("$batch.id->jsonStringify", spec)
3543 .shape()
3544 .pretty_print(),
3545 "String",
3546 );
3547
3548 assert_eq!(
3549 selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3550 .shape()
3551 .pretty_print(),
3552 "String",
3553 );
3554
3555 assert_eq!(
3556 selection!("$batch.id->map(@)->echo(@)", spec)
3557 .shape()
3558 .pretty_print(),
3559 "List<$batch.id.*>",
3560 );
3561
3562 assert_eq!(
3563 selection!("$batch.id->map(@)->echo([@])", spec)
3564 .shape()
3565 .pretty_print(),
3566 "[List<$batch.id.*>]",
3567 );
3568
3569 assert_eq!(
3570 selection!("$batch.id->map([@])->echo(@)", spec)
3571 .shape()
3572 .pretty_print(),
3573 "List<[$batch.id.*]>",
3574 );
3575
3576 assert_eq!(
3577 selection!("$batch.id->map([@])->echo([@])", spec)
3578 .shape()
3579 .pretty_print(),
3580 "[List<[$batch.id.*]>]",
3581 );
3582
3583 assert_eq!(
3584 selection!("$batch.id->map([@])->echo([@])", spec)
3585 .compute_output_shape(&shape_context, root_shape,)
3586 .pretty_print(),
3587 "[List<[Int]>]",
3588 );
3589 }
3590
3591 fn lit_paths_data() -> JSON {
3595 json!({
3596 "value": {
3597 "key": 123,
3598 },
3599 })
3600 }
3601
3602 #[test]
3603 fn test_lit_paths_string_head_first() {
3604 assert_eq!(
3605 selection!("$(\"a\")->first").apply_to(&lit_paths_data()),
3606 (Some(json!("a")), vec![]),
3607 );
3608 }
3609
3610 #[test]
3611 fn test_lit_paths_string_head_last_inner() {
3612 assert_eq!(
3613 selection!("$('asdf'->last)").apply_to(&lit_paths_data()),
3614 (Some(json!("f")), vec![]),
3615 );
3616 }
3617
3618 #[test]
3619 fn test_lit_paths_number_head_add_outer() {
3620 assert_eq!(
3621 selection!("$(1234)->add(1111)").apply_to(&lit_paths_data()),
3622 (Some(json!(2345)), vec![]),
3623 );
3624 }
3625
3626 #[test]
3627 fn test_lit_paths_number_head_add_inner() {
3628 assert_eq!(
3629 selection!("$(1234->add(1111))").apply_to(&lit_paths_data()),
3630 (Some(json!(2345)), vec![]),
3631 );
3632 }
3633
3634 #[test]
3635 fn test_lit_paths_path_head_mul_inner() {
3636 assert_eq!(
3637 selection!("$(value.key->mul(10))").apply_to(&lit_paths_data()),
3638 (Some(json!(1230)), vec![]),
3639 );
3640 }
3641
3642 #[test]
3643 fn test_lit_paths_path_head_mul_outer() {
3644 assert_eq!(
3645 selection!("$(value.key)->mul(10)").apply_to(&lit_paths_data()),
3646 (Some(json!(1230)), vec![]),
3647 );
3648 }
3649
3650 #[test]
3651 fn test_lit_paths_path_head_typeof_inner() {
3652 assert_eq!(
3653 selection!("$(value.key->typeof)").apply_to(&lit_paths_data()),
3654 (Some(json!("number")), vec![]),
3655 );
3656 }
3657
3658 #[test]
3659 fn test_lit_paths_path_head_typeof_outer() {
3660 assert_eq!(
3661 selection!("$(value.key)->typeof").apply_to(&lit_paths_data()),
3662 (Some(json!("number")), vec![]),
3663 );
3664 }
3665
3666 #[test]
3667 fn test_lit_paths_array_head_last_outer() {
3668 assert_eq!(
3669 selection!("$([1, 2, 3])->last").apply_to(&lit_paths_data()),
3670 (Some(json!(3)), vec![]),
3671 );
3672 }
3673
3674 #[test]
3675 fn test_lit_paths_array_head_first_inner() {
3676 assert_eq!(
3677 selection!("$([1, 2, 3]->first)").apply_to(&lit_paths_data()),
3678 (Some(json!(1)), vec![]),
3679 );
3680 }
3681
3682 #[test]
3683 fn test_lit_paths_object_head_key_outer() {
3684 assert_eq!(
3685 selection!("$({ a: 'ay', b: 1 }).a").apply_to(&lit_paths_data()),
3686 (Some(json!("ay")), vec![]),
3687 );
3688 }
3689
3690 #[test]
3691 fn test_lit_paths_object_head_key_inner() {
3692 assert_eq!(
3693 selection!("$({ a: 'ay', b: 2 }.a)").apply_to(&lit_paths_data()),
3694 (Some(json!("ay")), vec![]),
3695 );
3696 }
3697
3698 #[test]
3699 fn test_lit_paths_negative_literal_precedence() {
3700 assert_eq!(
3704 selection!("$(-1->add(10))").apply_to(&lit_paths_data()),
3705 (Some(json!(9)), vec![]),
3706 );
3707 }
3708
3709 #[test]
3714 fn test_lit_paths_v0_4_string_head_first() {
3715 assert_eq!(
3716 selection!(r#""a"->first"#, ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3717 (Some(json!("a")), vec![]),
3718 );
3719 }
3720
3721 #[test]
3722 fn test_lit_paths_v0_4_string_head_last() {
3723 assert_eq!(
3724 selection!("'asdf'->last", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3725 (Some(json!("f")), vec![]),
3726 );
3727 }
3728
3729 #[test]
3730 fn test_lit_paths_v0_4_number_head_add() {
3731 assert_eq!(
3732 selection!("1234->add(1111)", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3733 (Some(json!(2345)), vec![]),
3734 );
3735 }
3736
3737 #[test]
3738 fn test_lit_paths_v0_4_array_head_last() {
3739 assert_eq!(
3740 selection!("[1, 2, 3]->last", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3741 (Some(json!(3)), vec![]),
3742 );
3743 }
3744
3745 #[test]
3746 fn test_lit_paths_v0_4_array_head_first() {
3747 assert_eq!(
3748 selection!("[1, 2, 3]->first", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3749 (Some(json!(1)), vec![]),
3750 );
3751 }
3752
3753 #[test]
3754 fn test_lit_paths_v0_4_object_head_key() {
3755 assert_eq!(
3756 selection!("{ a: 'ay', b: 1 }.a", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3757 (Some(json!("ay")), vec![]),
3758 );
3759 }
3760
3761 #[test]
3762 fn test_lit_paths_v0_4_negative_literal_precedence() {
3763 assert_eq!(
3764 selection!("-1->add(10)", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3765 (Some(json!(9)), vec![]),
3766 );
3767 }
3768
3769 #[test]
3770 fn test_lit_paths_v0_4_path_head_mul() {
3771 assert_eq!(
3772 selection!("value.key->mul(10)", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3773 (Some(json!(1230)), vec![]),
3774 );
3775 }
3776
3777 #[test]
3778 fn test_lit_paths_v0_4_path_head_typeof() {
3779 assert_eq!(
3780 selection!("value.key->typeof", ConnectSpec::V0_4).apply_to(&lit_paths_data()),
3781 (Some(json!("number")), vec![]),
3782 );
3783 }
3784
3785 fn reserved_key_data() -> JSON {
3789 json!({
3790 "true": "yes",
3791 "false": "no",
3792 "null": "nope",
3793 "some property": "with spaces",
3794 "weird \"quoted\" key": "escapes work",
3795 })
3796 }
3797
3798 #[test]
3799 fn test_reserved_key_true() {
3800 assert_eq!(
3801 selection!("$.true").apply_to(&reserved_key_data()),
3802 (Some(json!("yes")), vec![]),
3803 );
3804 }
3805
3806 #[test]
3807 fn test_reserved_key_false() {
3808 assert_eq!(
3809 selection!("$.false").apply_to(&reserved_key_data()),
3810 (Some(json!("no")), vec![]),
3811 );
3812 }
3813
3814 #[test]
3815 fn test_reserved_key_null() {
3816 assert_eq!(
3817 selection!("$.null").apply_to(&reserved_key_data()),
3818 (Some(json!("nope")), vec![]),
3819 );
3820 }
3821
3822 #[test]
3823 fn test_reserved_key_quoted_with_space() {
3824 assert_eq!(
3825 selection!(r#"$."some property""#).apply_to(&reserved_key_data()),
3826 (Some(json!("with spaces")), vec![]),
3827 );
3828 }
3829
3830 #[test]
3831 fn test_reserved_key_quoted_with_escapes() {
3832 assert_eq!(
3833 selection!(r#"$."weird \"quoted\" key""#).apply_to(&reserved_key_data()),
3834 (Some(json!("escapes work")), vec![]),
3835 );
3836 }
3837
3838 #[test]
3839 fn test_reserved_key_v0_4_true() {
3840 assert_eq!(
3841 selection!("$.true", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3842 (Some(json!("yes")), vec![]),
3843 );
3844 }
3845
3846 #[test]
3847 fn test_reserved_key_v0_4_false() {
3848 assert_eq!(
3849 selection!("$.false", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3850 (Some(json!("no")), vec![]),
3851 );
3852 }
3853
3854 #[test]
3855 fn test_reserved_key_v0_4_null() {
3856 assert_eq!(
3857 selection!("$.null", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3858 (Some(json!("nope")), vec![]),
3859 );
3860 }
3861
3862 #[test]
3863 fn test_reserved_key_v0_4_quoted_with_space() {
3864 assert_eq!(
3865 selection!(r#"$."some property""#, ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3866 (Some(json!("with spaces")), vec![]),
3867 );
3868 }
3869
3870 #[test]
3875 fn test_bare_keyword_true_v0_3_is_field_name() {
3876 assert_eq!(
3877 selection!("true", ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3878 (Some(json!({ "true": "yes" })), vec![]),
3879 );
3880 }
3881
3882 #[test]
3883 fn test_bare_keyword_false_v0_3_is_field_name() {
3884 assert_eq!(
3885 selection!("false", ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3886 (Some(json!({ "false": "no" })), vec![]),
3887 );
3888 }
3889
3890 #[test]
3891 fn test_bare_keyword_null_v0_3_is_field_name() {
3892 assert_eq!(
3893 selection!("null", ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3894 (Some(json!({ "null": "nope" })), vec![]),
3895 );
3896 }
3897
3898 #[test]
3899 fn test_bare_keyword_true_v0_4_is_literal() {
3900 assert_eq!(
3901 selection!("true", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3902 (Some(json!(true)), vec![]),
3903 );
3904 }
3905
3906 #[test]
3907 fn test_bare_keyword_false_v0_4_is_literal() {
3908 assert_eq!(
3909 selection!("false", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3910 (Some(json!(false)), vec![]),
3911 );
3912 }
3913
3914 #[test]
3915 fn test_bare_keyword_null_v0_4_is_literal() {
3916 assert_eq!(
3917 selection!("null", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3918 (Some(json!(null)), vec![]),
3919 );
3920 }
3921
3922 #[test]
3927 fn test_bare_quoted_string_v0_3_is_field_name() {
3928 assert_eq!(
3929 selection!(r#""some property""#, ConnectSpec::V0_3).apply_to(&reserved_key_data()),
3930 (Some(json!({ "some property": "with spaces" })), vec![]),
3931 );
3932 }
3933
3934 #[test]
3935 fn test_bare_quoted_string_v0_4_is_literal() {
3936 assert_eq!(
3937 selection!(r#""some property""#, ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3938 (Some(json!("some property")), vec![]),
3939 );
3940 }
3941
3942 #[test]
3948 fn test_bare_null_question_v0_4_short_circuits() {
3949 assert_eq!(
3950 selection!("null?", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3951 (None, vec![]),
3952 );
3953 }
3954
3955 #[test]
3956 fn test_bare_true_question_v0_4_passes_through() {
3957 assert_eq!(
3958 selection!("true?", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3959 (Some(json!(true)), vec![]),
3960 );
3961 }
3962
3963 #[test]
3964 fn test_bare_false_question_v0_4_passes_through() {
3965 assert_eq!(
3966 selection!("false?", ConnectSpec::V0_4).apply_to(&reserved_key_data()),
3967 (Some(json!(false)), vec![]),
3968 );
3969 }
3970
3971 #[rstest]
3972 #[case::number("1234", "1234")]
3973 #[case::negative_number("-1", "-1")]
3974 #[case::bool_true("true", "true")]
3975 #[case::bool_false("false", "false")]
3976 #[case::null("null", "null")]
3977 #[case::string(r#""hello""#, r#""hello""#)]
3978 #[case::array("[1, 2, 3]", "[1, 2, 3]")]
3979 #[case::object("{ a: 1, b: 2 }", "{ a: 1, b: 2 }")]
3980 #[case::object_key_access("{ a: 1, b: 2 }.a", "1")]
3981 #[case::array_last("[1, 2, 3]->last", "3")]
3982 #[case::array_first("[1, 2, 3]->first", "1")]
3983 #[case::number_add("1234->add(1111)", "Int")]
3984 #[case::null_question("null?", "None")]
3985 #[case::true_question("true?", "true")]
3986 #[case::nullish_coalescing(r#"$args.maybe ?? "fallback""#, r#"One<$args.maybe?!, "fallback">"#)]
3987 #[case::multiline_nested_object(
3988 r#"{
3989 a: 1,
3990 b: {
3991 x: "hi",
3992 y: [1, 2, 3],
3993 },
3994 }"#,
3995 r#"{ a: 1, b: { x: "hi", y: [1, 2, 3] } }"#
3996 )]
3997 #[case::multiline_array_of_objects(
3998 r#"[
3999 { id: 1, label: "one" },
4000 { id: 2, label: "two" },
4001 ]"#,
4002 r#"[{ id: 1, label: "one" }, { id: 2, label: "two" }]"#
4003 )]
4004 #[case::multiline_method_chain(
4005 r#"[1, 2, 3]
4006 ->map(@)
4007 ->first"#,
4008 "1"
4009 )]
4010 #[case::multiline_object_with_comment(
4011 r#"{
4012 # pick the user id
4013 userid: $args.userid,
4014 name: "fixed",
4015 }"#,
4016 r#"{ name: "fixed", userid: $args.userid }"#
4017 )]
4018 #[case::multiline_nullish_coalescing_chain(
4019 r#"$args.a
4020 ?? $args.b
4021 ?? "fallback""#,
4022 r#"One<$args.a?!, $args.b?!, "fallback">"#
4023 )]
4024 #[case::multiline_copy_paste_json(
4025 r#"{
4026 hello: "world",
4027 count: 3,
4028 items: ["a", "b", "c"],
4029 when: $args.when,
4030 }"#,
4031 "{\n count: 3,\n hello: \"world\",\n items: [\"a\", \"b\", \"c\"],\n when: $args.when,\n}"
4032 )]
4033 fn test_v0_4_top_level_shape_literal(#[case] input: &str, #[case] expected: &str) {
4034 assert_eq!(
4035 selection!(input, ConnectSpec::V0_4).shape().pretty_print(),
4036 expected,
4037 );
4038 }
4039
4040 #[test]
4041 fn test_compute_output_shape() {
4042 let spec = ConnectSpec::V0_3;
4043
4044 assert_eq!(selection!("", spec).shape().pretty_print(), "{}");
4045
4046 assert_eq!(
4047 selection!("id name", spec).shape().pretty_print(),
4048 "{ id: $root.*.id, name: $root.*.name }",
4049 );
4050
4051 assert_eq!(
4052 selection!("$.data { thisOrThat: $(maybe.this ?? maybe.that) }", spec)
4053 .shape()
4054 .pretty_print(),
4055 "{ thisOrThat: One<$root.data.*.maybe.this?!, $root.data.*.maybe.that> }",
4056 );
4057
4058 assert_eq!(
4059 selection!(
4060 r#"
4061 id
4062 name
4063 friends: friend_ids { id: @ }
4064 alias: arrayOfArrays { x y }
4065 ys: arrayOfArrays.y xs: arrayOfArrays.x
4066 "#,
4067 spec
4068 )
4069 .shape()
4070 .pretty_print(),
4071 r#"{
4078 alias: {
4079 x: $root.*.arrayOfArrays.*.x,
4080 y: $root.*.arrayOfArrays.*.y,
4081 },
4082 friends: { id: $root.*.friend_ids.* },
4083 id: $root.*.id,
4084 name: $root.*.name,
4085 xs: $root.*.arrayOfArrays.x,
4086 ys: $root.*.arrayOfArrays.y,
4087}"#,
4088 );
4089
4090 assert_eq!(
4091 selection!(
4092 r#"
4093 id
4094 name
4095 friends: friend_ids->map({ id: @ })
4096 alias: arrayOfArrays { x y }
4097 ys: arrayOfArrays.y xs: arrayOfArrays.x
4098 "#,
4099 spec
4100 )
4101 .shape()
4102 .pretty_print(),
4103 r#"{
4104 alias: {
4105 x: $root.*.arrayOfArrays.*.x,
4106 y: $root.*.arrayOfArrays.*.y,
4107 },
4108 friends: List<{ id: $root.*.friend_ids.* }>,
4109 id: $root.*.id,
4110 name: $root.*.name,
4111 xs: $root.*.arrayOfArrays.x,
4112 ys: $root.*.arrayOfArrays.y,
4113}"#,
4114 );
4115
4116 assert_eq!(
4117 selection!("$->echo({ thrice: [@, @, @] })", spec)
4118 .shape()
4119 .pretty_print(),
4120 "{ thrice: [$root, $root, $root] }",
4121 );
4122
4123 assert_eq!(
4124 selection!("$->echo({ thrice: [@, @, @] })->entries", spec)
4125 .shape()
4126 .pretty_print(),
4127 "[{ key: \"thrice\", value: [$root, $root, $root] }]",
4128 );
4129
4130 assert_eq!(
4131 selection!("$->echo({ thrice: [@, @, @] })->entries.key", spec)
4132 .shape()
4133 .pretty_print(),
4134 "[\"thrice\"]",
4135 );
4136
4137 assert_eq!(
4138 selection!("$->echo({ thrice: [@, @, @] })->entries.value", spec)
4139 .shape()
4140 .pretty_print(),
4141 "[[$root, $root, $root]]",
4142 );
4143
4144 assert_eq!(
4145 selection!("$->echo({ wrapped: @ })->entries { k: key v: value }", spec)
4146 .shape()
4147 .pretty_print(),
4148 "[{ k: \"wrapped\", v: $root }]",
4149 );
4150 }
4151
4152 #[rstest]
4153 #[case::v0_3(ConnectSpec::V0_3)]
4154 #[case::v0_4(ConnectSpec::V0_4)]
4155 fn test_optional_key_access_with_existing_property(#[case] spec: ConnectSpec) {
4156 use serde_json_bytes::json;
4157
4158 let data = json!({
4159 "user": {
4160 "profile": {
4161 "name": "Alice"
4162 }
4163 }
4164 });
4165
4166 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4167 .unwrap()
4168 .apply_to(&data);
4169 assert!(errors.is_empty());
4170 assert_eq!(result, Some(json!("Alice")));
4171 }
4172
4173 #[rstest]
4174 #[case::v0_3(ConnectSpec::V0_3)]
4175 #[case::v0_4(ConnectSpec::V0_4)]
4176 fn test_optional_key_access_with_null_value(#[case] spec: ConnectSpec) {
4177 use serde_json_bytes::json;
4178
4179 let data_null = json!({
4180 "user": null
4181 });
4182
4183 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4184 .unwrap()
4185 .apply_to(&data_null);
4186 assert!(errors.is_empty());
4187 assert_eq!(result, None);
4188 }
4189
4190 #[rstest]
4191 #[case::v0_3(ConnectSpec::V0_3)]
4192 #[case::v0_4(ConnectSpec::V0_4)]
4193 fn test_optional_key_access_on_non_object(#[case] spec: ConnectSpec) {
4194 use serde_json_bytes::json;
4195
4196 let data_non_obj = json!({
4197 "user": "not an object"
4198 });
4199
4200 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4201 .unwrap()
4202 .apply_to(&data_non_obj);
4203 assert_eq!(errors.len(), 1);
4204 assert!(
4205 errors[0]
4206 .message()
4207 .contains("Property .profile not found in string")
4208 );
4209 assert_eq!(result, None);
4210 }
4211
4212 #[rstest]
4213 #[case::v0_3(ConnectSpec::V0_3)]
4214 #[case::v0_4(ConnectSpec::V0_4)]
4215 fn test_optional_key_access_with_missing_property(#[case] spec: ConnectSpec) {
4216 use serde_json_bytes::json;
4217
4218 let data = json!({
4219 "user": {
4220 "other": "value"
4221 }
4222 });
4223
4224 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
4225 .unwrap()
4226 .apply_to(&data);
4227 assert_eq!(errors.len(), 1);
4228 assert!(
4229 errors[0]
4230 .message()
4231 .contains("Property .profile not found in object")
4232 );
4233 assert_eq!(result, None);
4234 }
4235
4236 #[rstest]
4237 #[case::v0_3(ConnectSpec::V0_3)]
4238 #[case::v0_4(ConnectSpec::V0_4)]
4239 fn test_chained_optional_key_access(#[case] spec: ConnectSpec) {
4240 use serde_json_bytes::json;
4241
4242 let data = json!({
4243 "user": {
4244 "profile": {
4245 "name": "Alice"
4246 }
4247 }
4248 });
4249
4250 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
4251 .unwrap()
4252 .apply_to(&data);
4253 assert!(errors.is_empty());
4254 assert_eq!(result, Some(json!("Alice")));
4255 }
4256
4257 #[rstest]
4258 #[case::v0_3(ConnectSpec::V0_3)]
4259 #[case::v0_4(ConnectSpec::V0_4)]
4260 fn test_chained_optional_access_with_null_in_middle(#[case] spec: ConnectSpec) {
4261 use serde_json_bytes::json;
4262
4263 let data_partial_null = json!({
4264 "user": {
4265 "profile": null
4266 }
4267 });
4268
4269 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
4270 .unwrap()
4271 .apply_to(&data_partial_null);
4272 assert!(errors.is_empty());
4273 assert_eq!(result, None);
4274 }
4275
4276 #[rstest]
4277 #[case::v0_3(ConnectSpec::V0_3)]
4278 #[case::v0_4(ConnectSpec::V0_4)]
4279 fn test_optional_method_on_null(#[case] spec: ConnectSpec) {
4280 use serde_json_bytes::json;
4281
4282 let data = json!({
4283 "items": null
4284 });
4285
4286 let (result, errors) = JSONSelection::parse_with_spec("$.items?->first", spec)
4287 .unwrap()
4288 .apply_to(&data);
4289 assert!(errors.is_empty());
4290 assert_eq!(result, None);
4291 }
4292
4293 #[rstest]
4294 #[case::v0_3(ConnectSpec::V0_3)]
4295 #[case::v0_4(ConnectSpec::V0_4)]
4296 fn test_optional_method_with_valid_method(#[case] spec: ConnectSpec) {
4297 use serde_json_bytes::json;
4298
4299 let data = json!({
4300 "values": [1, 2, 3]
4301 });
4302
4303 let (result, errors) = JSONSelection::parse_with_spec("$.values?->first", spec)
4304 .unwrap()
4305 .apply_to(&data);
4306 assert!(errors.is_empty());
4307 assert_eq!(result, Some(json!(1)));
4308 }
4309
4310 #[rstest]
4311 #[case::v0_3(ConnectSpec::V0_3)]
4312 #[case::v0_4(ConnectSpec::V0_4)]
4313 fn test_optional_method_with_unknown_method(#[case] spec: ConnectSpec) {
4314 use serde_json_bytes::json;
4315
4316 let data = json!({
4317 "values": [1, 2, 3]
4318 });
4319
4320 let (result, errors) = JSONSelection::parse_with_spec("$.values?->length", spec)
4321 .unwrap()
4322 .apply_to(&data);
4323 assert_eq!(errors.len(), 1);
4324 assert!(errors[0].message().contains("Method ->length not found"));
4325 assert_eq!(result, None);
4326 }
4327
4328 #[rstest]
4329 #[case::v0_3(ConnectSpec::V0_3)]
4330 #[case::v0_4(ConnectSpec::V0_4)]
4331 fn test_optional_chaining_with_subselection_on_valid_data(#[case] spec: ConnectSpec) {
4332 use serde_json_bytes::json;
4333
4334 let data = json!({
4335 "user": {
4336 "profile": {
4337 "name": "Alice",
4338 "age": 30,
4339 "email": "alice@example.com"
4340 }
4341 }
4342 });
4343
4344 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
4345 .unwrap()
4346 .apply_to(&data);
4347 assert!(errors.is_empty());
4348 assert_eq!(
4349 result,
4350 Some(json!({
4351 "name": "Alice",
4352 "age": 30
4353 }))
4354 );
4355 }
4356
4357 #[rstest]
4358 #[case::v0_3(ConnectSpec::V0_3)]
4359 #[case::v0_4(ConnectSpec::V0_4)]
4360 fn test_optional_chaining_with_subselection_on_null_data(#[case] spec: ConnectSpec) {
4361 use serde_json_bytes::json;
4362
4363 let data_null = json!({
4364 "user": null
4365 });
4366
4367 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
4368 .unwrap()
4369 .apply_to(&data_null);
4370 assert!(errors.is_empty());
4371 assert_eq!(result, None);
4372 }
4373
4374 #[rstest]
4375 #[case::v0_3(ConnectSpec::V0_3)]
4376 #[case::v0_4(ConnectSpec::V0_4)]
4377 fn test_mixed_regular_and_optional_chaining_working_case(#[case] spec: ConnectSpec) {
4378 use serde_json_bytes::json;
4379
4380 let data = json!({
4381 "response": {
4382 "data": {
4383 "user": {
4384 "profile": {
4385 "name": "Bob"
4386 }
4387 }
4388 }
4389 }
4390 });
4391
4392 let (result, errors) =
4393 JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
4394 .unwrap()
4395 .apply_to(&data);
4396 assert!(errors.is_empty());
4397 assert_eq!(result, Some(json!("Bob")));
4398 }
4399
4400 #[rstest]
4401 #[case::v0_3(ConnectSpec::V0_3)]
4402 #[case::v0_4(ConnectSpec::V0_4)]
4403 fn test_mixed_regular_and_optional_chaining_with_null(#[case] spec: ConnectSpec) {
4404 use serde_json_bytes::json;
4405
4406 let data_null_data = json!({
4407 "response": {
4408 "data": null
4409 }
4410 });
4411
4412 let (result, errors) =
4413 JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
4414 .unwrap()
4415 .apply_to(&data_null_data);
4416 assert!(errors.is_empty());
4417 assert_eq!(result, None);
4418 }
4419
4420 #[rstest]
4421 #[case::v0_3(ConnectSpec::V0_3)]
4422 #[case::v0_4(ConnectSpec::V0_4)]
4423 fn test_optional_selection_set_with_valid_data(#[case] spec: ConnectSpec) {
4424 use serde_json_bytes::json;
4425
4426 let data = json!({
4427 "user": {
4428 "id": 123,
4429 "name": "Alice"
4430 }
4431 });
4432
4433 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4434 .unwrap()
4435 .apply_to(&data);
4436 assert_eq!(
4437 result,
4438 Some(json!({
4439 "id": 123,
4440 "name": "Alice"
4441 }))
4442 );
4443 assert_eq!(errors, vec![]);
4444 }
4445
4446 #[rstest]
4447 #[case::v0_3(ConnectSpec::V0_3)]
4448 #[case::v0_4(ConnectSpec::V0_4)]
4449 fn test_optional_selection_set_with_null_data(#[case] spec: ConnectSpec) {
4450 use serde_json_bytes::json;
4451
4452 let data = json!({
4453 "user": null
4454 });
4455
4456 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4457 .unwrap()
4458 .apply_to(&data);
4459 assert_eq!(result, None);
4460 assert_eq!(errors, vec![]);
4461 }
4462
4463 #[rstest]
4464 #[case::v0_3(ConnectSpec::V0_3)]
4465 #[case::v0_4(ConnectSpec::V0_4)]
4466 fn test_optional_selection_set_with_missing_property(#[case] spec: ConnectSpec) {
4467 use serde_json_bytes::json;
4468
4469 let data = json!({
4470 "other": "value"
4471 });
4472
4473 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4474 .unwrap()
4475 .apply_to(&data);
4476 assert_eq!(result, None);
4477 assert_eq!(errors.len(), 0);
4478 }
4479
4480 #[rstest]
4481 #[case::v0_3(ConnectSpec::V0_3)]
4482 #[case::v0_4(ConnectSpec::V0_4)]
4483 fn test_optional_selection_set_with_non_object(#[case] spec: ConnectSpec) {
4484 use serde_json_bytes::json;
4485
4486 let data = json!({
4487 "user": "not an object"
4488 });
4489
4490 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
4491 .unwrap()
4492 .apply_to(&data);
4493 assert_eq!(result, Some(json!("not an object")));
4496 assert_eq!(errors.len(), 2);
4497 assert!(
4498 errors[0]
4499 .message()
4500 .contains("Property .id not found in string")
4501 );
4502 assert!(
4503 errors[1]
4504 .message()
4505 .contains("Property .name not found in string")
4506 );
4507 }
4508
4509 #[test]
4510 fn test_optional_field_selections() {
4511 let spec = ConnectSpec::V0_3;
4512 let author_selection = selection!("author? { age middleName? }", spec);
4513 assert_debug_snapshot!(author_selection);
4514 assert_eq!(
4515 author_selection.pretty_print(),
4516 "author? { age middleName? }",
4517 );
4518 assert_eq!(
4519 author_selection.shape().pretty_print(),
4520 r#"{ author: One<
4521 {
4522 age: $root.*.author?.*.age,
4523 middleName: $root.*.author?.*.middleName?,
4524 },
4525 None,
4526 > }"#,
4527 );
4528 }
4529
4530 #[test]
4531 fn test_optional_input_shape_with_selection() {
4532 let spec = ConnectSpec::V0_3;
4533 let optional_author_shape_selection =
4534 selection!("unreliableAuthor { age middleName? }", spec);
4535
4536 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
4537 .with_spec(spec)
4538 .with_named_shapes([(
4539 "$root".to_string(),
4540 Shape::record(
4541 {
4542 let mut map = Shape::empty_map();
4543 map.insert(
4544 "unreliableAuthor".to_string(),
4545 Shape::one(
4546 [
4547 Shape::record(
4548 {
4549 let mut map = Shape::empty_map();
4550 map.insert("age".to_string(), Shape::int([]));
4551 map.insert(
4552 "middleName".to_string(),
4553 Shape::one([Shape::string([]), Shape::none()], []),
4554 );
4555 map
4556 },
4557 [],
4558 ),
4559 Shape::none(),
4560 ],
4561 [],
4562 ),
4563 );
4564 map
4565 },
4566 [],
4567 ),
4568 )]);
4569
4570 assert_eq!(
4571 optional_author_shape_selection
4572 .compute_output_shape(
4573 &shape_context,
4574 shape_context.named_shapes().get("$root").unwrap().clone(),
4575 )
4576 .pretty_print(),
4577 "{ unreliableAuthor: One<{ age: Int, middleName: One<String, None> }, None> }",
4578 );
4579 }
4580
4581 #[rstest]
4647 #[case::v0_3(ConnectSpec::V0_3)]
4648 #[case::v0_4(ConnectSpec::V0_4)]
4649 fn test_nested_optional_selection_sets(#[case] spec: ConnectSpec) {
4650 use serde_json_bytes::json;
4651
4652 let data = json!({
4653 "user": {
4654 "profile": {
4655 "name": "Alice",
4656 "email": "alice@example.com"
4657 }
4658 }
4659 });
4660
4661 let (result, errors) =
4662 JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4663 .unwrap()
4664 .apply_to(&data);
4665 assert_eq!(
4666 result,
4667 Some(json!({
4668 "name": "Alice",
4669 "email": "alice@example.com"
4670 }))
4671 );
4672 assert_eq!(errors, vec![]);
4673
4674 let data_with_null_profile = json!({
4676 "user": {
4677 "profile": null
4678 }
4679 });
4680
4681 let (result, errors) =
4682 JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4683 .unwrap()
4684 .apply_to(&data_with_null_profile);
4685 assert_eq!(result, None);
4686 assert_eq!(errors, vec![]);
4687 }
4688
4689 #[rstest]
4690 #[case::v0_3(ConnectSpec::V0_3)]
4691 #[case::v0_4(ConnectSpec::V0_4)]
4692 fn test_mixed_optional_selection_and_optional_chaining(#[case] spec: ConnectSpec) {
4693 use serde_json_bytes::json;
4694
4695 let data = json!({
4696 "user": {
4697 "id": 123,
4698 "profile": null
4699 }
4700 });
4701
4702 let (result, errors) =
4703 JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4704 .unwrap()
4705 .apply_to(&data);
4706 assert_eq!(
4707 result,
4708 Some(json!({
4709 "id": 123
4710 }))
4711 );
4712 assert_eq!(errors, vec![]);
4713
4714 let data_no_user = json!({
4716 "other": "value"
4717 });
4718
4719 let (result, errors) =
4720 JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4721 .unwrap()
4722 .apply_to(&data_no_user);
4723 assert_eq!(result, None);
4724 assert_eq!(errors.len(), 0);
4725 }
4726
4727 #[rstest]
4728 #[case::v0_3(ConnectSpec::V0_3)]
4729 #[case::v0_4(ConnectSpec::V0_4)]
4730 fn test_optional_selection_set_parsing(#[case] spec: ConnectSpec) {
4731 let selection = JSONSelection::parse_with_spec("$.user? { id name }", spec).unwrap();
4733 assert_eq!(selection.pretty_print(), "$.user? { id name }");
4734
4735 let selection = JSONSelection::parse_with_spec("$.user.profile? { name }", spec).unwrap();
4737 assert_eq!(selection.pretty_print(), "$.user.profile? { name }");
4738
4739 let selection =
4741 JSONSelection::parse_with_spec("$.user? { id profile { name } }", spec).unwrap();
4742 assert_eq!(selection.pretty_print(), "$.user? { id profile { name } }");
4743 }
4744
4745 #[rstest]
4746 #[case::v0_3(ConnectSpec::V0_3)]
4747 #[case::v0_4(ConnectSpec::V0_4)]
4748 fn test_optional_selection_set_with_arrays(#[case] spec: ConnectSpec) {
4749 use serde_json_bytes::json;
4750
4751 let data = json!({
4752 "users": [
4753 {
4754 "id": 1,
4755 "name": "Alice"
4756 },
4757 null,
4758 {
4759 "id": 3,
4760 "name": "Charlie"
4761 }
4762 ]
4763 });
4764
4765 let (result, errors) = JSONSelection::parse_with_spec("$.users ?{ id name }", spec)
4766 .unwrap()
4767 .apply_to(&data);
4768 assert_eq!(
4769 result,
4770 Some(json!([
4771 {
4772 "id": 1,
4773 "name": "Alice"
4774 },
4775 null,
4776 {
4777 "id": 3,
4778 "name": "Charlie"
4779 }
4780 ]))
4781 );
4782
4783 assert_eq!(errors.len(), 2);
4784 assert!(
4785 errors[0]
4786 .message()
4787 .contains("Property .id not found in null")
4788 );
4789 assert!(
4790 errors[1]
4791 .message()
4792 .contains("Property .name not found in null")
4793 );
4794 }
4795
4796 #[test]
5327 fn null_coalescing_should_return_left_when_left_not_null() {
5328 let spec = ConnectSpec::V0_3;
5329 assert_eq!(
5330 selection!("$('Foo' ?? 'Bar')", spec).apply_to(&json!({})),
5331 (Some(json!("Foo")), vec![]),
5332 );
5333 }
5334
5335 #[test]
5336 fn null_coalescing_should_return_right_when_left_is_null() {
5337 let spec = ConnectSpec::V0_3;
5338 assert_eq!(
5339 selection!("$(null ?? 'Bar')", spec).apply_to(&json!({})),
5340 (Some(json!("Bar")), vec![]),
5341 );
5342 }
5343
5344 #[test]
5345 fn none_coalescing_should_return_left_when_left_not_none() {
5346 let spec = ConnectSpec::V0_3;
5347 assert_eq!(
5348 selection!("$('Foo' ?! 'Bar')", spec).apply_to(&json!({})),
5349 (Some(json!("Foo")), vec![]),
5350 );
5351 }
5352
5353 #[test]
5354 fn none_coalescing_should_preserve_null_when_left_is_null() {
5355 let spec = ConnectSpec::V0_3;
5356 assert_eq!(
5357 selection!("$(null ?! 'Bar')", spec).apply_to(&json!({})),
5358 (Some(json!(null)), vec![]),
5359 );
5360 }
5361
5362 #[test]
5363 fn nullish_coalescing_should_return_final_null() {
5364 let spec = ConnectSpec::V0_3;
5365 assert_eq!(
5366 selection!("$(missing ?? null)", spec).apply_to(&json!({})),
5367 (Some(json!(null)), vec![]),
5368 );
5369 assert_eq!(
5370 selection!("$(missing ?! null)", spec).apply_to(&json!({})),
5371 (Some(json!(null)), vec![]),
5372 );
5373 }
5374
5375 #[test]
5376 fn nullish_coalescing_should_return_final_none() {
5377 let spec = ConnectSpec::V0_3;
5378 assert_eq!(
5379 selection!("$(missing ?? also_missing)", spec).apply_to(&json!({})),
5380 (
5381 None,
5382 vec![
5383 ApplyToError::new(
5384 "Property .missing not found in object".to_string(),
5385 vec![json!("missing")],
5386 Some(2..9),
5387 spec,
5388 ),
5389 ApplyToError::new(
5390 "Property .also_missing not found in object".to_string(),
5391 vec![json!("also_missing")],
5392 Some(13..25),
5393 spec,
5394 ),
5395 ]
5396 ),
5397 );
5398 assert_eq!(
5399 selection!("maybe: $(missing ?! also_missing)", spec).apply_to(&json!({})),
5400 (
5401 Some(json!({})),
5402 vec![
5403 ApplyToError::new(
5404 "Property .missing not found in object".to_string(),
5405 vec![json!("missing")],
5406 Some(9..16),
5407 spec,
5408 ),
5409 ApplyToError::new(
5410 "Property .also_missing not found in object".to_string(),
5411 vec![json!("also_missing")],
5412 Some(20..32),
5413 spec,
5414 ),
5415 ]
5416 ),
5417 );
5418 }
5419
5420 #[test]
5421 fn coalescing_operators_should_return_earlier_values_if_later_missing() {
5422 let spec = ConnectSpec::V0_3;
5423 assert_eq!(
5424 selection!("$(1234 ?? missing)", spec).apply_to(&json!({})),
5425 (Some(json!(1234)), vec![]),
5426 );
5427 assert_eq!(
5428 selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": 1234 })),
5429 (Some(json!(1234)), vec![]),
5430 );
5431 assert_eq!(
5432 selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": null })),
5433 (
5434 None,
5435 vec![ApplyToError::new(
5436 "Property .missing not found in object".to_string(),
5437 vec![json!("missing")],
5438 Some(10..17),
5439 spec,
5440 )]
5441 ),
5442 );
5443 assert_eq!(
5444 selection!("$(null ?! missing)", spec).apply_to(&json!({})),
5445 (Some(json!(null)), vec![]),
5446 );
5447 assert_eq!(
5448 selection!("$(item ?! missing)", spec).apply_to(&json!({ "item": null })),
5449 (Some(json!(null)), vec![]),
5450 );
5451 }
5452
5453 #[test]
5454 fn null_coalescing_should_chain_left_to_right_when_multiple_nulls() {
5455 let spec = ConnectSpec::V0_3;
5457 assert_eq!(
5458 selection!("$(null ?? null ?? 'Bar')", spec).apply_to(&json!({})),
5459 (Some(json!("Bar")), vec![]),
5460 );
5461 }
5462
5463 #[test]
5464 fn null_coalescing_should_stop_at_first_non_null_when_chaining() {
5465 let spec = ConnectSpec::V0_3;
5466 assert_eq!(
5467 selection!("$('Foo' ?? null ?? 'Bar')", spec).apply_to(&json!({})),
5468 (Some(json!("Foo")), vec![]),
5469 );
5470 }
5471
5472 #[test]
5473 fn null_coalescing_should_fallback_when_field_is_null() {
5474 let spec = ConnectSpec::V0_3;
5475 let data = json!({"field1": null, "field2": "value2"});
5476 assert_eq!(
5477 selection!("$($.field1 ?? $.field2)", spec).apply_to(&data),
5478 (Some(json!("value2")), vec![]),
5479 );
5480 }
5481
5482 #[test]
5483 fn null_coalescing_should_use_literal_fallback_when_all_fields_null() {
5484 let spec = ConnectSpec::V0_3;
5485 let data = json!({"field1": null, "field3": null});
5486 assert_eq!(
5487 selection!("$($.field1 ?? $.field3 ?? 'fallback')", spec).apply_to(&data),
5488 (Some(json!("fallback")), vec![]),
5489 );
5490 }
5491
5492 #[test]
5493 fn none_coalescing_should_preserve_null_field() {
5494 let spec = ConnectSpec::V0_3;
5495 let data = json!({"nullField": null});
5496 assert_eq!(
5497 selection!("$($.nullField ?! 'fallback')", spec).apply_to(&data),
5498 (Some(json!(null)), vec![]),
5499 );
5500 }
5501
5502 #[test]
5503 fn none_coalescing_should_replace_missing_field() {
5504 let spec = ConnectSpec::V0_3;
5505 let data = json!({"nullField": null});
5506 assert_eq!(
5507 selection!("$($.missingField ?! 'fallback')", spec).apply_to(&data),
5508 (Some(json!("fallback")), vec![]),
5509 );
5510 }
5511
5512 #[test]
5513 fn null_coalescing_should_replace_null_field() {
5514 let spec = ConnectSpec::V0_3;
5515 let data = json!({"nullField": null});
5516 assert_eq!(
5517 selection!("$($.nullField ?? 'fallback')", spec).apply_to(&data),
5518 (Some(json!("fallback")), vec![]),
5519 );
5520 }
5521
5522 #[test]
5523 fn null_coalescing_should_replace_missing_field() {
5524 let spec = ConnectSpec::V0_3;
5525 let data = json!({"nullField": null});
5526 assert_eq!(
5527 selection!("$($.missingField ?? 'fallback')", spec).apply_to(&data),
5528 (Some(json!("fallback")), vec![]),
5529 );
5530 }
5531
5532 #[test]
5533 fn null_coalescing_should_preserve_number_type() {
5534 let spec = ConnectSpec::V0_3;
5535 assert_eq!(
5536 selection!("$(null ?? 42)", spec).apply_to(&json!({})),
5537 (Some(json!(42)), vec![]),
5538 );
5539 }
5540
5541 #[test]
5542 fn null_coalescing_should_preserve_boolean_type() {
5543 let spec = ConnectSpec::V0_3;
5544 assert_eq!(
5545 selection!("$(null ?? true)", spec).apply_to(&json!({})),
5546 (Some(json!(true)), vec![]),
5547 );
5548 }
5549
5550 #[test]
5551 fn null_coalescing_should_preserve_object_type() {
5552 let spec = ConnectSpec::V0_3;
5553 assert_eq!(
5554 selection!("$(null ?? {'key': 'value'})", spec).apply_to(&json!({})),
5555 (Some(json!({"key": "value"})), vec![]),
5556 );
5557 }
5558
5559 #[test]
5560 fn null_coalescing_should_preserve_array_type() {
5561 let spec = ConnectSpec::V0_3;
5562 assert_eq!(
5563 selection!("$(null ?? [1, 2, 3])", spec).apply_to(&json!({})),
5564 (Some(json!([1, 2, 3])), vec![]),
5565 );
5566 }
5567
5568 #[test]
5569 fn null_coalescing_should_fallback_when_null_used_as_method_arg() {
5570 let spec = ConnectSpec::V0_3;
5571 assert_eq!(
5572 selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": null, "c": 5})),
5573 (Some(json!(10)), vec![]),
5574 );
5575 }
5576
5577 #[test]
5578 fn null_coalescing_should_fallback_when_none_used_as_method_arg() {
5579 let spec = ConnectSpec::V0_3;
5580 assert_eq!(
5581 selection!("$.a->add(missing ?? c)", spec)
5582 .apply_to(&json!({"a": 5, "b": null, "c": 5})),
5583 (Some(json!(10)), vec![]),
5584 );
5585 }
5586
5587 #[test]
5588 fn null_coalescing_should_not_fallback_when_not_null_used_as_method_arg() {
5589 let spec = ConnectSpec::V0_3;
5590 assert_eq!(
5591 selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": 3, "c": 5})),
5592 (Some(json!(8)), vec![]),
5593 );
5594 }
5595
5596 #[test]
5597 fn null_coalescing_should_allow_multiple_method_args() {
5598 let spec = ConnectSpec::V0_3;
5599 let add_selection = selection!("a->add(b ?? c, missing ?! c)", spec);
5600 assert_eq!(
5601 add_selection.apply_to(&json!({ "a": 5, "b": 3, "c": 7 })),
5602 (Some(json!(15)), vec![]),
5603 );
5604 assert_eq!(
5605 add_selection.apply_to(&json!({ "a": 5, "b": null, "c": 7 })),
5606 (Some(json!(19)), vec![]),
5607 );
5608 }
5609
5610 #[test]
5696 fn wtf_operator_should_not_exclude_null_from_nullable_union_shape() {
5697 let spec = ConnectSpec::V0_3;
5698
5699 let nullish_selection = selection!("$($value ?? 'fallback')", spec);
5701 let wtf_selection = selection!("$($value ?! 'fallback')", spec);
5702
5703 let mut vars = IndexMap::default();
5704 vars.insert("$value".to_string(), json!(null));
5705
5706 assert_eq!(
5707 nullish_selection.apply_with_vars(&json!({}), &vars),
5708 (Some(json!("fallback")), vec![]),
5709 );
5710
5711 assert_eq!(
5712 wtf_selection.apply_with_vars(&json!({}), &vars),
5713 (Some(json!(null)), vec![]),
5714 );
5715
5716 let mut vars_with_string_value = IndexMap::default();
5717 vars_with_string_value.insert("$value".to_string(), json!("fine"));
5718
5719 assert_eq!(
5720 nullish_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5721 (Some(json!("fine")), vec![]),
5722 );
5723
5724 assert_eq!(
5725 wtf_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5726 (Some(json!("fine")), vec![]),
5727 );
5728
5729 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5730 .with_spec(spec)
5731 .with_named_shapes([(
5732 "$value".to_string(),
5733 Shape::one([Shape::string([]), Shape::null([]), Shape::none()], []),
5734 )]);
5735
5736 assert_eq!(
5737 nullish_selection
5738 .compute_output_shape(&shape_context, Shape::none())
5741 .pretty_print(),
5742 "String",
5743 );
5744
5745 assert_eq!(
5746 wtf_selection
5747 .compute_output_shape(&shape_context, Shape::none())
5750 .pretty_print(),
5751 "One<String, null>",
5752 );
5753 }
5754
5755 #[test]
5756 fn question_operator_should_map_null_to_none() {
5757 let spec = ConnectSpec::V0_3;
5758
5759 let nullish_string_selection = selection!("$(stringOrNull?)", spec);
5760 assert_eq!(
5761 nullish_string_selection.apply_to(&json!({"stringOrNull": "a string"})),
5762 (Some(json!("a string")), vec![]),
5763 );
5764 assert_eq!(
5765 nullish_string_selection.apply_to(&json!({"stringOrNull": null})),
5766 (None, vec![]),
5767 );
5768 assert_eq!(
5769 nullish_string_selection.apply_to(&json!({})),
5770 (None, vec![]),
5771 );
5772
5773 let shape_context = {
5774 let mut named_shapes = IndexMap::default();
5775
5776 named_shapes.insert(
5777 "$root".to_string(),
5778 Shape::record(
5779 {
5780 let mut map = Shape::empty_map();
5781 map.insert(
5782 "stringOrNull".to_string(),
5783 Shape::one([Shape::string([]), Shape::null([])], []),
5784 );
5785 map
5786 },
5787 [],
5788 ),
5789 );
5790
5791 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5792 .with_spec(spec)
5793 .with_named_shapes(named_shapes)
5794 };
5795
5796 assert_eq!(
5797 nullish_string_selection
5798 .compute_output_shape(
5799 &shape_context,
5800 shape_context.named_shapes()["$root"].clone()
5801 )
5802 .pretty_print(),
5803 "One<String, None>",
5804 );
5805 }
5806
5807 #[test]
5808 fn question_operator_should_add_none_to_named_shapes() {
5809 let spec = ConnectSpec::V0_3;
5810
5811 let string_or_null_expr = selection!("$(stringOrNull?)", spec);
5812
5813 assert_eq!(
5814 string_or_null_expr.shape().pretty_print(),
5815 "$root.stringOrNull?",
5816 );
5817 }
5818
5819 #[test]
5820 fn question_operator_with_nested_objects() {
5821 let spec = ConnectSpec::V0_3;
5822
5823 let nested_selection = selection!("$(user?.profile?.name)", spec);
5824 assert_eq!(
5825 nested_selection.apply_to(&json!({"user": {"profile": {"name": "Alice"}}})),
5826 (Some(json!("Alice")), vec![]),
5827 );
5828 assert_eq!(
5829 nested_selection.apply_to(&json!({"user": null})),
5830 (None, vec![]),
5831 );
5832 assert_eq!(
5833 nested_selection.apply_to(&json!({"user": {"profile": null}})),
5834 (None, vec![]),
5835 );
5836 assert_eq!(nested_selection.apply_to(&json!({})), (None, vec![]));
5837 }
5838
5839 #[test]
5840 fn question_operator_with_nested_objects_shape() {
5841 let spec = ConnectSpec::V0_3;
5842
5843 let shape_context = {
5844 let mut named_shapes = IndexMap::default();
5845
5846 let person_shape = Shape::record(
5847 {
5848 let mut map = Shape::empty_map();
5849 map.insert("name".to_string(), Shape::string([]));
5850 map.insert("age".to_string(), Shape::int([]));
5851 map
5852 },
5853 [],
5854 );
5855
5856 named_shapes.insert(
5857 "$root".to_string(),
5858 Shape::record(
5859 {
5860 let mut map = Shape::empty_map();
5861 map.insert("person".to_string(), person_shape);
5862 map
5863 },
5864 [],
5865 ),
5866 );
5867
5868 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5869 .with_spec(spec)
5870 .with_named_shapes(named_shapes)
5871 };
5872
5873 let nested_selection = selection!("$(person?.name)", spec);
5874 assert_eq!(
5875 nested_selection
5876 .compute_output_shape(
5877 &shape_context,
5878 shape_context.named_shapes()["$root"].clone()
5879 )
5880 .pretty_print(),
5881 "One<String, None>",
5882 );
5883 }
5884
5885 #[test]
5886 fn question_operator_with_array_access() {
5887 let spec = ConnectSpec::V0_3;
5888
5889 let array_selection = selection!("$(items?->first?.name)", spec);
5890 assert_eq!(
5891 array_selection.apply_to(&json!({"items": [{"name": "first"}]})),
5892 (Some(json!("first")), vec![]),
5893 );
5894 assert_eq!(
5895 array_selection.apply_to(&json!({"items": []})),
5896 (None, vec![]),
5897 );
5898 assert_eq!(
5899 array_selection.apply_to(&json!({"items": null})),
5900 (None, vec![]),
5901 );
5902 assert_eq!(array_selection.apply_to(&json!({})), (None, vec![]));
5903 }
5904
5905 #[test]
5906 fn question_operator_with_union_shapes() {
5907 let spec = ConnectSpec::V0_3;
5908
5909 let shape_context = {
5910 let mut named_shapes = IndexMap::default();
5911
5912 named_shapes.insert(
5913 "$root".to_string(),
5914 Shape::record(
5915 {
5916 let mut map = Shape::empty_map();
5917 map.insert(
5918 "unionField".to_string(),
5919 Shape::one([Shape::string([]), Shape::int([]), Shape::null([])], []),
5920 );
5921 map
5922 },
5923 [],
5924 ),
5925 );
5926
5927 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5928 .with_spec(spec)
5929 .with_named_shapes(named_shapes)
5930 };
5931
5932 let union_selection = selection!("$(unionField?)", spec);
5933
5934 assert_eq!(
5935 union_selection
5936 .compute_output_shape(
5937 &shape_context,
5938 shape_context.named_shapes().get("$root").unwrap().clone(),
5939 )
5940 .pretty_print(),
5941 "One<String, Int, None>",
5942 );
5943 }
5944
5945 #[test]
5946 fn question_operator_with_union_shapes_non_nullable() {
5947 let spec = ConnectSpec::V0_3;
5948
5949 let shape_context = {
5950 let mut named_shapes = IndexMap::default();
5951
5952 named_shapes.insert(
5953 "$root".to_string(),
5954 Shape::record(
5955 {
5956 let mut map = Shape::empty_map();
5957 map.insert(
5958 "value".to_string(),
5959 Shape::one([Shape::string([]), Shape::int([])], []),
5960 );
5961 map
5962 },
5963 [],
5964 ),
5965 );
5966
5967 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5968 .with_spec(spec)
5969 .with_named_shapes(named_shapes)
5970 };
5971
5972 let union_selection = selection!("$(value?)", spec);
5973 assert_eq!(
5974 union_selection
5975 .compute_output_shape(
5976 &shape_context,
5977 shape_context.named_shapes()["$root"].clone()
5978 )
5979 .pretty_print(),
5980 "One<String, Int>",
5981 );
5982 }
5983
5984 #[test]
5985 fn question_operator_with_error_shapes() {
5986 let spec = ConnectSpec::V0_3;
5987
5988 let shape_context = {
5989 let mut named_shapes = IndexMap::default();
5990
5991 named_shapes.insert(
5992 "$root".to_string(),
5993 Shape::record(
5994 {
5995 let mut map = Shape::empty_map();
5996 map.insert(
5997 "errorField".to_string(),
5998 Shape::error_with_partial(
5999 "Test error".to_string(),
6000 Shape::one([Shape::string([]), Shape::null([])], []),
6001 [],
6002 ),
6003 );
6004 map
6005 },
6006 [],
6007 ),
6008 );
6009
6010 ShapeContext::new(SourceId::Other("JSONSelection".into()))
6011 .with_spec(spec)
6012 .with_named_shapes(named_shapes)
6013 };
6014
6015 let error_selection = selection!("$(errorField?)", spec);
6016
6017 let result_shape = error_selection.compute_output_shape(
6018 &shape_context,
6019 shape_context.named_shapes().get("$root").unwrap().clone(),
6020 );
6021
6022 assert!(result_shape.pretty_print().contains("Error"));
6024 assert!(result_shape.pretty_print().contains("None"));
6025 }
6026
6027 #[test]
6028 fn question_operator_with_simple_error_shapes() {
6029 let spec = ConnectSpec::V0_3;
6030
6031 let shape_context = {
6032 let mut named_shapes = IndexMap::default();
6033
6034 named_shapes.insert(
6035 "$root".to_string(),
6036 Shape::record(
6037 {
6038 let mut map = Shape::empty_map();
6039 map.insert(
6040 "error".to_string(),
6041 Shape::error("Something went wrong".to_string(), []),
6042 );
6043 map
6044 },
6045 [],
6046 ),
6047 );
6048
6049 ShapeContext::new(SourceId::Other("JSONSelection".into()))
6050 .with_spec(spec)
6051 .with_named_shapes(named_shapes)
6052 };
6053
6054 let error_selection = selection!("$(error?)", spec);
6055 assert_eq!(
6056 error_selection
6057 .compute_output_shape(
6058 &shape_context,
6059 shape_context.named_shapes()["$root"].clone()
6060 )
6061 .pretty_print(),
6062 "Error<\"Something went wrong\">",
6063 );
6064 }
6065
6066 #[test]
6067 fn question_operator_with_all_shapes() {
6068 let spec = ConnectSpec::V0_3;
6069
6070 let shape_context = {
6071 let mut named_shapes = IndexMap::default();
6072
6073 named_shapes.insert(
6074 "$root".to_string(),
6075 Shape::record(
6076 {
6077 let mut map = Shape::empty_map();
6078 map.insert(
6079 "allField".to_string(),
6080 Shape::all(
6081 [
6082 Shape::string([]),
6083 Shape::one([Shape::string([]), Shape::null([])], []),
6084 ],
6085 [],
6086 ),
6087 );
6088 map
6089 },
6090 [],
6091 ),
6092 );
6093
6094 ShapeContext::new(SourceId::Other("JSONSelection".into()))
6095 .with_spec(spec)
6096 .with_named_shapes(named_shapes)
6097 };
6098
6099 let all_selection = selection!("$(allField?)", spec);
6100
6101 assert_eq!(
6102 all_selection
6103 .compute_output_shape(
6104 &shape_context,
6105 shape_context.named_shapes().get("$root").unwrap().clone(),
6106 )
6107 .pretty_print(),
6108 "One<String, None>",
6109 );
6110 }
6111
6112 #[test]
6113 fn question_operator_with_simple_all_shapes() {
6114 let spec = ConnectSpec::V0_3;
6115
6116 let shape_context = {
6117 let mut named_shapes = IndexMap::default();
6118
6119 named_shapes.insert(
6120 "$root".to_string(),
6121 Shape::record(
6122 {
6123 let mut map = Shape::empty_map();
6124 map.insert(
6125 "intersection".to_string(),
6126 Shape::all([Shape::string([]), Shape::int([])], []),
6127 );
6128 map
6129 },
6130 [],
6131 ),
6132 );
6133
6134 ShapeContext::new(SourceId::Other("JSONSelection".into()))
6135 .with_spec(spec)
6136 .with_named_shapes(named_shapes)
6137 };
6138
6139 let all_selection = selection!("$(intersection?)", spec);
6140 assert_eq!(
6141 all_selection
6142 .compute_output_shape(
6143 &shape_context,
6144 shape_context.named_shapes()["$root"].clone()
6145 )
6146 .pretty_print(),
6147 "All<String, Int>",
6148 );
6149 }
6150
6151 #[test]
6152 fn question_operator_preserves_non_null_shapes() {
6153 let spec = ConnectSpec::V0_3;
6154
6155 let shape_context = {
6156 let mut named_shapes = IndexMap::default();
6157
6158 named_shapes.insert(
6159 "$root".to_string(),
6160 Shape::record(
6161 {
6162 let mut map = Shape::empty_map();
6163 map.insert("nonNullString".to_string(), Shape::string([]));
6164 map
6165 },
6166 [],
6167 ),
6168 );
6169
6170 ShapeContext::new(SourceId::Other("JSONSelection".into()))
6171 .with_spec(spec)
6172 .with_named_shapes(named_shapes)
6173 };
6174
6175 let non_null_selection = selection!("$(nonNullString?)", spec);
6176
6177 assert_eq!(
6178 non_null_selection
6179 .compute_output_shape(
6180 &shape_context,
6181 shape_context.named_shapes().get("$root").unwrap().clone(),
6182 )
6183 .pretty_print(),
6184 "String",
6185 );
6186 }
6187
6188 #[test]
6189 fn question_operator_with_multiple_operators_in_chain() {
6190 let spec = ConnectSpec::V0_3;
6191
6192 let mixed_chain_selection = selection!("$(field? ?? 'default')", spec);
6194 assert_eq!(
6195 mixed_chain_selection.apply_to(&json!({"field": "value"})),
6196 (Some(json!("value")), vec![]),
6197 );
6198 assert_eq!(
6199 mixed_chain_selection.apply_to(&json!({"field": null})),
6200 (Some(json!("default")), vec![]),
6201 );
6202 assert_eq!(
6203 mixed_chain_selection.apply_to(&json!({})),
6204 (Some(json!("default")), vec![]),
6205 );
6206 }
6207
6208 #[test]
6209 fn question_operator_chained_shape() {
6210 let spec = ConnectSpec::V0_3;
6211
6212 let shape_context = {
6213 let mut named_shapes = IndexMap::default();
6214
6215 named_shapes.insert(
6216 "$root".to_string(),
6217 Shape::record(
6218 {
6219 let mut map = Shape::empty_map();
6220 map.insert(
6221 "level1".to_string(),
6222 Shape::record(
6223 {
6224 let mut inner_map = Shape::empty_map();
6225 inner_map.insert(
6226 "level2".to_string(),
6227 Shape::record(
6228 {
6229 let mut inner_inner_map = Shape::empty_map();
6230 inner_inner_map
6231 .insert("value".to_string(), Shape::string([]));
6232 inner_inner_map
6233 },
6234 [],
6235 ),
6236 );
6237 inner_map
6238 },
6239 [],
6240 ),
6241 );
6242 map
6243 },
6244 [],
6245 ),
6246 );
6247
6248 ShapeContext::new(SourceId::Other("JSONSelection".into()))
6249 .with_spec(spec)
6250 .with_named_shapes(named_shapes)
6251 };
6252
6253 let chained_selection = selection!("$(level1?.level2?.value)", spec);
6254 assert_eq!(
6255 chained_selection
6256 .compute_output_shape(
6257 &shape_context,
6258 shape_context.named_shapes()["$root"].clone()
6259 )
6260 .pretty_print(),
6261 "One<String, None>",
6262 );
6263 }
6264
6265 #[test]
6266 fn question_operator_direct_null_input_shape() {
6267 let spec = ConnectSpec::V0_3;
6268
6269 let shape_context = {
6270 let mut named_shapes = IndexMap::default();
6271
6272 named_shapes.insert("$root".to_string(), Shape::null([]));
6273
6274 ShapeContext::new(SourceId::Other("JSONSelection".into()))
6275 .with_spec(spec)
6276 .with_named_shapes(named_shapes)
6277 };
6278
6279 let null_selection = selection!("$root?", spec);
6280
6281 assert_eq!(
6282 null_selection
6283 .compute_output_shape(
6284 &shape_context,
6285 shape_context.named_shapes().get("$root").unwrap().clone(),
6286 )
6287 .pretty_print(),
6288 "None",
6289 );
6290 }
6291
6292 #[test]
6293 fn test_unknown_name() {
6294 let spec = ConnectSpec::V0_3;
6295 let sel = selection!("book.author? { name age? }", spec);
6296 assert_eq!(
6297 sel.shape().pretty_print(),
6298 r#"One<
6299 {
6300 age: $root.book.author?.*.age?,
6301 name: $root.book.author?.*.name,
6302 },
6303 None,
6304>"#,
6305 );
6306 }
6307
6308 #[test]
6309 fn test_nullish_coalescing_shape() {
6310 let spec = ConnectSpec::V0_3;
6311 let sel = selection!("$(a ?? b ?? c)", spec);
6312 assert_eq!(
6313 sel.shape().pretty_print(),
6314 "One<$root.a?!, $root.b?!, $root.c>",
6315 );
6316
6317 let mut named_shapes = IndexMap::default();
6318 named_shapes.insert(
6319 "$root".to_string(),
6320 Shape::record(
6321 {
6322 let mut map = Shape::empty_map();
6323 map.insert(
6324 "a".to_string(),
6325 Shape::one([Shape::string([]), Shape::null([])], []),
6326 );
6327 map.insert("b".to_string(), Shape::string([]));
6328 map.insert("c".to_string(), Shape::int([]));
6329 map
6330 },
6331 [],
6332 ),
6333 );
6334
6335 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
6336 .with_spec(spec)
6337 .with_named_shapes(named_shapes);
6338
6339 assert_eq!(
6340 sel.compute_output_shape(
6341 &shape_context,
6342 shape_context.named_shapes().get("$root").unwrap().clone(),
6343 )
6344 .pretty_print(),
6345 "One<String, Int>",
6346 );
6347 }
6348
6349 #[test]
6350 fn test_none_coalescing_shape() {
6351 let spec = ConnectSpec::V0_3;
6352 let sel = selection!("$(a ?! b ?! c)", spec);
6353 assert_eq!(
6354 sel.shape().pretty_print(),
6355 "One<$root.a!, $root.b!, $root.c>",
6356 );
6357
6358 let mut named_shapes = IndexMap::default();
6359 named_shapes.insert(
6360 "$root".to_string(),
6361 Shape::record(
6362 {
6363 let mut map = Shape::empty_map();
6364 map.insert(
6365 "a".to_string(),
6366 Shape::one([Shape::string([]), Shape::null([])], []),
6367 );
6368 map.insert(
6369 "b".to_string(),
6370 Shape::one([Shape::string([]), Shape::none()], []),
6371 );
6372 map.insert("c".to_string(), Shape::null([]));
6373 map
6374 },
6375 [],
6376 ),
6377 );
6378
6379 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
6380 .with_spec(spec)
6381 .with_named_shapes(named_shapes);
6382
6383 assert_eq!(
6384 sel.compute_output_shape(
6385 &shape_context,
6386 shape_context.named_shapes().get("$root").unwrap().clone(),
6387 )
6388 .pretty_print(),
6389 "One<String, null>",
6390 );
6391 }
6392
6393 #[test]
6394 fn test_none_coalescing_with_literal_fallback() {
6395 let spec = ConnectSpec::V0_3;
6396
6397 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
6398 .with_spec(spec)
6399 .with_named_shapes([(
6400 "$root".to_string(),
6401 Shape::record(
6402 {
6403 let mut map = Shape::empty_map();
6404 map.insert(
6405 "optional".to_string(),
6406 Shape::one([Shape::string([]), Shape::none()], []),
6407 );
6408 map
6409 },
6410 [],
6411 ),
6412 )]);
6413
6414 let none_coalesce = selection!("$(optional ?! 'fallback')", spec);
6415 assert_eq!(
6416 none_coalesce
6417 .compute_output_shape(
6418 &shape_context,
6419 shape_context.named_shapes()["$root"].clone()
6420 )
6421 .pretty_print(),
6422 "String",
6423 );
6424 }
6425
6426 #[test]
6432 fn test_multiple_spreads_shape_with_conflicting_typename() {
6433 let spec = ConnectSpec::V0_4;
6434
6435 let selection = selection!(
6437 r#"
6438 upc
6439 ... category->match(
6440 ['book', { __typename: $('Book'), title: $.title }],
6441 ['film', { __typename: $('Film'), director: $.director }],
6442 [@, null]
6443 )
6444 ... format->match(
6445 ['digital', { __typename: $('Digital'), downloadUrl: $.url }],
6446 ['physical', { __typename: $('Physical'), weight: $.weight }],
6447 [@, null]
6448 )
6449 "#,
6450 spec
6451 );
6452
6453 assert_eq!(
6455 selection.shape().pretty_print(),
6456 r#"One<
6457 {
6458 __typename: All<"Book", "Digital">,
6459 downloadUrl: $root.*.url,
6460 title: $root.*.title,
6461 upc: $root.*.upc,
6462 },
6463 {
6464 __typename: All<"Book", "Physical">,
6465 title: $root.*.title,
6466 upc: $root.*.upc,
6467 weight: $root.*.weight,
6468 },
6469 null,
6470 {
6471 __typename: All<"Film", "Digital">,
6472 director: $root.*.director,
6473 downloadUrl: $root.*.url,
6474 upc: $root.*.upc,
6475 },
6476 {
6477 __typename: All<"Film", "Physical">,
6478 director: $root.*.director,
6479 upc: $root.*.upc,
6480 weight: $root.*.weight,
6481 },
6482>"#,
6483 );
6484 }
6485
6486 #[test]
6491 fn test_multiple_spreads_shape_without_typename_conflict() {
6492 let spec = ConnectSpec::V0_4;
6493
6494 let selection = selection!(
6496 r#"
6497 upc
6498 ... category->match(
6499 ['book', { __typename: $('Book'), title: $.title }],
6500 ['film', { __typename: $('Film'), director: $.director }],
6501 [@, null]
6502 )
6503 ... format->match(
6504 ['digital', { downloadUrl: $.url }],
6505 ['physical', { weight: $.weight }],
6506 [@, null]
6507 )
6508 "#,
6509 spec
6510 );
6511
6512 assert_eq!(
6514 selection.shape().pretty_print(),
6515 r#"One<
6516 {
6517 __typename: "Book",
6518 downloadUrl: $root.*.url,
6519 title: $root.*.title,
6520 upc: $root.*.upc,
6521 },
6522 {
6523 __typename: "Book",
6524 title: $root.*.title,
6525 upc: $root.*.upc,
6526 weight: $root.*.weight,
6527 },
6528 null,
6529 {
6530 __typename: "Film",
6531 director: $root.*.director,
6532 downloadUrl: $root.*.url,
6533 upc: $root.*.upc,
6534 },
6535 {
6536 __typename: "Film",
6537 director: $root.*.director,
6538 upc: $root.*.upc,
6539 weight: $root.*.weight,
6540 },
6541>"#,
6542 );
6543 }
6544
6545 #[test]
6547 fn test_single_spread_shape() {
6548 let spec = ConnectSpec::V0_4;
6549
6550 let selection = selection!(
6551 r#"
6552 upc
6553 ... category->match(
6554 ['book', { __typename: $('Book'), title: $.title }],
6555 ['film', { __typename: $('Film'), director: $.director }],
6556 [@, null]
6557 )
6558 "#,
6559 spec
6560 );
6561
6562 assert_eq!(
6564 selection.shape().pretty_print(),
6565 r#"One<
6566 {
6567 __typename: "Book",
6568 title: $root.*.title,
6569 upc: $root.*.upc,
6570 },
6571 {
6572 __typename: "Film",
6573 director: $root.*.director,
6574 upc: $root.*.upc,
6575 },
6576 null,
6577>"#,
6578 );
6579 }
6580
6581 #[test]
6586 fn test_multiple_spreads_same_typename_no_conflict() {
6587 let spec = ConnectSpec::V0_4;
6588
6589 let selection = selection!(
6591 r#"
6592 upc
6593 ... category->match(
6594 ['book', { __typename: $('Book'), title: $.title }],
6595 ['film', { __typename: $('Film'), director: $.director }],
6596 [@, null]
6597 )
6598 ... category->match(
6599 ['book', { __typename: $('Book'), author: $.author }]
6600 )
6601 "#,
6602 spec
6603 );
6604
6605 assert_eq!(
6611 selection.shape().pretty_print(),
6612 r#"One<
6613 {
6614 __typename: "Book",
6615 author: $root.*.author,
6616 title: $root.*.title,
6617 upc: $root.*.upc,
6618 },
6619 {
6620 __typename: "Book",
6621 title: $root.*.title,
6622 upc: $root.*.upc,
6623 },
6624 {
6625 __typename: All<"Film", "Book">,
6626 author: $root.*.author,
6627 director: $root.*.director,
6628 upc: $root.*.upc,
6629 },
6630 {
6631 __typename: "Film",
6632 director: $root.*.director,
6633 upc: $root.*.upc,
6634 },
6635 null,
6636>"#,
6637 );
6638 }
6639
6640 #[test]
6642 fn test_multiple_spreads_field_shadowing() {
6643 let spec = ConnectSpec::V0_4;
6644
6645 let selection = selection!(
6647 r#"
6648 upc
6649 ... category->match(
6650 ['book', { __typename: $('Book'), greeting: $('hello') }],
6651 [@, null]
6652 )
6653 ... category->match(
6654 ['book', { __typename: $('Book'), greeting: $('goodbye') }]
6655 )
6656 "#,
6657 spec
6658 );
6659
6660 assert_eq!(
6664 selection.shape().pretty_print(),
6665 r#"One<
6666 {
6667 __typename: "Book",
6668 greeting: All<"hello", "goodbye">,
6669 upc: $root.*.upc,
6670 },
6671 { __typename: "Book", greeting: "hello", upc: $root.*.upc },
6672 null,
6673>"#,
6674 );
6675 }
6676
6677 #[test]
6679 fn test_second_spread_no_typename_adds_to_all() {
6680 let spec = ConnectSpec::V0_4;
6681
6682 let selection = selection!(
6685 r#"
6686 upc
6687 ... category->match(
6688 ['book', { __typename: $('Book') }],
6689 ['film', { __typename: $('Film') }],
6690 [@, null]
6691 )
6692 ... format->match(
6693 ['digital', { isDigital: $(true) }],
6694 [@, null]
6695 )
6696 "#,
6697 spec
6698 );
6699
6700 assert_eq!(
6702 selection.shape().pretty_print(),
6703 r#"One<
6704 { __typename: "Book", isDigital: true, upc: $root.*.upc },
6705 null,
6706 { __typename: "Film", isDigital: true, upc: $root.*.upc },
6707>"#,
6708 );
6709 }
6710
6711 #[test]
6719 fn test_spread_cartesian_product_conservative() {
6720 let spec = ConnectSpec::V0_4;
6721
6722 let selection = selection!(
6724 r#"
6725 upc
6726 ... category->match(
6727 ['book', { __typename: $('Book'), title: $.title }],
6728 ['film', { __typename: $('Film'), director: $.director }],
6729 [@, null]
6730 )
6731 ... category->match(
6732 ['book', { extraField: $('only for books') }]
6733 )
6734 "#,
6735 spec
6736 );
6737
6738 assert_eq!(
6744 selection.shape().pretty_print(),
6745 r#"One<
6746 {
6747 __typename: "Book",
6748 extraField: "only for books",
6749 title: $root.*.title,
6750 upc: $root.*.upc,
6751 },
6752 {
6753 __typename: "Book",
6754 title: $root.*.title,
6755 upc: $root.*.upc,
6756 },
6757 {
6758 __typename: "Film",
6759 director: $root.*.director,
6760 extraField: "only for books",
6761 upc: $root.*.upc,
6762 },
6763 {
6764 __typename: "Film",
6765 director: $root.*.director,
6766 upc: $root.*.upc,
6767 },
6768 null,
6769>"#,
6770 );
6771 }
6772
6773 #[test]
6779 fn test_multiple_spreads_runtime_both_match_book() {
6780 let spec = ConnectSpec::V0_4;
6781 let selection = selection!(
6782 r#"
6783 upc
6784 ... category->match(
6785 ['book', { __typename: $('Book'), title: $.title }],
6786 ['film', { __typename: $('Film'), director: $.director }],
6787 [@, null]
6788 )
6789 ... category->match(
6790 ['book', { __typename: $('Book'), author: $.author }]
6791 )
6792 "#,
6793 spec
6794 );
6795
6796 let book_data = json!({
6798 "upc": "123",
6799 "category": "book",
6800 "title": "Great Gatsby",
6801 "author": "Fitzgerald"
6802 });
6803 let (result, errors) = selection.apply_to(&book_data);
6804 assert_eq!(errors, vec![]);
6805 assert_eq!(
6806 result,
6807 Some(json!({
6808 "upc": "123",
6809 "__typename": "Book",
6810 "title": "Great Gatsby",
6811 "author": "Fitzgerald"
6812 }))
6813 );
6814 }
6815
6816 #[test]
6818 fn test_multiple_spreads_runtime_film_no_second_match() {
6819 let spec = ConnectSpec::V0_4;
6820 let selection = selection!(
6821 r#"
6822 upc
6823 ... category->match(
6824 ['book', { __typename: $('Book'), title: $.title }],
6825 ['film', { __typename: $('Film'), director: $.director }],
6826 [@, null]
6827 )
6828 ... category->match(
6829 ['book', { __typename: $('Book'), author: $.author }]
6830 )
6831 "#,
6832 spec
6833 );
6834
6835 let film_data = json!({
6837 "upc": "456",
6838 "category": "film",
6839 "director": "Nolan"
6840 });
6841 let (result, errors) = selection.apply_to(&film_data);
6842 assert!(!errors.is_empty());
6844 assert_eq!(
6845 result,
6846 Some(json!({
6847 "upc": "456",
6848 "__typename": "Film",
6849 "director": "Nolan"
6850 }))
6851 );
6852 }
6853
6854 #[test]
6856 fn test_multiple_spreads_runtime_field_shadowing() {
6857 let spec = ConnectSpec::V0_4;
6858 let selection = selection!(
6859 r#"
6860 upc
6861 ... category->match(
6862 ['book', { __typename: $('Book'), greeting: $('hello') }],
6863 [@, null]
6864 )
6865 ... category->match(
6866 ['book', { __typename: $('Book'), greeting: $('goodbye') }]
6867 )
6868 "#,
6869 spec
6870 );
6871
6872 let book_data = json!({
6874 "upc": "789",
6875 "category": "book"
6876 });
6877 let (result, errors) = selection.apply_to(&book_data);
6878 assert_eq!(errors, vec![]);
6879 assert_eq!(
6881 result,
6882 Some(json!({
6883 "upc": "789",
6884 "__typename": "Book",
6885 "greeting": "goodbye"
6886 }))
6887 );
6888 }
6889
6890 #[test]
6892 fn test_multiple_spreads_runtime_no_typename_adds_to_all() {
6893 let spec = ConnectSpec::V0_4;
6894 let selection = selection!(
6895 r#"
6896 upc
6897 ... category->match(
6898 ['book', { __typename: $('Book') }],
6899 ['film', { __typename: $('Film') }],
6900 [@, null]
6901 )
6902 ... format->match(
6903 ['digital', { isDigital: $(true) }],
6904 [@, null]
6905 )
6906 "#,
6907 spec
6908 );
6909
6910 let book_digital = json!({
6912 "upc": "111",
6913 "category": "book",
6914 "format": "digital"
6915 });
6916 let (result, errors) = selection.apply_to(&book_digital);
6917 assert_eq!(errors, vec![]);
6918 assert_eq!(
6919 result,
6920 Some(json!({
6921 "upc": "111",
6922 "__typename": "Book",
6923 "isDigital": true
6924 }))
6925 );
6926
6927 let film_digital = json!({
6929 "upc": "222",
6930 "category": "film",
6931 "format": "digital"
6932 });
6933 let (result, errors) = selection.apply_to(&film_digital);
6934 assert_eq!(errors, vec![]);
6935 assert_eq!(
6936 result,
6937 Some(json!({
6938 "upc": "222",
6939 "__typename": "Film",
6940 "isDigital": true
6941 }))
6942 );
6943
6944 let book_physical = json!({
6947 "upc": "333",
6948 "category": "book",
6949 "format": "physical"
6950 });
6951 let (result, errors) = selection.apply_to(&book_physical);
6952 assert_eq!(errors, vec![]);
6953 assert_eq!(result, Some(json!(null)));
6955 }
6956
6957 #[test]
6959 fn test_multiple_spreads_runtime_no_match_returns_null() {
6960 let spec = ConnectSpec::V0_4;
6961 let selection = selection!(
6962 r#"
6963 upc
6964 ... category->match(
6965 ['book', { __typename: $('Book'), title: $.title }],
6966 ['film', { __typename: $('Film'), director: $.director }],
6967 [@, null]
6968 )
6969 "#,
6970 spec
6971 );
6972
6973 let unknown_data = json!({
6975 "upc": "999",
6976 "category": "unknown"
6977 });
6978 let (result, errors) = selection.apply_to(&unknown_data);
6979 assert_eq!(errors, vec![]);
6980 assert_eq!(result, Some(json!(null)));
6981 }
6982}