1use std::hash::Hash;
4
5use apollo_compiler::collections::IndexMap;
6use apollo_compiler::collections::IndexSet;
7use serde_json_bytes::Map as JSONMap;
8use serde_json_bytes::Value as JSON;
9use serde_json_bytes::json;
10use shape::Shape;
11use shape::ShapeCase;
12use shape::location::Location;
13use shape::location::SourceId;
14
15use super::helpers::json_merge;
16use super::helpers::json_type_name;
17use super::immutable::InputPath;
18use super::known_var::KnownVariable;
19use super::lit_expr::LitExpr;
20use super::lit_expr::LitOp;
21use super::location::OffsetRange;
22use super::location::Ranged;
23use super::location::WithRange;
24use super::methods::ArrowMethod;
25use super::parser::*;
26use crate::connectors::spec::ConnectSpec;
27
28pub(super) type VarsWithPathsMap<'a> = IndexMap<KnownVariable, (&'a JSON, InputPath<JSON>)>;
29
30impl JSONSelection {
31 pub fn apply_to(&self, data: &JSON) -> (Option<JSON>, Vec<ApplyToError>) {
37 self.apply_with_vars(data, &IndexMap::default())
38 }
39
40 pub fn apply_with_vars(
41 &self,
42 data: &JSON,
43 vars: &IndexMap<String, JSON>,
44 ) -> (Option<JSON>, Vec<ApplyToError>) {
45 let mut errors = IndexSet::default();
47
48 let mut vars_with_paths: VarsWithPathsMap = IndexMap::default();
49 for (var_name, var_data) in vars {
50 vars_with_paths.insert(
51 KnownVariable::External(var_name.as_str().to_string()),
52 (var_data, InputPath::empty().append(json!(var_name))),
53 );
54 }
55 vars_with_paths.insert(KnownVariable::Dollar, (data, InputPath::empty()));
59
60 let spec = self.spec();
61 let (value, apply_errors) =
62 self.apply_to_path(data, &vars_with_paths, &InputPath::empty(), spec);
63
64 errors.extend(apply_errors);
70
71 (value, errors.into_iter().collect())
72 }
73
74 pub fn shape(&self) -> Shape {
75 let context =
76 ShapeContext::new(SourceId::Other("JSONSelection".into())).with_spec(self.spec());
77
78 self.compute_output_shape(
79 &context,
82 Shape::name("$root", Vec::new()),
93 )
94 }
95
96 pub(crate) fn compute_output_shape(&self, context: &ShapeContext, input_shape: Shape) -> Shape {
97 debug_assert_eq!(context.spec(), self.spec());
98
99 let computable: &dyn ApplyToInternal = match &self.inner {
100 TopLevelSelection::Named(selection) => selection,
101 TopLevelSelection::Path(path_selection) => path_selection,
102 };
103
104 let dollar_shape = input_shape.clone();
105
106 if Some(&input_shape) == context.named_shapes().get("$root") {
107 computable.compute_output_shape(context, input_shape, dollar_shape)
110 } else {
111 let cloned_context = context
114 .clone()
115 .with_named_shapes([("$root".to_string(), input_shape.clone())]);
116 computable.compute_output_shape(&cloned_context, input_shape, dollar_shape)
117 }
118 }
119}
120
121fn lookup_variable<'a>(
122 vars: &'a VarsWithPathsMap,
123 var_name: &str,
124) -> Option<(&'a JSON, &'a InputPath<JSON>)> {
125 let entry = if var_name == "$" {
126 vars.get(&KnownVariable::Dollar)
127 } else {
128 vars.get(&KnownVariable::Local(var_name.to_string()))
132 .or_else(|| vars.get(&KnownVariable::External(var_name.to_string())))
133 };
134 entry.map(|(data, path)| (*data, path))
135}
136
137impl Ranged for JSONSelection {
138 fn range(&self) -> OffsetRange {
139 match &self.inner {
140 TopLevelSelection::Named(selection) => selection.range(),
141 TopLevelSelection::Path(path_selection) => path_selection.range(),
142 }
143 }
144
145 fn shape_location(&self, source_id: &SourceId) -> Option<Location> {
146 self.range().map(|range| source_id.location(range))
147 }
148}
149
150pub(super) trait ApplyToInternal {
151 fn apply_to_path(
154 &self,
155 data: &JSON,
156 vars: &VarsWithPathsMap,
157 input_path: &InputPath<JSON>,
158 spec: ConnectSpec,
159 ) -> (Option<JSON>, Vec<ApplyToError>);
160
161 fn apply_to_array(
164 &self,
165 data_array: &[JSON],
166 vars: &VarsWithPathsMap,
167 input_path: &InputPath<JSON>,
168 spec: ConnectSpec,
169 ) -> (Option<JSON>, Vec<ApplyToError>) {
170 let mut output = Vec::with_capacity(data_array.len());
171 let mut errors = Vec::new();
172
173 for (i, element) in data_array.iter().enumerate() {
174 let input_path_with_index = input_path.append(json!(i));
175 let (applied, apply_errors) =
176 self.apply_to_path(element, vars, &input_path_with_index, spec);
177 errors.extend(apply_errors);
178 output.push(applied.unwrap_or(JSON::Null));
182 }
183
184 (Some(JSON::Array(output)), errors)
185 }
186
187 fn compute_output_shape(
191 &self,
192 context: &ShapeContext,
193 input_shape: Shape,
196 dollar_shape: Shape,
200 ) -> Shape;
201}
202
203#[derive(Debug, Clone, Eq, PartialEq)]
204pub(crate) struct ShapeContext {
205 #[allow(dead_code)]
208 spec: ConnectSpec,
209
210 named_shapes: IndexMap<String, Shape>,
215
216 source_id: SourceId,
219}
220
221impl ShapeContext {
222 pub(crate) fn new(source_id: SourceId) -> Self {
223 Self {
224 spec: JSONSelection::default_connect_spec(),
225 named_shapes: IndexMap::default(),
226 source_id,
227 }
228 }
229
230 #[allow(dead_code)]
231 pub(crate) fn spec(&self) -> ConnectSpec {
232 self.spec
233 }
234
235 pub(crate) fn with_spec(mut self, spec: ConnectSpec) -> Self {
236 self.spec = spec;
237 self
238 }
239
240 pub(crate) fn named_shapes(&self) -> &IndexMap<String, Shape> {
241 &self.named_shapes
242 }
243
244 pub(crate) fn with_named_shapes(
245 mut self,
246 named_shapes: impl IntoIterator<Item = (String, Shape)>,
247 ) -> Self {
248 for (name, shape) in named_shapes {
249 self.named_shapes.insert(name.clone(), shape.clone());
250 }
251 self
252 }
253
254 pub(crate) fn source_id(&self) -> &SourceId {
255 &self.source_id
256 }
257}
258
259#[derive(Debug, Eq, PartialEq, Clone, Hash)]
260pub struct ApplyToError {
261 message: String,
262 path: Vec<JSON>,
263 range: OffsetRange,
264 spec: ConnectSpec,
265}
266
267impl ApplyToError {
268 pub(crate) const fn new(
269 message: String,
270 path: Vec<JSON>,
271 range: OffsetRange,
272 spec: ConnectSpec,
273 ) -> Self {
274 Self {
275 message,
276 path,
277 range,
278 spec,
279 }
280 }
281
282 #[cfg(test)]
285 pub(crate) fn from_json(json: &JSON) -> Self {
286 use crate::link::spec::Version;
287
288 let error = json.as_object().unwrap();
289 let message = error.get("message").unwrap().as_str().unwrap().to_string();
290 let path = error.get("path").unwrap().as_array().unwrap().clone();
291 let range = error.get("range").unwrap().as_array().unwrap();
292 let spec = error
293 .get("spec")
294 .and_then(|s| s.as_str())
295 .and_then(|s| match s.parse::<Version>() {
296 Ok(version) => ConnectSpec::try_from(&version).ok(),
297 Err(_) => None,
298 })
299 .unwrap_or_else(ConnectSpec::latest);
300
301 Self {
302 message,
303 path,
304 range: if range.len() == 2 {
305 let start = range[0].as_u64().unwrap() as usize;
306 let end = range[1].as_u64().unwrap() as usize;
307 Some(start..end)
308 } else {
309 None
310 },
311 spec,
312 }
313 }
314
315 pub fn message(&self) -> &str {
316 self.message.as_str()
317 }
318
319 pub fn path(&self) -> &[JSON] {
320 self.path.as_slice()
321 }
322
323 pub fn range(&self) -> OffsetRange {
324 self.range.clone()
325 }
326
327 pub fn spec(&self) -> ConnectSpec {
328 self.spec
329 }
330}
331
332pub(super) trait ApplyToResultMethods {
336 fn prepend_errors(self, errors: Vec<ApplyToError>) -> Self;
337
338 fn and_then_collecting_errors(
339 self,
340 f: impl FnOnce(&JSON) -> (Option<JSON>, Vec<ApplyToError>),
341 ) -> (Option<JSON>, Vec<ApplyToError>);
342}
343
344impl ApplyToResultMethods for (Option<JSON>, Vec<ApplyToError>) {
345 fn prepend_errors(self, mut errors: Vec<ApplyToError>) -> Self {
349 if errors.is_empty() {
350 self
351 } else {
352 let (value_opt, apply_errors) = self;
353 errors.extend(apply_errors);
354 (value_opt, errors)
355 }
356 }
357
358 fn and_then_collecting_errors(
361 self,
362 f: impl FnOnce(&JSON) -> (Option<JSON>, Vec<ApplyToError>),
363 ) -> (Option<JSON>, Vec<ApplyToError>) {
364 match self {
365 (Some(data), errors) => f(&data).prepend_errors(errors),
366 (None, errors) => (None, errors),
367 }
368 }
369}
370
371impl ApplyToInternal for JSONSelection {
372 fn apply_to_path(
373 &self,
374 data: &JSON,
375 vars: &VarsWithPathsMap,
376 input_path: &InputPath<JSON>,
377 _spec: ConnectSpec,
378 ) -> (Option<JSON>, Vec<ApplyToError>) {
379 match &self.inner {
380 TopLevelSelection::Named(named_selections) => {
387 named_selections.apply_to_path(data, vars, input_path, self.spec)
388 }
389 TopLevelSelection::Path(path_selection) => {
390 path_selection.apply_to_path(data, vars, input_path, self.spec)
391 }
392 }
393 }
394
395 fn compute_output_shape(
396 &self,
397 context: &ShapeContext,
398 input_shape: Shape,
399 dollar_shape: Shape,
400 ) -> Shape {
401 debug_assert_eq!(context.spec(), self.spec());
402
403 match &self.inner {
404 TopLevelSelection::Named(selection) => {
405 selection.compute_output_shape(context, input_shape, dollar_shape)
406 }
407 TopLevelSelection::Path(path_selection) => {
408 path_selection.compute_output_shape(context, input_shape, dollar_shape)
409 }
410 }
411 }
412}
413
414impl ApplyToInternal for NamedSelection {
415 fn apply_to_path(
416 &self,
417 data: &JSON,
418 vars: &VarsWithPathsMap,
419 input_path: &InputPath<JSON>,
420 spec: ConnectSpec,
421 ) -> (Option<JSON>, Vec<ApplyToError>) {
422 let mut output: Option<JSON> = None;
423 let mut errors = Vec::new();
424
425 let (value_opt, apply_errors) = self.path.apply_to_path(data, vars, input_path, spec);
426 errors.extend(apply_errors);
427
428 match &self.prefix {
429 NamingPrefix::Alias(alias) => {
430 if let Some(value) = value_opt {
431 output = Some(json!({ alias.name.as_str(): value }));
432 }
433 }
434
435 NamingPrefix::Spread(_spread_range) => {
436 match value_opt {
437 Some(JSON::Object(_) | JSON::Null) => {
438 output = value_opt;
441 }
442 Some(value) => {
443 errors.push(ApplyToError::new(
444 format!("Expected object or null, not {}", json_type_name(&value)),
445 input_path.to_vec(),
446 self.path.range(),
447 spec,
448 ));
449 }
450 None => {
451 errors.push(ApplyToError::new(
452 "Inlined path produced no value".to_string(),
453 input_path.to_vec(),
454 self.path.range(),
455 spec,
456 ));
457 }
458 };
459 }
460
461 NamingPrefix::None => {
462 if let Some(single_key) = self.path.get_single_key() {
467 if let Some(value) = value_opt {
468 output = Some(json!({ single_key.as_str(): value }));
469 }
470 } else {
471 output = value_opt;
472 }
473 }
474 }
475
476 (output, errors)
477 }
478
479 fn compute_output_shape(
480 &self,
481 context: &ShapeContext,
482 input_shape: Shape,
483 dollar_shape: Shape,
484 ) -> Shape {
485 let path_shape = self
486 .path
487 .compute_output_shape(context, input_shape, dollar_shape);
488
489 if let Some(single_output_key) = self.get_single_key() {
490 let mut map = Shape::empty_map();
491 map.insert(single_output_key.as_string(), path_shape);
492 Shape::record(map, self.shape_location(context.source_id()))
493 } else {
494 path_shape
495 }
496 }
497}
498
499impl ApplyToInternal for PathSelection {
500 fn apply_to_path(
501 &self,
502 data: &JSON,
503 vars: &VarsWithPathsMap,
504 input_path: &InputPath<JSON>,
505 spec: ConnectSpec,
506 ) -> (Option<JSON>, Vec<ApplyToError>) {
507 match (self.path.as_ref(), vars.get(&KnownVariable::Dollar)) {
508 (PathList::Key(_, _), Some((dollar_data, dollar_path))) => {
514 self.path
515 .apply_to_path(dollar_data, vars, dollar_path, spec)
516 }
517
518 _ => self.path.apply_to_path(data, vars, input_path, spec),
523 }
524 }
525
526 fn compute_output_shape(
527 &self,
528 context: &ShapeContext,
529 input_shape: Shape,
530 dollar_shape: Shape,
531 ) -> Shape {
532 match self.path.as_ref() {
533 PathList::Key(_, _) => {
534 self.path
538 .compute_output_shape(context, dollar_shape.clone(), dollar_shape)
539 }
540 _ => self
543 .path
544 .compute_output_shape(context, input_shape, dollar_shape),
545 }
546 }
547}
548
549impl ApplyToInternal for WithRange<PathList> {
550 fn apply_to_path(
551 &self,
552 data: &JSON,
553 vars: &VarsWithPathsMap,
554 input_path: &InputPath<JSON>,
555 spec: ConnectSpec,
556 ) -> (Option<JSON>, Vec<ApplyToError>) {
557 match self.as_ref() {
558 PathList::Var(ranged_var_name, tail) => {
559 let var_name = ranged_var_name.as_ref();
560 if var_name == &KnownVariable::AtSign {
561 tail.apply_to_path(data, vars, input_path, spec)
565 } else if let Some((var_data, var_path)) = lookup_variable(vars, var_name.as_str())
566 {
567 tail.apply_to_path(var_data, vars, var_path, spec)
572 } else {
573 (
574 None,
575 vec![ApplyToError::new(
576 format!("Variable {} not found", var_name.as_str()),
577 input_path.to_vec(),
578 ranged_var_name.range(),
579 spec,
580 )],
581 )
582 }
583 }
584 PathList::Key(key, tail) => {
585 let input_path_with_key = input_path.append(key.to_json());
586
587 if let JSON::Array(array) = data {
588 let empty_tail = WithRange::new(PathList::Empty, tail.range());
594 let self_with_empty_tail =
595 WithRange::new(PathList::Key(key.clone(), empty_tail), key.range());
596
597 self_with_empty_tail
598 .apply_to_array(array, vars, input_path, spec)
599 .and_then_collecting_errors(|shallow_mapped_array| {
600 tail.apply_to_path(
604 shallow_mapped_array,
605 vars,
606 &input_path_with_key,
607 spec,
608 )
609 })
610 } else {
611 let not_found = || {
612 (
613 None,
614 vec![ApplyToError::new(
615 format!(
616 "Property {} not found in {}",
617 key.dotted(),
618 json_type_name(data),
619 ),
620 input_path_with_key.to_vec(),
621 key.range(),
622 spec,
623 )],
624 )
625 };
626
627 if !matches!(data, JSON::Object(_)) {
628 return not_found();
629 }
630
631 if let Some(child) = data.get(key.as_str()) {
632 tail.apply_to_path(child, vars, &input_path_with_key, spec)
633 } else if tail.is_question() {
634 (None, vec![])
635 } else {
636 not_found()
637 }
638 }
639 }
640 PathList::Expr(expr, tail) => expr
641 .apply_to_path(data, vars, input_path, spec)
642 .and_then_collecting_errors(|value| {
643 tail.apply_to_path(value, vars, input_path, spec)
644 }),
645 PathList::Method(method_name, method_args, tail) => {
646 let method_path =
647 input_path.append(JSON::String(format!("->{}", method_name.as_ref()).into()));
648
649 ArrowMethod::lookup(method_name).map_or_else(
650 || {
651 (
652 None,
653 vec![ApplyToError::new(
654 format!("Method ->{} not found", method_name.as_ref()),
655 method_path.to_vec(),
656 method_name.range(),
657 spec,
658 )],
659 )
660 },
661 |method| {
662 let (result_opt, errors) = method.apply(
663 method_name,
664 method_args.as_ref(),
665 data,
666 vars,
667 &method_path,
668 spec,
669 );
670
671 if let (ArrowMethod::As, Some(JSON::Object(bindings))) =
678 (method, result_opt.as_ref())
679 {
680 let mut updated_vars = vars.clone();
681
682 for (var_name, var_value) in bindings {
683 updated_vars.insert(
684 KnownVariable::Local(var_name.as_str().to_string()),
685 (var_value, InputPath::empty().append(json!(var_name))),
687 );
688 }
689
690 return tail
691 .apply_to_path(data, &updated_vars, &method_path, spec)
694 .prepend_errors(errors);
695 }
696
697 if let Some(result) = result_opt {
698 tail.apply_to_path(&result, vars, &method_path, spec)
699 .prepend_errors(errors)
700 } else {
701 (None, errors)
707 }
708 },
709 )
710 }
711 PathList::Selection(selection) => selection.apply_to_path(data, vars, input_path, spec),
712 PathList::Question(tail) => {
713 if data.is_null() {
715 (None, vec![])
716 } else {
717 tail.apply_to_path(data, vars, input_path, spec)
718 }
719 }
720 PathList::Empty => {
721 (Some(data.clone()), vec![])
724 }
725 }
726 }
727
728 fn compute_output_shape(
729 &self,
730 context: &ShapeContext,
731 input_shape: Shape,
732 dollar_shape: Shape,
733 ) -> Shape {
734 match input_shape.case() {
735 ShapeCase::One(shapes) => {
736 return Shape::one(
737 shapes.iter().map(|shape| {
738 self.compute_output_shape(context, shape.clone(), dollar_shape.clone())
739 }),
740 input_shape.locations().cloned(),
741 );
742 }
743 ShapeCase::All(shapes) => {
744 return Shape::all(
745 shapes.iter().map(|shape| {
746 self.compute_output_shape(context, shape.clone(), dollar_shape.clone())
747 }),
748 input_shape.locations().cloned(),
749 );
750 }
751 ShapeCase::Error(error) => {
752 return match error.partial.as_ref() {
753 Some(partial) => Shape::error_with_partial(
754 error.message.clone(),
755 self.compute_output_shape(context, partial.clone(), dollar_shape),
756 input_shape.locations().cloned(),
757 ),
758 None => input_shape.clone(),
759 };
760 }
761 _ => {}
762 };
763
764 let mut extra_vars_opt: Option<Shape> = None;
768 let (current_shape, tail_opt) = match self.as_ref() {
769 PathList::Var(ranged_var_name, tail) => {
770 let var_name = ranged_var_name.as_ref();
771 let var_shape = if var_name == &KnownVariable::AtSign {
772 input_shape
773 } else if var_name == &KnownVariable::Dollar {
774 dollar_shape.clone()
775 } else if let Some(shape) = context.named_shapes().get(var_name.as_str()) {
776 shape.clone()
777 } else {
778 Shape::name(
779 var_name.as_str(),
780 ranged_var_name.shape_location(context.source_id()),
781 )
782 };
783 (var_shape, Some(tail))
784 }
785
786 PathList::Key(key, tail) => {
791 if input_shape.is_none() {
792 return input_shape;
804 }
805
806 let child_shape = field(&input_shape, key, context.source_id());
807
808 if child_shape.is_none() {
819 return Shape::error(
820 format!(
821 "Property {} not found in {}",
822 key.dotted(),
823 input_shape.pretty_print()
824 ),
825 key.shape_location(context.source_id()),
826 );
827 }
828
829 (child_shape, Some(tail))
830 }
831
832 PathList::Expr(expr, tail) => (
833 expr.compute_output_shape(context, input_shape, dollar_shape.clone()),
834 Some(tail),
835 ),
836
837 PathList::Method(method_name, method_args, tail) => {
838 if input_shape.is_none() {
839 return input_shape;
847 }
848
849 if let Some(method) = ArrowMethod::lookup(method_name) {
850 if context.spec() < ConnectSpec::V0_3 {
855 (
856 Shape::unknown(method_name.shape_location(context.source_id())),
857 None,
858 )
859 } else {
860 let result_shape = method.shape(
861 context,
862 method_name,
863 method_args.as_ref(),
864 input_shape.clone(),
865 dollar_shape.clone(),
866 );
867
868 if method == ArrowMethod::As {
871 extra_vars_opt = Some(result_shape);
876 (
877 input_shape,
881 Some(tail),
882 )
883 } else {
884 (result_shape, Some(tail))
885 }
886 }
887 } else {
888 (
889 Shape::error(
890 format!("Method ->{} not found", method_name.as_str()),
891 method_name.shape_location(context.source_id()),
892 ),
893 None,
894 )
895 }
896 }
897
898 PathList::Question(tail) => {
899 let q_shape = input_shape.question(self.shape_location(context.source_id()));
900 (
901 if tail.is_empty() {
902 q_shape
907 } else {
908 Shape::one([q_shape, Shape::none()], [])
915 },
916 Some(tail),
917 )
918 }
919
920 PathList::Selection(selection) => {
921 if input_shape.is_none() {
922 return input_shape;
928 }
929 (
930 selection.compute_output_shape(context, input_shape, dollar_shape.clone()),
931 None,
932 )
933 }
934
935 PathList::Empty => (input_shape, None),
936 };
937
938 if let Some(tail) = tail_opt {
939 fn compute_tail_shape(
943 tail: &WithRange<PathList>,
944 extra_vars_opt: &Option<Shape>,
945 context: &ShapeContext,
946 input_shape: Shape,
947 dollar_shape: Shape,
948 ) -> Shape {
949 match extra_vars_opt.as_ref().map(|s| s.case()) {
950 Some(ShapeCase::Object { fields, .. }) => {
951 let new_context = context.clone().with_named_shapes(
955 fields
956 .iter()
957 .map(|(name, shape)| (name.clone(), shape.clone())),
958 );
959 tail.compute_output_shape(&new_context, input_shape, dollar_shape)
960 }
961
962 Some(ShapeCase::Error(shape::Error { message, partial })) => {
963 if partial.is_some() {
964 let tail_shape = compute_tail_shape(
965 tail,
966 partial,
967 context,
968 input_shape,
969 dollar_shape,
970 );
971
972 Shape::error_with_partial(
973 message.clone(),
974 tail_shape,
975 tail.shape_location(context.source_id()),
976 )
977 } else {
978 Shape::error(message.clone(), tail.shape_location(context.source_id()))
979 }
980 }
981
982 _ => tail.compute_output_shape(context, input_shape, dollar_shape),
983 }
984 }
985
986 compute_tail_shape(
987 tail,
988 &extra_vars_opt,
989 context,
990 current_shape,
993 dollar_shape,
994 )
995 } else {
996 current_shape
997 }
998 }
999}
1000
1001impl ApplyToInternal for WithRange<LitExpr> {
1002 fn apply_to_path(
1003 &self,
1004 data: &JSON,
1005 vars: &VarsWithPathsMap,
1006 input_path: &InputPath<JSON>,
1007 spec: ConnectSpec,
1008 ) -> (Option<JSON>, Vec<ApplyToError>) {
1009 match self.as_ref() {
1010 LitExpr::String(s) => (Some(JSON::String(s.clone().into())), vec![]),
1011 LitExpr::Number(n) => (Some(JSON::Number(n.clone())), vec![]),
1012 LitExpr::Bool(b) => (Some(JSON::Bool(*b)), vec![]),
1013 LitExpr::Null => (Some(JSON::Null), vec![]),
1014 LitExpr::Object(map) => {
1015 let mut output = JSONMap::with_capacity(map.len());
1016 let mut errors = Vec::new();
1017 for (key, value) in map {
1018 let (value_opt, apply_errors) =
1019 value.apply_to_path(data, vars, input_path, spec);
1020 errors.extend(apply_errors);
1021 if let Some(value_json) = value_opt {
1022 output.insert(key.as_str(), value_json);
1023 }
1024 }
1025 (Some(JSON::Object(output)), errors)
1026 }
1027 LitExpr::Array(vec) => {
1028 let mut output = Vec::with_capacity(vec.len());
1029 let mut errors = Vec::new();
1030 for value in vec {
1031 let (value_opt, apply_errors) =
1032 value.apply_to_path(data, vars, input_path, spec);
1033 errors.extend(apply_errors);
1034 output.push(value_opt.unwrap_or(JSON::Null));
1035 }
1036 (Some(JSON::Array(output)), errors)
1037 }
1038 LitExpr::Path(path) => path.apply_to_path(data, vars, input_path, spec),
1039 LitExpr::LitPath(literal, subpath) => literal
1040 .apply_to_path(data, vars, input_path, spec)
1041 .and_then_collecting_errors(|value| {
1042 subpath.apply_to_path(value, vars, input_path, spec)
1043 }),
1044 LitExpr::OpChain(op, operands) => {
1045 match op.as_ref() {
1046 LitOp::NullishCoalescing => {
1047 let mut accumulated_errors = Vec::new();
1050 let mut last_value: Option<JSON> = None;
1051
1052 for operand in operands {
1053 let (value, errors) =
1054 operand.apply_to_path(data, vars, input_path, spec);
1055
1056 match value {
1057 Some(JSON::Null) | None => {
1059 accumulated_errors.extend(errors);
1061 last_value = value;
1062 continue;
1063 }
1064 Some(value) => {
1065 return (Some(value), errors);
1067 }
1068 }
1069 }
1070
1071 if last_value.is_none() {
1077 (None, accumulated_errors)
1080 } else {
1081 (last_value, Vec::new())
1086 }
1087 }
1088
1089 LitOp::NoneCoalescing => {
1090 let mut accumulated_errors = Vec::new();
1093
1094 for operand in operands {
1095 let (value, errors) =
1096 operand.apply_to_path(data, vars, input_path, spec);
1097
1098 match value {
1099 None => {
1101 accumulated_errors.extend(errors);
1102 continue;
1103 }
1104 Some(value) => {
1106 return (Some(value), errors);
1107 }
1108 }
1109 }
1110
1111 (None, accumulated_errors)
1113 }
1114 }
1115 }
1116 }
1117 }
1118
1119 fn compute_output_shape(
1120 &self,
1121 context: &ShapeContext,
1122 input_shape: Shape,
1123 dollar_shape: Shape,
1124 ) -> Shape {
1125 let locations = self.shape_location(context.source_id());
1126
1127 match self.as_ref() {
1128 LitExpr::Null => Shape::null(locations),
1129 LitExpr::Bool(value) => Shape::bool_value(*value, locations),
1130 LitExpr::String(value) => Shape::string_value(value.as_str(), locations),
1131
1132 LitExpr::Number(value) => {
1133 if let Some(n) = value.as_i64() {
1134 Shape::int_value(n, locations)
1135 } else if value.is_f64() {
1136 Shape::float(locations)
1137 } else {
1138 Shape::error("Number neither Int nor Float", locations)
1139 }
1140 }
1141
1142 LitExpr::Object(map) => {
1143 let mut fields = Shape::empty_map();
1144 for (key, value) in map {
1145 fields.insert(
1146 key.as_string(),
1147 value.compute_output_shape(
1148 context,
1149 input_shape.clone(),
1150 dollar_shape.clone(),
1151 ),
1152 );
1153 }
1154 Shape::object(fields, Shape::none(), locations)
1155 }
1156
1157 LitExpr::Array(vec) => {
1158 let mut shapes = Vec::with_capacity(vec.len());
1159 for value in vec {
1160 shapes.push(value.compute_output_shape(
1161 context,
1162 input_shape.clone(),
1163 dollar_shape.clone(),
1164 ));
1165 }
1166 Shape::array(shapes, Shape::none(), locations)
1167 }
1168
1169 LitExpr::Path(path) => path.compute_output_shape(context, input_shape, dollar_shape),
1170
1171 LitExpr::LitPath(literal, subpath) => {
1172 let literal_shape =
1173 literal.compute_output_shape(context, input_shape, dollar_shape.clone());
1174 subpath.compute_output_shape(context, literal_shape, dollar_shape)
1175 }
1176
1177 LitExpr::OpChain(op, operands) => {
1178 let mut shapes: Vec<Shape> = operands
1179 .iter()
1180 .map(|operand| {
1181 operand.compute_output_shape(
1182 context,
1183 input_shape.clone(),
1184 dollar_shape.clone(),
1185 )
1186 })
1187 .collect();
1188
1189 match op.as_ref() {
1190 LitOp::NullishCoalescing => {
1191 if let Some(last_shape) = shapes.pop() {
1192 let mut new_shapes = shapes
1193 .iter()
1194 .map(|shape| {
1195 shape
1196 .question(locations.clone())
1197 .not_none(locations.clone())
1198 })
1199 .collect::<Vec<_>>();
1200 new_shapes.push(last_shape);
1201 Shape::one(new_shapes, locations)
1202 } else {
1203 Shape::one(shapes, locations)
1204 }
1205 }
1206
1207 LitOp::NoneCoalescing => {
1209 if let Some(last_shape) = shapes.pop() {
1210 let mut new_shapes = shapes
1211 .iter()
1212 .map(|shape| shape.not_none(locations.clone()))
1213 .collect::<Vec<_>>();
1214 new_shapes.push(last_shape);
1215 Shape::one(new_shapes, locations)
1216 } else {
1217 Shape::one(shapes, locations)
1218 }
1219 }
1220 }
1221 }
1222 }
1223 }
1224}
1225
1226impl ApplyToInternal for SubSelection {
1227 fn apply_to_path(
1228 &self,
1229 data: &JSON,
1230 vars: &VarsWithPathsMap,
1231 input_path: &InputPath<JSON>,
1232 spec: ConnectSpec,
1233 ) -> (Option<JSON>, Vec<ApplyToError>) {
1234 if let JSON::Array(array) = data {
1235 return self.apply_to_array(array, vars, input_path, spec);
1236 }
1237
1238 let vars: VarsWithPathsMap = {
1239 let mut vars = vars.clone();
1240 vars.insert(KnownVariable::Dollar, (data, input_path.clone()));
1241 vars
1242 };
1243
1244 let mut output = JSON::Object(JSONMap::new());
1245 let mut errors = Vec::new();
1246
1247 for named_selection in self.selections.iter() {
1248 let (named_output_opt, apply_errors) =
1249 named_selection.apply_to_path(data, &vars, input_path, spec);
1250 errors.extend(apply_errors);
1251
1252 let (merged, merge_errors) = json_merge(Some(&output), named_output_opt.as_ref());
1253
1254 errors.extend(merge_errors.into_iter().map(|message| {
1255 ApplyToError::new(message, input_path.to_vec(), self.range(), spec)
1256 }));
1257
1258 if let Some(merged) = merged {
1259 output = merged;
1260 }
1261 }
1262
1263 if !matches!(data, JSON::Object(_)) {
1264 let output_is_empty = match &output {
1265 JSON::Object(map) => map.is_empty(),
1266 _ => false,
1267 };
1268 if output_is_empty {
1269 return (Some(data.clone()), errors);
1273 }
1274 }
1275
1276 (Some(output), errors)
1277 }
1278
1279 fn compute_output_shape(
1280 &self,
1281 context: &ShapeContext,
1282 input_shape: Shape,
1283 _previous_dollar_shape: Shape,
1284 ) -> Shape {
1285 if let ShapeCase::Array { prefix, tail } = input_shape.case() {
1289 let new_prefix = prefix
1290 .iter()
1291 .map(|shape| self.compute_output_shape(context, shape.clone(), shape.clone()))
1292 .collect::<Vec<_>>();
1293
1294 let new_tail = if tail.is_none() {
1295 tail.clone()
1296 } else {
1297 self.compute_output_shape(context, tail.clone(), tail.clone())
1298 };
1299
1300 return Shape::array(
1301 new_prefix,
1302 new_tail,
1303 self.shape_location(context.source_id()),
1304 );
1305 }
1306
1307 let input_shape = input_shape.any_item(Vec::new());
1311
1312 let dollar_shape = input_shape.clone();
1315
1316 let mut all_shape = Shape::unknown([]);
1319
1320 for named_selection in self.selections.iter() {
1321 all_shape = Shape::all(
1326 [
1327 all_shape,
1328 named_selection.compute_output_shape(
1329 context,
1330 input_shape.clone(),
1331 dollar_shape.clone(),
1332 ),
1333 ],
1334 self.shape_location(context.source_id()),
1335 );
1336
1337 if all_shape.is_null() {
1341 break;
1342 }
1343 }
1344
1345 if all_shape.is_unknown() {
1346 Shape::empty_object(self.shape_location(context.source_id()))
1347 } else {
1348 all_shape
1349 }
1350 }
1351}
1352
1353fn field(shape: &Shape, key: &WithRange<Key>, source_id: &SourceId) -> Shape {
1355 if let ShapeCase::One(inner) = shape.case() {
1356 let mut new_fields = Vec::new();
1357 for inner_field in inner.iter() {
1358 new_fields.push(field(inner_field, key, source_id));
1359 }
1360 return Shape::one(new_fields, shape.locations().cloned());
1361 }
1362 if shape.is_none() || shape.is_null() {
1363 return Shape::none();
1364 }
1365 let field_shape = shape.field(key.as_str(), key.shape_location(source_id));
1366 if field_shape.is_none() {
1367 return Shape::error(
1368 format!("field `{field}` not found", field = key.as_str()),
1369 key.shape_location(source_id),
1370 );
1371 }
1372 field_shape
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use rstest::rstest;
1378
1379 use super::*;
1380 use crate::assert_debug_snapshot;
1381 use crate::connectors::json_selection::PrettyPrintable;
1382 use crate::selection;
1383
1384 #[rstest]
1385 #[case::v0_2(ConnectSpec::V0_2)]
1386 #[case::v0_3(ConnectSpec::V0_3)]
1387 #[case::v0_4(ConnectSpec::V0_4)]
1388 fn test_apply_to_selection(#[case] spec: ConnectSpec) {
1389 let data = json!({
1390 "hello": "world",
1391 "nested": {
1392 "hello": "world",
1393 "world": "hello",
1394 },
1395 "array": [
1396 { "hello": "world 0" },
1397 { "hello": "world 1" },
1398 { "hello": "world 2" },
1399 ],
1400 });
1401
1402 #[track_caller]
1403 fn check_ok(data: &JSON, selection: JSONSelection, expected_json: JSON) {
1404 let (actual_json, errors) = selection.apply_to(data);
1405 assert_eq!(actual_json, Some(expected_json));
1406 assert_eq!(errors, vec![]);
1407 }
1408
1409 check_ok(&data, selection!("hello", spec), json!({"hello": "world"}));
1410
1411 check_ok(
1412 &data,
1413 selection!("nested", spec),
1414 json!({
1415 "nested": {
1416 "hello": "world",
1417 "world": "hello",
1418 },
1419 }),
1420 );
1421
1422 check_ok(&data, selection!("nested.hello", spec), json!("world"));
1423 check_ok(&data, selection!("$.nested.hello", spec), json!("world"));
1424
1425 check_ok(&data, selection!("nested.world", spec), json!("hello"));
1426 check_ok(&data, selection!("$.nested.world", spec), json!("hello"));
1427
1428 check_ok(
1429 &data,
1430 selection!("nested hello", spec),
1431 json!({
1432 "hello": "world",
1433 "nested": {
1434 "hello": "world",
1435 "world": "hello",
1436 },
1437 }),
1438 );
1439
1440 check_ok(
1441 &data,
1442 selection!("array { hello }", spec),
1443 json!({
1444 "array": [
1445 { "hello": "world 0" },
1446 { "hello": "world 1" },
1447 { "hello": "world 2" },
1448 ],
1449 }),
1450 );
1451
1452 check_ok(
1453 &data,
1454 selection!("greetings: array { hello }", spec),
1455 json!({
1456 "greetings": [
1457 { "hello": "world 0" },
1458 { "hello": "world 1" },
1459 { "hello": "world 2" },
1460 ],
1461 }),
1462 );
1463
1464 check_ok(
1465 &data,
1466 selection!("$.array { hello }", spec),
1467 json!([
1468 { "hello": "world 0" },
1469 { "hello": "world 1" },
1470 { "hello": "world 2" },
1471 ]),
1472 );
1473
1474 check_ok(
1475 &data,
1476 selection!("worlds: array.hello", spec),
1477 json!({
1478 "worlds": [
1479 "world 0",
1480 "world 1",
1481 "world 2",
1482 ],
1483 }),
1484 );
1485
1486 check_ok(
1487 &data,
1488 selection!("worlds: $.array.hello", spec),
1489 json!({
1490 "worlds": [
1491 "world 0",
1492 "world 1",
1493 "world 2",
1494 ],
1495 }),
1496 );
1497
1498 check_ok(
1499 &data,
1500 selection!("array.hello", spec),
1501 json!(["world 0", "world 1", "world 2",]),
1502 );
1503
1504 check_ok(
1505 &data,
1506 selection!("$.array.hello", spec),
1507 json!(["world 0", "world 1", "world 2",]),
1508 );
1509
1510 check_ok(
1511 &data,
1512 selection!("nested grouped: { hello worlds: array.hello }", spec),
1513 json!({
1514 "nested": {
1515 "hello": "world",
1516 "world": "hello",
1517 },
1518 "grouped": {
1519 "hello": "world",
1520 "worlds": [
1521 "world 0",
1522 "world 1",
1523 "world 2",
1524 ],
1525 },
1526 }),
1527 );
1528
1529 check_ok(
1530 &data,
1531 selection!("nested grouped: { hello worlds: $.array.hello }", spec),
1532 json!({
1533 "nested": {
1534 "hello": "world",
1535 "world": "hello",
1536 },
1537 "grouped": {
1538 "hello": "world",
1539 "worlds": [
1540 "world 0",
1541 "world 1",
1542 "world 2",
1543 ],
1544 },
1545 }),
1546 );
1547 }
1548
1549 #[rstest]
1550 #[case::v0_2(ConnectSpec::V0_2)]
1551 #[case::v0_3(ConnectSpec::V0_3)]
1552 #[case::v0_4(ConnectSpec::V0_4)]
1553 fn test_apply_to_errors(#[case] spec: ConnectSpec) {
1554 let data = json!({
1555 "hello": "world",
1556 "nested": {
1557 "hello": 123,
1558 "world": true,
1559 },
1560 "array": [
1561 { "hello": 1, "goodbye": "farewell" },
1562 { "hello": "two" },
1563 { "hello": 3.0, "smello": "yellow" },
1564 ],
1565 });
1566
1567 assert_eq!(
1568 selection!("hello", spec).apply_to(&data),
1569 (Some(json!({"hello": "world"})), vec![],)
1570 );
1571
1572 fn make_yellow_errors_expected(
1573 yellow_range: std::ops::Range<usize>,
1574 spec: ConnectSpec,
1575 ) -> Vec<ApplyToError> {
1576 vec![ApplyToError::new(
1577 "Property .yellow not found in object".to_string(),
1578 vec![json!("yellow")],
1579 Some(yellow_range),
1580 spec,
1581 )]
1582 }
1583 assert_eq!(
1584 selection!("yellow", spec).apply_to(&data),
1585 (Some(json!({})), make_yellow_errors_expected(0..6, spec)),
1586 );
1587 assert_eq!(
1588 selection!("$.yellow", spec).apply_to(&data),
1589 (None, make_yellow_errors_expected(2..8, spec)),
1590 );
1591
1592 assert_eq!(
1593 selection!("nested.hello", spec).apply_to(&data),
1594 (Some(json!(123)), vec![],)
1595 );
1596
1597 fn make_quoted_yellow_expected(
1598 yellow_range: std::ops::Range<usize>,
1599 spec: ConnectSpec,
1600 ) -> (Option<JSON>, Vec<ApplyToError>) {
1601 (
1602 None,
1603 vec![ApplyToError::new(
1604 "Property .\"yellow\" not found in object".to_string(),
1605 vec![json!("nested"), json!("yellow")],
1606 Some(yellow_range),
1607 spec,
1608 )],
1609 )
1610 }
1611 assert_eq!(
1612 selection!("nested.'yellow'", spec).apply_to(&data),
1613 make_quoted_yellow_expected(7..15, spec),
1614 );
1615 assert_eq!(
1616 selection!("nested.\"yellow\"", spec).apply_to(&data),
1617 make_quoted_yellow_expected(7..15, spec),
1618 );
1619 assert_eq!(
1620 selection!("$.nested.'yellow'", spec).apply_to(&data),
1621 make_quoted_yellow_expected(9..17, spec),
1622 );
1623
1624 fn make_nested_path_expected(
1625 hola_range: (usize, usize),
1626 yellow_range: (usize, usize),
1627 spec: ConnectSpec,
1628 ) -> (Option<JSON>, Vec<ApplyToError>) {
1629 (
1630 Some(json!({
1631 "world": true,
1632 })),
1633 vec![
1634 ApplyToError::from_json(&json!({
1635 "message": "Property .hola not found in object",
1636 "path": ["nested", "hola"],
1637 "range": hola_range,
1638 "spec": spec.to_string(),
1639 })),
1640 ApplyToError::from_json(&json!({
1641 "message": "Property .yellow not found in object",
1642 "path": ["nested", "yellow"],
1643 "range": yellow_range,
1644 "spec": spec.to_string(),
1645 })),
1646 ],
1647 )
1648 }
1649 assert_eq!(
1650 selection!("$.nested { hola yellow world }", spec).apply_to(&data),
1651 make_nested_path_expected((11, 15), (16, 22), spec),
1652 );
1653 assert_eq!(
1654 selection!(" $ . nested { hola yellow world } ", spec).apply_to(&data),
1655 make_nested_path_expected((14, 18), (19, 25), spec),
1656 );
1657
1658 fn make_partial_array_expected(
1659 goodbye_range: (usize, usize),
1660 spec: ConnectSpec,
1661 ) -> (Option<JSON>, Vec<ApplyToError>) {
1662 (
1663 Some(json!({
1664 "partial": [
1665 { "hello": 1, "goodbye": "farewell" },
1666 { "hello": "two" },
1667 { "hello": 3.0 },
1668 ],
1669 })),
1670 vec![
1671 ApplyToError::from_json(&json!({
1672 "message": "Property .goodbye not found in object",
1673 "path": ["array", 1, "goodbye"],
1674 "range": goodbye_range,
1675 "spec": spec.to_string(),
1676 })),
1677 ApplyToError::from_json(&json!({
1678 "message": "Property .goodbye not found in object",
1679 "path": ["array", 2, "goodbye"],
1680 "range": goodbye_range,
1681 "spec": spec.to_string(),
1682 })),
1683 ],
1684 )
1685 }
1686 assert_eq!(
1687 selection!("partial: $.array { hello goodbye }", spec).apply_to(&data),
1688 make_partial_array_expected((25, 32), spec),
1689 );
1690 assert_eq!(
1691 selection!(" partial : $ . array { hello goodbye } ", spec).apply_to(&data),
1692 make_partial_array_expected((29, 36), spec),
1693 );
1694
1695 assert_eq!(
1696 selection!("good: array.hello bad: array.smello", spec).apply_to(&data),
1697 (
1698 Some(json!({
1699 "good": [
1700 1,
1701 "two",
1702 3.0,
1703 ],
1704 "bad": [
1705 null,
1706 null,
1707 "yellow",
1708 ],
1709 })),
1710 vec![
1711 ApplyToError::from_json(&json!({
1712 "message": "Property .smello not found in object",
1713 "path": ["array", 0, "smello"],
1714 "range": [29, 35],
1715 "spec": spec.to_string(),
1716 })),
1717 ApplyToError::from_json(&json!({
1718 "message": "Property .smello not found in object",
1719 "path": ["array", 1, "smello"],
1720 "range": [29, 35],
1721 "spec": spec.to_string(),
1722 })),
1723 ],
1724 )
1725 );
1726
1727 assert_eq!(
1728 selection!("array { hello smello }", spec).apply_to(&data),
1729 (
1730 Some(json!({
1731 "array": [
1732 { "hello": 1 },
1733 { "hello": "two" },
1734 { "hello": 3.0, "smello": "yellow" },
1735 ],
1736 })),
1737 vec![
1738 ApplyToError::from_json(&json!({
1739 "message": "Property .smello not found in object",
1740 "path": ["array", 0, "smello"],
1741 "range": [14, 20],
1742 "spec": spec.to_string(),
1743 })),
1744 ApplyToError::from_json(&json!({
1745 "message": "Property .smello not found in object",
1746 "path": ["array", 1, "smello"],
1747 "range": [14, 20],
1748 "spec": spec.to_string(),
1749 })),
1750 ],
1751 )
1752 );
1753
1754 assert_eq!(
1755 selection!("$.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1756 (
1757 Some(json!({
1758 "grouped": {
1759 "hello": 123,
1760 "world": true,
1761 },
1762 })),
1763 vec![ApplyToError::from_json(&json!({
1764 "message": "Property .smelly not found in object",
1765 "path": ["nested", "smelly"],
1766 "range": [28, 34],
1767 "spec": spec.to_string(),
1768 }))],
1769 )
1770 );
1771
1772 assert_eq!(
1773 selection!("alias: $.nested { grouped: { hello smelly world } }", spec).apply_to(&data),
1774 (
1775 Some(json!({
1776 "alias": {
1777 "grouped": {
1778 "hello": 123,
1779 "world": true,
1780 },
1781 },
1782 })),
1783 vec![ApplyToError::from_json(&json!({
1784 "message": "Property .smelly not found in object",
1785 "path": ["nested", "smelly"],
1786 "range": [35, 41],
1787 "spec": spec.to_string(),
1788 }))],
1789 )
1790 );
1791 }
1792
1793 #[rstest]
1794 #[case::v0_2(ConnectSpec::V0_2)]
1795 #[case::v0_3(ConnectSpec::V0_3)]
1796 #[case::v0_4(ConnectSpec::V0_4)]
1797 fn test_apply_to_nested_arrays(#[case] spec: ConnectSpec) {
1798 let data = json!({
1799 "arrayOfArrays": [
1800 [
1801 { "x": 0, "y": 0 },
1802 ],
1803 [
1804 { "x": 1, "y": 0 },
1805 { "x": 1, "y": 1 },
1806 { "x": 1, "y": 2 },
1807 ],
1808 [
1809 { "x": 2, "y": 0 },
1810 { "x": 2, "y": 1 },
1811 ],
1812 [],
1813 [
1814 null,
1815 { "x": 4, "y": 1 },
1816 { "x": 4, "why": 2 },
1817 null,
1818 { "x": 4, "y": 4 },
1819 ]
1820 ],
1821 });
1822
1823 fn make_array_of_arrays_x_expected(
1824 x_range: (usize, usize),
1825 spec: ConnectSpec,
1826 ) -> (Option<JSON>, Vec<ApplyToError>) {
1827 (
1828 Some(json!([[0], [1, 1, 1], [2, 2], [], [null, 4, 4, null, 4]])),
1829 vec![
1830 ApplyToError::from_json(&json!({
1831 "message": "Property .x not found in null",
1832 "path": ["arrayOfArrays", 4, 0, "x"],
1833 "range": x_range,
1834 "spec": spec.to_string(),
1835 })),
1836 ApplyToError::from_json(&json!({
1837 "message": "Property .x not found in null",
1838 "path": ["arrayOfArrays", 4, 3, "x"],
1839 "range": x_range,
1840 "spec": spec.to_string(),
1841 })),
1842 ],
1843 )
1844 }
1845 assert_eq!(
1846 selection!("arrayOfArrays.x", spec).apply_to(&data),
1847 make_array_of_arrays_x_expected((14, 15), spec),
1848 );
1849 assert_eq!(
1850 selection!("$.arrayOfArrays.x", spec).apply_to(&data),
1851 make_array_of_arrays_x_expected((16, 17), spec),
1852 );
1853
1854 fn make_array_of_arrays_y_expected(
1855 y_range: (usize, usize),
1856 spec: ConnectSpec,
1857 ) -> (Option<JSON>, Vec<ApplyToError>) {
1858 (
1859 Some(json!([
1860 [0],
1861 [0, 1, 2],
1862 [0, 1],
1863 [],
1864 [null, 1, null, null, 4],
1865 ])),
1866 vec![
1867 ApplyToError::from_json(&json!({
1868 "message": "Property .y not found in null",
1869 "path": ["arrayOfArrays", 4, 0, "y"],
1870 "range": y_range,
1871 "spec": spec.to_string(),
1872 })),
1873 ApplyToError::from_json(&json!({
1874 "message": "Property .y not found in object",
1875 "path": ["arrayOfArrays", 4, 2, "y"],
1876 "range": y_range,
1877 "spec": spec.to_string(),
1878 })),
1879 ApplyToError::from_json(&json!({
1880 "message": "Property .y not found in null",
1881 "path": ["arrayOfArrays", 4, 3, "y"],
1882 "range": y_range,
1883 "spec": spec.to_string(),
1884 })),
1885 ],
1886 )
1887 }
1888 assert_eq!(
1889 selection!("arrayOfArrays.y", spec).apply_to(&data),
1890 make_array_of_arrays_y_expected((14, 15), spec),
1891 );
1892 assert_eq!(
1893 selection!("$.arrayOfArrays.y", spec).apply_to(&data),
1894 make_array_of_arrays_y_expected((16, 17), spec),
1895 );
1896
1897 assert_eq!(
1898 selection!("alias: arrayOfArrays { x y }", spec).apply_to(&data),
1899 (
1900 Some(json!({
1901 "alias": [
1902 [
1903 { "x": 0, "y": 0 },
1904 ],
1905 [
1906 { "x": 1, "y": 0 },
1907 { "x": 1, "y": 1 },
1908 { "x": 1, "y": 2 },
1909 ],
1910 [
1911 { "x": 2, "y": 0 },
1912 { "x": 2, "y": 1 },
1913 ],
1914 [],
1915 [
1916 null,
1917 { "x": 4, "y": 1 },
1918 { "x": 4 },
1919 null,
1920 { "x": 4, "y": 4 },
1921 ]
1922 ],
1923 })),
1924 vec![
1925 ApplyToError::from_json(&json!({
1926 "message": "Property .x not found in null",
1927 "path": ["arrayOfArrays", 4, 0, "x"],
1928 "range": [23, 24],
1929 "spec": spec.to_string(),
1930 })),
1931 ApplyToError::from_json(&json!({
1932 "message": "Property .y not found in null",
1933 "path": ["arrayOfArrays", 4, 0, "y"],
1934 "range": [25, 26],
1935 "spec": spec.to_string(),
1936 })),
1937 ApplyToError::from_json(&json!({
1938 "message": "Property .y not found in object",
1939 "path": ["arrayOfArrays", 4, 2, "y"],
1940 "range": [25, 26],
1941 "spec": spec.to_string(),
1942 })),
1943 ApplyToError::from_json(&json!({
1944 "message": "Property .x not found in null",
1945 "path": ["arrayOfArrays", 4, 3, "x"],
1946 "range": [23, 24],
1947 "spec": spec.to_string(),
1948 })),
1949 ApplyToError::from_json(&json!({
1950 "message": "Property .y not found in null",
1951 "path": ["arrayOfArrays", 4, 3, "y"],
1952 "range": [25, 26],
1953 "spec": spec.to_string(),
1954 })),
1955 ],
1956 ),
1957 );
1958
1959 fn make_array_of_arrays_x_y_expected(
1960 x_range: (usize, usize),
1961 y_range: (usize, usize),
1962 spec: ConnectSpec,
1963 ) -> (Option<JSON>, Vec<ApplyToError>) {
1964 (
1965 Some(json!({
1966 "ys": [
1967 [0],
1968 [0, 1, 2],
1969 [0, 1],
1970 [],
1971 [null, 1, null, null, 4],
1972 ],
1973 "xs": [
1974 [0],
1975 [1, 1, 1],
1976 [2, 2],
1977 [],
1978 [null, 4, 4, null, 4],
1979 ],
1980 })),
1981 vec![
1982 ApplyToError::from_json(&json!({
1983 "message": "Property .y not found in null",
1984 "path": ["arrayOfArrays", 4, 0, "y"],
1985 "range": y_range,
1986 "spec": spec.to_string(),
1987 })),
1988 ApplyToError::from_json(&json!({
1989 "message": "Property .y not found in object",
1990 "path": ["arrayOfArrays", 4, 2, "y"],
1991 "range": y_range,
1992 "spec": spec.to_string(),
1993 })),
1994 ApplyToError::from_json(&json!({
1995 "path": ["arrayOfArrays", 4, 3, "y"],
1998 "message": "Property .y not found in null",
1999 "range": y_range,
2000 "spec": spec.to_string(),
2001 })),
2002 ApplyToError::from_json(&json!({
2003 "message": "Property .x not found in null",
2004 "path": ["arrayOfArrays", 4, 0, "x"],
2005 "range": x_range,
2006 "spec": spec.to_string(),
2007 })),
2008 ApplyToError::from_json(&json!({
2009 "message": "Property .x not found in null",
2010 "path": ["arrayOfArrays", 4, 3, "x"],
2011 "range": x_range,
2012 "spec": spec.to_string(),
2013 })),
2014 ],
2015 )
2016 }
2017 assert_eq!(
2018 selection!("ys: arrayOfArrays.y xs: arrayOfArrays.x", spec).apply_to(&data),
2019 make_array_of_arrays_x_y_expected((38, 39), (18, 19), spec),
2020 );
2021 assert_eq!(
2022 selection!("ys: $.arrayOfArrays.y xs: $.arrayOfArrays.x", spec).apply_to(&data),
2023 make_array_of_arrays_x_y_expected((42, 43), (20, 21), spec),
2024 );
2025 }
2026
2027 #[rstest]
2028 #[case::v0_2(ConnectSpec::V0_2)]
2029 #[case::v0_3(ConnectSpec::V0_3)]
2030 #[case::v0_4(ConnectSpec::V0_4)]
2031 fn test_apply_to_variable_expressions(#[case] spec: ConnectSpec) {
2032 let id_object = selection!("id: $", spec).apply_to(&json!(123));
2033 assert_eq!(id_object, (Some(json!({"id": 123})), vec![]));
2034
2035 let data = json!({
2036 "id": 123,
2037 "name": "Ben",
2038 "friend_ids": [234, 345, 456]
2039 });
2040
2041 assert_eq!(
2042 selection!("id name friends: friend_ids { id: $ }", spec).apply_to(&data),
2043 (
2044 Some(json!({
2045 "id": 123,
2046 "name": "Ben",
2047 "friends": [
2048 { "id": 234 },
2049 { "id": 345 },
2050 { "id": 456 },
2051 ],
2052 })),
2053 vec![],
2054 ),
2055 );
2056
2057 let mut vars = IndexMap::default();
2058 vars.insert("$args".to_string(), json!({ "id": "id from args" }));
2059 assert_eq!(
2060 selection!("id: $args.id name", spec).apply_with_vars(&data, &vars),
2061 (
2062 Some(json!({
2063 "id": "id from args",
2064 "name": "Ben"
2065 })),
2066 vec![],
2067 ),
2068 );
2069 assert_eq!(
2070 selection!("nested.path { id: $args.id name }", spec).apply_to(&json!({
2071 "nested": {
2072 "path": data,
2073 },
2074 })),
2075 (
2076 Some(json!({
2077 "name": "Ben"
2078 })),
2079 vec![ApplyToError::from_json(&json!({
2080 "message": "Variable $args not found",
2081 "path": ["nested", "path"],
2082 "range": [18, 23],
2083 "spec": spec.to_string(),
2084 }))],
2085 ),
2086 );
2087 let mut vars_without_args_id = IndexMap::default();
2088 vars_without_args_id.insert("$args".to_string(), json!({ "unused": "ignored" }));
2089 assert_eq!(
2090 selection!("id: $args.id name", spec).apply_with_vars(&data, &vars_without_args_id),
2091 (
2092 Some(json!({
2093 "name": "Ben"
2094 })),
2095 vec![ApplyToError::from_json(&json!({
2096 "message": "Property .id not found in object",
2097 "path": ["$args", "id"],
2098 "range": [10, 12],
2099 "spec": spec.to_string(),
2100 }))],
2101 ),
2102 );
2103
2104 assert_eq!(
2106 selection!("$args.id", spec).apply_with_vars(&json!([1, 2, 3]), &vars),
2107 (Some(json!("id from args")), vec![]),
2108 );
2109 }
2110
2111 #[test]
2112 fn test_apply_to_variable_expressions_typename() {
2113 let typename_object =
2114 selection!("__typename: $->echo('Product') reviews { __typename: $->echo('Review') }")
2115 .apply_to(&json!({"reviews": [{}]}));
2116 assert_eq!(
2117 typename_object,
2118 (
2119 Some(json!({"__typename": "Product", "reviews": [{ "__typename": "Review" }] })),
2120 vec![]
2121 )
2122 );
2123 }
2124
2125 #[test]
2126 fn test_literal_expressions_in_parentheses() {
2127 assert_eq!(
2128 selection!("__typename: $('Product')").apply_to(&json!({})),
2129 (Some(json!({"__typename": "Product"})), vec![]),
2130 );
2131
2132 assert_eq!(
2133 selection!(" __typename : 'Product' ").apply_to(&json!({})),
2134 (
2135 Some(json!({})),
2136 vec![ApplyToError::new(
2137 "Property .\"Product\" not found in object".to_string(),
2138 vec![json!("Product")],
2139 Some(14..23),
2140 ConnectSpec::latest(),
2141 )],
2142 ),
2143 );
2144
2145 assert_eq!(
2146 selection!(
2147 r#"
2148 one: $(1)
2149 two: $(2)
2150 negativeThree: $(- 3)
2151 true: $(true )
2152 false: $( false)
2153 null: $(null)
2154 string: $("string")
2155 array: $( [ 1 , 2 , 3 ] )
2156 object: $( { "key" : "value" } )
2157 path: $(nested.path)
2158 "#
2159 )
2160 .apply_to(&json!({
2161 "nested": {
2162 "path": "nested path value"
2163 }
2164 })),
2165 (
2166 Some(json!({
2167 "one": 1,
2168 "two": 2,
2169 "negativeThree": -3,
2170 "true": true,
2171 "false": false,
2172 "null": null,
2173 "string": "string",
2174 "array": [1, 2, 3],
2175 "object": { "key": "value" },
2176 "path": "nested path value",
2177 })),
2178 vec![],
2179 ),
2180 );
2181
2182 assert_eq!(
2183 selection!(
2184 r#"
2185 one: $(1)->typeof
2186 two: $(2)->typeof
2187 negativeThree: $(-3)->typeof
2188 true: $(true)->typeof
2189 false: $(false)->typeof
2190 null: $(null)->typeof
2191 string: $("string")->typeof
2192 array: $([1, 2, 3])->typeof
2193 object: $({ "key": "value" })->typeof
2194 path: $(nested.path)->typeof
2195 "#
2196 )
2197 .apply_to(&json!({
2198 "nested": {
2199 "path": 12345
2200 }
2201 })),
2202 (
2203 Some(json!({
2204 "one": "number",
2205 "two": "number",
2206 "negativeThree": "number",
2207 "true": "boolean",
2208 "false": "boolean",
2209 "null": "null",
2210 "string": "string",
2211 "array": "array",
2212 "object": "object",
2213 "path": "number",
2214 })),
2215 vec![],
2216 ),
2217 );
2218
2219 assert_eq!(
2220 selection!(
2221 r#"
2222 items: $([
2223 1,
2224 -2.0,
2225 true,
2226 false,
2227 null,
2228 "string",
2229 [1, 2, 3],
2230 { "key": "value" },
2231 nested.path,
2232 ])->map(@->typeof)
2233 "#
2234 )
2235 .apply_to(&json!({
2236 "nested": {
2237 "path": { "deeply": "nested" }
2238 }
2239 })),
2240 (
2241 Some(json!({
2242 "items": [
2243 "number",
2244 "number",
2245 "boolean",
2246 "boolean",
2247 "null",
2248 "string",
2249 "array",
2250 "object",
2251 "object",
2252 ],
2253 })),
2254 vec![],
2255 ),
2256 );
2257
2258 assert_eq!(
2259 selection!(
2260 r#"
2261 $({
2262 one: 1,
2263 two: 2,
2264 negativeThree: -3,
2265 true: true,
2266 false: false,
2267 null: null,
2268 string: "string",
2269 array: [1, 2, 3],
2270 object: { "key": "value" },
2271 path: $ . nested . path ,
2272 })->entries
2273 "#
2274 )
2275 .apply_to(&json!({
2276 "nested": {
2277 "path": "nested path value"
2278 }
2279 })),
2280 (
2281 Some(json!([
2282 { "key": "one", "value": 1 },
2283 { "key": "two", "value": 2 },
2284 { "key": "negativeThree", "value": -3 },
2285 { "key": "true", "value": true },
2286 { "key": "false", "value": false },
2287 { "key": "null", "value": null },
2288 { "key": "string", "value": "string" },
2289 { "key": "array", "value": [1, 2, 3] },
2290 { "key": "object", "value": { "key": "value" } },
2291 { "key": "path", "value": "nested path value" },
2292 ])),
2293 vec![],
2294 ),
2295 );
2296
2297 assert_eq!(
2298 selection!(
2299 r#"
2300 $({
2301 string: $("string")->slice(1, 4),
2302 array: $([1, 2, 3])->map(@->add(10)),
2303 object: $({ "key": "value" })->get("key"),
2304 path: nested.path->slice($("nested ")->size),
2305 needlessParens: $("oyez"),
2306 withoutParens: "oyez",
2307 })
2308 "#
2309 )
2310 .apply_to(&json!({
2311 "nested": {
2312 "path": "nested path value"
2313 }
2314 })),
2315 (
2316 Some(json!({
2317 "string": "tri",
2318 "array": [11, 12, 13],
2319 "object": "value",
2320 "path": "path value",
2321 "needlessParens": "oyez",
2322 "withoutParens": "oyez",
2323 })),
2324 vec![],
2325 ),
2326 );
2327
2328 assert_eq!(
2329 selection!(
2330 r#"
2331 string: $("string")->slice(1, 4)
2332 array: $([1, 2, 3])->map(@->add(10))
2333 object: $({ "key": "value" })->get("key")
2334 path: nested.path->slice($("nested ")->size)
2335 "#
2336 )
2337 .apply_to(&json!({
2338 "nested": {
2339 "path": "nested path value"
2340 }
2341 })),
2342 (
2343 Some(json!({
2344 "string": "tri",
2345 "array": [11, 12, 13],
2346 "object": "value",
2347 "path": "path value",
2348 })),
2349 vec![],
2350 ),
2351 );
2352 }
2353
2354 #[rstest]
2355 #[case::v0_2(ConnectSpec::V0_2)]
2356 #[case::v0_3(ConnectSpec::V0_3)]
2357 #[case::v0_4(ConnectSpec::V0_4)]
2358 fn test_inline_paths_with_subselections(#[case] spec: ConnectSpec) {
2359 let data = json!({
2360 "id": 123,
2361 "created": "2021-01-01T00:00:00Z",
2362 "model": "gpt-4o",
2363 "choices": [{
2364 "index": 0,
2365 "message": {
2366 "role": "assistant",
2367 "content": "The capital of Australia is Canberra.",
2368 },
2369 }, {
2370 "index": 1,
2371 "message": {
2372 "role": "assistant",
2373 "content": "The capital of Australia is Sydney.",
2374 },
2375 }],
2376 });
2377
2378 {
2379 let expected = (
2380 Some(json!({
2381 "id": 123,
2382 "created": "2021-01-01T00:00:00Z",
2383 "model": "gpt-4o",
2384 "role": "assistant",
2385 "content": "The capital of Australia is Canberra.",
2386 })),
2387 vec![],
2388 );
2389
2390 assert_eq!(
2391 selection!(
2392 r#"
2393 id
2394 created
2395 model
2396 role: choices->first.message.role
2397 content: choices->first.message.content
2398 "#,
2399 spec
2400 )
2401 .apply_to(&data),
2402 expected,
2403 );
2404
2405 assert_eq!(
2406 selection!(
2407 r#"
2408 id
2409 created
2410 model
2411 choices->first.message {
2412 role
2413 content
2414 }
2415 "#,
2416 spec
2417 )
2418 .apply_to(&data),
2419 expected,
2420 );
2421
2422 assert_eq!(
2423 selection!(
2424 r#"
2425 id
2426 choices->first.message {
2427 role
2428 content
2429 }
2430 created
2431 model
2432 "#,
2433 spec
2434 )
2435 .apply_to(&data),
2436 expected,
2437 );
2438 }
2439
2440 {
2441 let expected = (
2442 Some(json!({
2443 "id": 123,
2444 "created": "2021-01-01T00:00:00Z",
2445 "model": "gpt-4o",
2446 "role": "assistant",
2447 "message": "The capital of Australia is Sydney.",
2448 })),
2449 vec![],
2450 );
2451
2452 assert_eq!(
2453 selection!(
2454 r#"
2455 id
2456 created
2457 model
2458 role: choices->last.message.role
2459 message: choices->last.message.content
2460 "#,
2461 spec
2462 )
2463 .apply_to(&data),
2464 expected,
2465 );
2466
2467 assert_eq!(
2468 selection!(
2469 r#"
2470 id
2471 created
2472 model
2473 choices->last.message {
2474 role
2475 message: content
2476 }
2477 "#,
2478 spec
2479 )
2480 .apply_to(&data),
2481 expected,
2482 );
2483
2484 assert_eq!(
2485 selection!(
2486 r#"
2487 created
2488 choices->last.message {
2489 message: content
2490 role
2491 }
2492 model
2493 id
2494 "#,
2495 spec
2496 )
2497 .apply_to(&data),
2498 expected,
2499 );
2500 }
2501
2502 {
2503 let expected = (
2504 Some(json!({
2505 "id": 123,
2506 "created": "2021-01-01T00:00:00Z",
2507 "model": "gpt-4o",
2508 "role": "assistant",
2509 "correct": "The capital of Australia is Canberra.",
2510 "incorrect": "The capital of Australia is Sydney.",
2511 })),
2512 vec![],
2513 );
2514
2515 assert_eq!(
2516 selection!(
2517 r#"
2518 id
2519 created
2520 model
2521 role: choices->first.message.role
2522 correct: choices->first.message.content
2523 incorrect: choices->last.message.content
2524 "#,
2525 spec
2526 )
2527 .apply_to(&data),
2528 expected,
2529 );
2530
2531 assert_eq!(
2532 selection!(
2533 r#"
2534 id
2535 created
2536 model
2537 choices->first.message {
2538 role
2539 correct: content
2540 }
2541 choices->last.message {
2542 incorrect: content
2543 }
2544 "#,
2545 spec
2546 )
2547 .apply_to(&data),
2548 expected,
2549 );
2550
2551 assert_eq!(
2552 selection!(
2553 r#"
2554 id
2555 created
2556 model
2557 choices->first.message {
2558 role
2559 correct: content
2560 }
2561 incorrect: choices->last.message.content
2562 "#,
2563 spec
2564 )
2565 .apply_to(&data),
2566 expected,
2567 );
2568
2569 assert_eq!(
2570 selection!(
2571 r#"
2572 id
2573 created
2574 model
2575 choices->first.message {
2576 correct: content
2577 }
2578 choices->last.message {
2579 role
2580 incorrect: content
2581 }
2582 "#,
2583 spec
2584 )
2585 .apply_to(&data),
2586 expected,
2587 );
2588
2589 assert_eq!(
2590 selection!(
2591 r#"
2592 id
2593 created
2594 correct: choices->first.message.content
2595 choices->last.message {
2596 role
2597 incorrect: content
2598 }
2599 model
2600 "#,
2601 spec
2602 )
2603 .apply_to(&data),
2604 expected,
2605 );
2606 }
2607
2608 {
2609 let data = json!({
2610 "from": "data",
2611 });
2612
2613 let vars = {
2614 let mut vars = IndexMap::default();
2615 vars.insert(
2616 "$this".to_string(),
2617 json!({
2618 "id": 1234,
2619 }),
2620 );
2621 vars.insert(
2622 "$args".to_string(),
2623 json!({
2624 "input": {
2625 "title": "The capital of Australia",
2626 "body": "Canberra",
2627 },
2628 "extra": "extra",
2629 }),
2630 );
2631 vars
2632 };
2633
2634 let expected = (
2635 Some(json!({
2636 "id": 1234,
2637 "title": "The capital of Australia",
2638 "body": "Canberra",
2639 "from": "data",
2640 })),
2641 vec![],
2642 );
2643
2644 assert_eq!(
2645 selection!(
2646 r#"
2647 id: $this.id
2648 $args.input {
2649 title
2650 body
2651 }
2652 from
2653 "#,
2654 spec
2655 )
2656 .apply_with_vars(&data, &vars),
2657 expected,
2658 );
2659
2660 assert_eq!(
2661 selection!(
2662 r#"
2663 from
2664 $args.input { title body }
2665 id: $this.id
2666 "#,
2667 spec
2668 )
2669 .apply_with_vars(&data, &vars),
2670 expected,
2671 );
2672
2673 assert_eq!(
2674 selection!(
2675 r#"
2676 $args.input { body title }
2677 from
2678 id: $this.id
2679 "#,
2680 spec
2681 )
2682 .apply_with_vars(&data, &vars),
2683 expected,
2684 );
2685
2686 assert_eq!(
2687 selection!(
2688 r#"
2689 id: $this.id
2690 $args { $.input { title body } }
2691 from
2692 "#,
2693 spec
2694 )
2695 .apply_with_vars(&data, &vars),
2696 expected,
2697 );
2698
2699 assert_eq!(
2700 selection!(
2701 r#"
2702 id: $this.id
2703 $args { $.input { title body } extra }
2704 from: $.from
2705 "#,
2706 spec
2707 )
2708 .apply_with_vars(&data, &vars),
2709 (
2710 Some(json!({
2711 "id": 1234,
2712 "title": "The capital of Australia",
2713 "body": "Canberra",
2714 "extra": "extra",
2715 "from": "data",
2716 })),
2717 vec![],
2718 ),
2719 );
2720
2721 assert_eq!(
2722 selection!(
2723 r#"
2724 # Equivalent to id: $this.id
2725 $this { id }
2726
2727 $args {
2728 __typename: $("Args")
2729
2730 # Requiring $. instead of just . prevents .input from
2731 # parsing as a key applied to the $("Args") string.
2732 $.input { title body }
2733
2734 extra
2735 }
2736
2737 from: $.from
2738 "#,
2739 spec
2740 )
2741 .apply_with_vars(&data, &vars),
2742 (
2743 Some(json!({
2744 "id": 1234,
2745 "title": "The capital of Australia",
2746 "body": "Canberra",
2747 "__typename": "Args",
2748 "extra": "extra",
2749 "from": "data",
2750 })),
2751 vec![],
2752 ),
2753 );
2754 }
2755 }
2756
2757 #[test]
2758 fn test_inline_path_errors() {
2759 {
2760 let data = json!({
2761 "id": 123,
2762 "created": "2021-01-01T00:00:00Z",
2763 "model": "gpt-4o",
2764 "choices": [{
2765 "message": "The capital of Australia is Canberra.",
2766 }, {
2767 "message": "The capital of Australia is Sydney.",
2768 }],
2769 });
2770
2771 let expected = (
2772 Some(json!({
2773 "id": 123,
2774 "created": "2021-01-01T00:00:00Z",
2775 "model": "gpt-4o",
2776 })),
2777 vec![
2778 ApplyToError::new(
2779 "Property .role not found in string".to_string(),
2780 vec![
2781 json!("choices"),
2782 json!("->first"),
2783 json!("message"),
2784 json!("role"),
2785 ],
2786 Some(123..127),
2787 ConnectSpec::latest(),
2788 ),
2789 ApplyToError::new(
2790 "Property .content not found in string".to_string(),
2791 vec![
2792 json!("choices"),
2793 json!("->first"),
2794 json!("message"),
2795 json!("content"),
2796 ],
2797 Some(128..135),
2798 ConnectSpec::latest(),
2799 ),
2800 ApplyToError::new(
2801 "Expected object or null, not string".to_string(),
2802 vec![],
2803 Some(98..137),
2807 ConnectSpec::latest(),
2808 ),
2809 ],
2810 );
2811
2812 assert_eq!(
2813 selection!(
2814 r#"
2815 id
2816 created
2817 model
2818 choices->first.message { role content }
2819 "#
2820 )
2821 .apply_to(&data),
2822 expected,
2823 );
2824 }
2825
2826 assert_eq!(
2827 selection!("id nested.path.nonexistent { name }").apply_to(&json!({
2828 "id": 2345,
2829 "nested": {
2830 "path": "nested path value",
2831 },
2832 })),
2833 (
2834 Some(json!({
2835 "id": 2345,
2836 })),
2837 vec![
2838 ApplyToError::new(
2839 "Property .nonexistent not found in string".to_string(),
2840 vec![json!("nested"), json!("path"), json!("nonexistent")],
2841 Some(15..26),
2842 ConnectSpec::latest(),
2843 ),
2844 ApplyToError::new(
2845 "Inlined path produced no value".to_string(),
2846 vec![],
2847 Some(3..35),
2850 ConnectSpec::latest(),
2851 ),
2852 ],
2853 ),
2854 );
2855
2856 let valid_inline_path_selection = JSONSelection::named(SubSelection {
2857 selections: vec![NamedSelection {
2858 prefix: NamingPrefix::None,
2859 path: PathSelection {
2860 path: PathList::Key(
2861 Key::field("some").into_with_range(),
2862 PathList::Key(
2863 Key::field("object").into_with_range(),
2864 PathList::Empty.into_with_range(),
2865 )
2866 .into_with_range(),
2867 )
2868 .into_with_range(),
2869 },
2870 }],
2871 ..Default::default()
2872 });
2873
2874 assert_eq!(
2875 valid_inline_path_selection.apply_to(&json!({
2876 "some": {
2877 "object": {
2878 "key": "value",
2879 },
2880 },
2881 })),
2882 (
2883 Some(json!({
2884 "key": "value",
2885 })),
2886 vec![],
2887 ),
2888 );
2889 }
2890
2891 #[test]
2892 fn test_apply_to_non_identifier_properties() {
2893 let data = json!({
2894 "not an identifier": [
2895 { "also.not.an.identifier": 0 },
2896 { "also.not.an.identifier": 1 },
2897 { "also.not.an.identifier": 2 },
2898 ],
2899 "another": {
2900 "pesky string literal!": {
2901 "identifier": 123,
2902 "{ evil braces }": true,
2903 },
2904 },
2905 });
2906
2907 assert_eq!(
2908 selection!("alias: 'not an identifier' { safe: 'also.not.an.identifier' }")
2912 .apply_to(&data),
2913 (
2914 Some(json!({
2915 "alias": [
2916 { "safe": 0 },
2917 { "safe": 1 },
2918 { "safe": 2 },
2919 ],
2920 })),
2921 vec![],
2922 ),
2923 );
2924
2925 assert_eq!(
2926 selection!("'not an identifier'.'also.not.an.identifier'").apply_to(&data),
2927 (Some(json!([0, 1, 2])), vec![],),
2928 );
2929
2930 assert_eq!(
2931 selection!("$.'not an identifier'.'also.not.an.identifier'").apply_to(&data),
2932 (Some(json!([0, 1, 2])), vec![],),
2933 );
2934
2935 assert_eq!(
2936 selection!("$.\"not an identifier\" { safe: \"also.not.an.identifier\" }")
2937 .apply_to(&data),
2938 (
2939 Some(json!([
2940 { "safe": 0 },
2941 { "safe": 1 },
2942 { "safe": 2 },
2943 ])),
2944 vec![],
2945 ),
2946 );
2947
2948 assert_eq!(
2949 selection!(
2950 "another {
2951 pesky: 'pesky string literal!' {
2952 identifier
2953 evil: '{ evil braces }'
2954 }
2955 }"
2956 )
2957 .apply_to(&data),
2958 (
2959 Some(json!({
2960 "another": {
2961 "pesky": {
2962 "identifier": 123,
2963 "evil": true,
2964 },
2965 },
2966 })),
2967 vec![],
2968 ),
2969 );
2970
2971 assert_eq!(
2972 selection!("another.'pesky string literal!'.'{ evil braces }'").apply_to(&data),
2973 (Some(json!(true)), vec![],),
2974 );
2975
2976 assert_eq!(
2977 selection!("another.'pesky string literal!'.\"identifier\"").apply_to(&data),
2978 (Some(json!(123)), vec![],),
2979 );
2980
2981 assert_eq!(
2982 selection!("$.another.'pesky string literal!'.\"identifier\"").apply_to(&data),
2983 (Some(json!(123)), vec![],),
2984 );
2985 }
2986
2987 #[rstest]
2988 #[case::latest(ConnectSpec::V0_2)]
2989 #[case::next(ConnectSpec::V0_3)]
2990 #[case::v0_4(ConnectSpec::V0_4)]
2991 fn test_left_associative_path_evaluation(#[case] spec: ConnectSpec) {
2992 assert_eq!(
2993 selection!("batch.id->first", spec).apply_to(&json!({
2994 "batch": [
2995 { "id": 1 },
2996 { "id": 2 },
2997 { "id": 3 },
2998 ],
2999 })),
3000 (Some(json!(1)), vec![]),
3001 );
3002
3003 assert_eq!(
3004 selection!("batch.id->last", spec).apply_to(&json!({
3005 "batch": [
3006 { "id": 1 },
3007 { "id": 2 },
3008 { "id": 3 },
3009 ],
3010 })),
3011 (Some(json!(3)), vec![]),
3012 );
3013
3014 assert_eq!(
3015 selection!("batch.id->size", spec).apply_to(&json!({
3016 "batch": [
3017 { "id": 1 },
3018 { "id": 2 },
3019 { "id": 3 },
3020 ],
3021 })),
3022 (Some(json!(3)), vec![]),
3023 );
3024
3025 assert_eq!(
3026 selection!("batch.id->slice(1)->first", spec).apply_to(&json!({
3027 "batch": [
3028 { "id": 1 },
3029 { "id": 2 },
3030 { "id": 3 },
3031 ],
3032 })),
3033 (Some(json!(2)), vec![]),
3034 );
3035
3036 assert_eq!(
3037 selection!("batch.id->map({ batchId: @ })", spec).apply_to(&json!({
3038 "batch": [
3039 { "id": 1 },
3040 { "id": 2 },
3041 { "id": 3 },
3042 ],
3043 })),
3044 (
3045 Some(json!([
3046 { "batchId": 1 },
3047 { "batchId": 2 },
3048 { "batchId": 3 },
3049 ])),
3050 vec![],
3051 ),
3052 );
3053
3054 let mut vars = IndexMap::default();
3055 vars.insert(
3056 "$batch".to_string(),
3057 json!([
3058 { "id": 4 },
3059 { "id": 5 },
3060 { "id": 6 },
3061 ]),
3062 );
3063 assert_eq!(
3064 selection!("$batch.id->map({ batchId: @ })", spec).apply_with_vars(
3065 &json!({
3066 "batch": "ignored",
3067 }),
3068 &vars
3069 ),
3070 (
3071 Some(json!([
3072 { "batchId": 4 },
3073 { "batchId": 5 },
3074 { "batchId": 6 },
3075 ])),
3076 vec![],
3077 ),
3078 );
3079
3080 assert_eq!(
3081 selection!("batch.id->map({ batchId: @ })->first", spec).apply_to(&json!({
3082 "batch": [
3083 { "id": 7 },
3084 { "id": 8 },
3085 { "id": 9 },
3086 ],
3087 })),
3088 (Some(json!({ "batchId": 7 })), vec![]),
3089 );
3090
3091 assert_eq!(
3092 selection!("batch.id->map({ batchId: @ })->last", spec).apply_to(&json!({
3093 "batch": [
3094 { "id": 7 },
3095 { "id": 8 },
3096 { "id": 9 },
3097 ],
3098 })),
3099 (Some(json!({ "batchId": 9 })), vec![]),
3100 );
3101
3102 assert_eq!(
3103 selection!("$batch.id->map({ batchId: @ })->first", spec).apply_with_vars(
3104 &json!({
3105 "batch": "ignored",
3106 }),
3107 &vars
3108 ),
3109 (Some(json!({ "batchId": 4 })), vec![]),
3110 );
3111
3112 assert_eq!(
3113 selection!("$batch.id->map({ batchId: @ })->last", spec).apply_with_vars(
3114 &json!({
3115 "batch": "ignored",
3116 }),
3117 &vars
3118 ),
3119 (Some(json!({ "batchId": 6 })), vec![]),
3120 );
3121
3122 assert_eq!(
3123 selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3124 "arrays": [
3125 { "as": { "bs": [10, 20, 30] } },
3126 { "as": { "bs": [40, 50, 60] } },
3127 { "as": { "bs": [70, 80, 90] } },
3128 ],
3129 })),
3130 (
3131 Some(json!({
3132 "echoed": [
3133 [10, 20, 30],
3134 [40, 50, 60],
3135 [70, 80, 90],
3136 ],
3137 })),
3138 vec![],
3139 ),
3140 );
3141
3142 assert_eq!(
3143 selection!("arrays.as.bs->echo({ echoed: @ })", spec).apply_to(&json!({
3144 "arrays": [
3145 { "as": { "bs": [10, 20, 30] } },
3146 { "as": [
3147 { "bs": [40, 50, 60] },
3148 { "bs": [70, 80, 90] },
3149 ] },
3150 { "as": { "bs": [100, 110, 120] } },
3151 ],
3152 })),
3153 (
3154 Some(json!({
3155 "echoed": [
3156 [10, 20, 30],
3157 [
3158 [40, 50, 60],
3159 [70, 80, 90],
3160 ],
3161 [100, 110, 120],
3162 ],
3163 })),
3164 vec![],
3165 ),
3166 );
3167
3168 assert_eq!(
3169 selection!("batch.id->jsonStringify", spec).apply_to(&json!({
3170 "batch": [
3171 { "id": 1 },
3172 { "id": 2 },
3173 { "id": 3 },
3174 ],
3175 })),
3176 (Some(json!("[1,2,3]")), vec![]),
3177 );
3178
3179 assert_eq!(
3180 selection!("batch.id->map([@])->echo([@])->jsonStringify", spec).apply_to(&json!({
3181 "batch": [
3182 { "id": 1 },
3183 { "id": 2 },
3184 { "id": 3 },
3185 ],
3186 })),
3187 (Some(json!("[[[1],[2],[3]]]")), vec![]),
3188 );
3189
3190 assert_eq!(
3191 selection!("batch.id->map([@])->echo([@])->jsonStringify->typeof", spec).apply_to(
3192 &json!({
3193 "batch": [
3194 { "id": 1 },
3195 { "id": 2 },
3196 { "id": 3 },
3197 ],
3198 })
3199 ),
3200 (Some(json!("string")), vec![]),
3201 );
3202 }
3203
3204 #[test]
3205 fn test_left_associative_output_shapes_v0_2() {
3206 let spec = ConnectSpec::V0_2;
3207
3208 assert_eq!(
3209 selection!("$batch.id", spec).shape().pretty_print(),
3210 "$batch.id"
3211 );
3212
3213 assert_eq!(
3214 selection!("$batch.id->first", spec).shape().pretty_print(),
3215 "Unknown",
3216 );
3217
3218 assert_eq!(
3219 selection!("$batch.id->last", spec).shape().pretty_print(),
3220 "Unknown",
3221 );
3222
3223 let mut named_shapes = IndexMap::default();
3224 named_shapes.insert(
3225 "$batch".to_string(),
3226 Shape::list(
3227 Shape::record(
3228 {
3229 let mut map = Shape::empty_map();
3230 map.insert("id".to_string(), Shape::int([]));
3231 map
3232 },
3233 [],
3234 ),
3235 [],
3236 ),
3237 );
3238
3239 let root_shape = Shape::name("$root", []);
3240 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3241 .with_spec(spec)
3242 .with_named_shapes(named_shapes);
3243
3244 let computed_batch_id =
3245 selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3246 assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3247
3248 let computed_first = selection!("$batch.id->first", spec)
3249 .compute_output_shape(&shape_context, root_shape.clone());
3250 assert_eq!(computed_first.pretty_print(), "Unknown");
3251
3252 let computed_last = selection!("$batch.id->last", spec)
3253 .compute_output_shape(&shape_context, root_shape.clone());
3254 assert_eq!(computed_last.pretty_print(), "Unknown");
3255
3256 assert_eq!(
3257 selection!("$batch.id->jsonStringify", spec)
3258 .shape()
3259 .pretty_print(),
3260 "Unknown",
3261 );
3262
3263 assert_eq!(
3264 selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3265 .shape()
3266 .pretty_print(),
3267 "Unknown",
3268 );
3269
3270 assert_eq!(
3271 selection!("$batch.id->map(@)->echo(@)", spec)
3272 .shape()
3273 .pretty_print(),
3274 "Unknown",
3275 );
3276
3277 assert_eq!(
3278 selection!("$batch.id->map(@)->echo([@])", spec)
3279 .shape()
3280 .pretty_print(),
3281 "Unknown",
3282 );
3283
3284 assert_eq!(
3285 selection!("$batch.id->map([@])->echo(@)", spec)
3286 .shape()
3287 .pretty_print(),
3288 "Unknown",
3289 );
3290
3291 assert_eq!(
3292 selection!("$batch.id->map([@])->echo([@])", spec)
3293 .shape()
3294 .pretty_print(),
3295 "Unknown",
3296 );
3297
3298 assert_eq!(
3299 selection!("$batch.id->map([@])->echo([@])", spec)
3300 .compute_output_shape(&shape_context, root_shape,)
3301 .pretty_print(),
3302 "Unknown",
3303 );
3304 }
3305
3306 #[test]
3307 fn test_left_associative_output_shapes_v0_3() {
3308 let spec = ConnectSpec::V0_3;
3309
3310 assert_eq!(
3311 selection!("$batch.id", spec).shape().pretty_print(),
3312 "$batch.id"
3313 );
3314
3315 assert_eq!(
3316 selection!("$batch.id->first", spec).shape().pretty_print(),
3317 "$batch.id.0",
3318 );
3319
3320 assert_eq!(
3321 selection!("$batch.id->last", spec).shape().pretty_print(),
3322 "$batch.id.*",
3323 );
3324
3325 let mut named_shapes = IndexMap::default();
3326 named_shapes.insert(
3327 "$batch".to_string(),
3328 Shape::list(
3329 Shape::record(
3330 {
3331 let mut map = Shape::empty_map();
3332 map.insert("id".to_string(), Shape::int([]));
3333 map
3334 },
3335 [],
3336 ),
3337 [],
3338 ),
3339 );
3340
3341 let root_shape = Shape::name("$root", []);
3342 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3343 .with_spec(spec)
3344 .with_named_shapes(named_shapes.clone());
3345
3346 let computed_batch_id =
3347 selection!("$batch.id", spec).compute_output_shape(&shape_context, root_shape.clone());
3348 assert_eq!(computed_batch_id.pretty_print(), "List<Int>");
3349
3350 let computed_first = selection!("$batch.id->first", spec)
3351 .compute_output_shape(&shape_context, root_shape.clone());
3352 assert_eq!(computed_first.pretty_print(), "One<Int, None>");
3353
3354 let computed_last = selection!("$batch.id->last", spec)
3355 .compute_output_shape(&shape_context, root_shape.clone());
3356 assert_eq!(computed_last.pretty_print(), "One<Int, None>");
3357
3358 assert_eq!(
3359 selection!("$batch.id->jsonStringify", spec)
3360 .shape()
3361 .pretty_print(),
3362 "String",
3363 );
3364
3365 assert_eq!(
3366 selection!("$batch.id->map([@])->echo([@])->jsonStringify", spec)
3367 .shape()
3368 .pretty_print(),
3369 "String",
3370 );
3371
3372 assert_eq!(
3373 selection!("$batch.id->map(@)->echo(@)", spec)
3374 .shape()
3375 .pretty_print(),
3376 "List<$batch.id.*>",
3377 );
3378
3379 assert_eq!(
3380 selection!("$batch.id->map(@)->echo([@])", spec)
3381 .shape()
3382 .pretty_print(),
3383 "[List<$batch.id.*>]",
3384 );
3385
3386 assert_eq!(
3387 selection!("$batch.id->map([@])->echo(@)", spec)
3388 .shape()
3389 .pretty_print(),
3390 "List<[$batch.id.*]>",
3391 );
3392
3393 assert_eq!(
3394 selection!("$batch.id->map([@])->echo([@])", spec)
3395 .shape()
3396 .pretty_print(),
3397 "[List<[$batch.id.*]>]",
3398 );
3399
3400 assert_eq!(
3401 selection!("$batch.id->map([@])->echo([@])", spec)
3402 .compute_output_shape(&shape_context, root_shape,)
3403 .pretty_print(),
3404 "[List<[Int]>]",
3405 );
3406 }
3407
3408 #[test]
3409 fn test_lit_paths() {
3410 let data = json!({
3411 "value": {
3412 "key": 123,
3413 },
3414 });
3415
3416 assert_eq!(
3417 selection!("$(\"a\")->first").apply_to(&data),
3418 (Some(json!("a")), vec![]),
3419 );
3420
3421 assert_eq!(
3422 selection!("$('asdf'->last)").apply_to(&data),
3423 (Some(json!("f")), vec![]),
3424 );
3425
3426 assert_eq!(
3427 selection!("$(1234)->add(1111)").apply_to(&data),
3428 (Some(json!(2345)), vec![]),
3429 );
3430
3431 assert_eq!(
3432 selection!("$(1234->add(1111))").apply_to(&data),
3433 (Some(json!(2345)), vec![]),
3434 );
3435
3436 assert_eq!(
3437 selection!("$(value.key->mul(10))").apply_to(&data),
3438 (Some(json!(1230)), vec![]),
3439 );
3440
3441 assert_eq!(
3442 selection!("$(value.key)->mul(10)").apply_to(&data),
3443 (Some(json!(1230)), vec![]),
3444 );
3445
3446 assert_eq!(
3447 selection!("$(value.key->typeof)").apply_to(&data),
3448 (Some(json!("number")), vec![]),
3449 );
3450
3451 assert_eq!(
3452 selection!("$(value.key)->typeof").apply_to(&data),
3453 (Some(json!("number")), vec![]),
3454 );
3455
3456 assert_eq!(
3457 selection!("$([1, 2, 3])->last").apply_to(&data),
3458 (Some(json!(3)), vec![]),
3459 );
3460
3461 assert_eq!(
3462 selection!("$([1, 2, 3]->first)").apply_to(&data),
3463 (Some(json!(1)), vec![]),
3464 );
3465
3466 assert_eq!(
3467 selection!("$({ a: 'ay', b: 1 }).a").apply_to(&data),
3468 (Some(json!("ay")), vec![]),
3469 );
3470
3471 assert_eq!(
3472 selection!("$({ a: 'ay', b: 2 }.a)").apply_to(&data),
3473 (Some(json!("ay")), vec![]),
3474 );
3475
3476 assert_eq!(
3477 selection!("$(-1->add(10))").apply_to(&data),
3481 (Some(json!(9)), vec![]),
3482 );
3483 }
3484
3485 #[test]
3486 fn test_compute_output_shape() {
3487 let spec = ConnectSpec::V0_3;
3488
3489 assert_eq!(selection!("", spec).shape().pretty_print(), "{}");
3490
3491 assert_eq!(
3492 selection!("id name", spec).shape().pretty_print(),
3493 "{ id: $root.*.id, name: $root.*.name }",
3494 );
3495
3496 assert_eq!(
3497 selection!("$.data { thisOrThat: $(maybe.this ?? maybe.that) }", spec)
3498 .shape()
3499 .pretty_print(),
3500 "{ thisOrThat: One<$root.data.*.maybe.this?!, $root.data.*.maybe.that> }",
3501 );
3502
3503 assert_eq!(
3504 selection!(
3505 r#"
3506 id
3507 name
3508 friends: friend_ids { id: @ }
3509 alias: arrayOfArrays { x y }
3510 ys: arrayOfArrays.y xs: arrayOfArrays.x
3511 "#,
3512 spec
3513 )
3514 .shape()
3515 .pretty_print(),
3516 r#"{
3523 alias: {
3524 x: $root.*.arrayOfArrays.*.x,
3525 y: $root.*.arrayOfArrays.*.y,
3526 },
3527 friends: { id: $root.*.friend_ids.* },
3528 id: $root.*.id,
3529 name: $root.*.name,
3530 xs: $root.*.arrayOfArrays.x,
3531 ys: $root.*.arrayOfArrays.y,
3532}"#,
3533 );
3534
3535 assert_eq!(
3536 selection!(
3537 r#"
3538 id
3539 name
3540 friends: friend_ids->map({ id: @ })
3541 alias: arrayOfArrays { x y }
3542 ys: arrayOfArrays.y xs: arrayOfArrays.x
3543 "#,
3544 spec
3545 )
3546 .shape()
3547 .pretty_print(),
3548 r#"{
3549 alias: {
3550 x: $root.*.arrayOfArrays.*.x,
3551 y: $root.*.arrayOfArrays.*.y,
3552 },
3553 friends: List<{ id: $root.*.friend_ids.* }>,
3554 id: $root.*.id,
3555 name: $root.*.name,
3556 xs: $root.*.arrayOfArrays.x,
3557 ys: $root.*.arrayOfArrays.y,
3558}"#,
3559 );
3560
3561 assert_eq!(
3562 selection!("$->echo({ thrice: [@, @, @] })", spec)
3563 .shape()
3564 .pretty_print(),
3565 "{ thrice: [$root, $root, $root] }",
3566 );
3567
3568 assert_eq!(
3569 selection!("$->echo({ thrice: [@, @, @] })->entries", spec)
3570 .shape()
3571 .pretty_print(),
3572 "[{ key: \"thrice\", value: [$root, $root, $root] }]",
3573 );
3574
3575 assert_eq!(
3576 selection!("$->echo({ thrice: [@, @, @] })->entries.key", spec)
3577 .shape()
3578 .pretty_print(),
3579 "[\"thrice\"]",
3580 );
3581
3582 assert_eq!(
3583 selection!("$->echo({ thrice: [@, @, @] })->entries.value", spec)
3584 .shape()
3585 .pretty_print(),
3586 "[[$root, $root, $root]]",
3587 );
3588
3589 assert_eq!(
3590 selection!("$->echo({ wrapped: @ })->entries { k: key v: value }", spec)
3591 .shape()
3592 .pretty_print(),
3593 "[{ k: \"wrapped\", v: $root }]",
3594 );
3595 }
3596
3597 #[rstest]
3598 #[case::v0_3(ConnectSpec::V0_3)]
3599 #[case::v0_4(ConnectSpec::V0_4)]
3600 fn test_optional_key_access_with_existing_property(#[case] spec: ConnectSpec) {
3601 use serde_json_bytes::json;
3602
3603 let data = json!({
3604 "user": {
3605 "profile": {
3606 "name": "Alice"
3607 }
3608 }
3609 });
3610
3611 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3612 .unwrap()
3613 .apply_to(&data);
3614 assert!(errors.is_empty());
3615 assert_eq!(result, Some(json!("Alice")));
3616 }
3617
3618 #[rstest]
3619 #[case::v0_3(ConnectSpec::V0_3)]
3620 #[case::v0_4(ConnectSpec::V0_4)]
3621 fn test_optional_key_access_with_null_value(#[case] spec: ConnectSpec) {
3622 use serde_json_bytes::json;
3623
3624 let data_null = json!({
3625 "user": null
3626 });
3627
3628 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3629 .unwrap()
3630 .apply_to(&data_null);
3631 assert!(errors.is_empty());
3632 assert_eq!(result, None);
3633 }
3634
3635 #[rstest]
3636 #[case::v0_3(ConnectSpec::V0_3)]
3637 #[case::v0_4(ConnectSpec::V0_4)]
3638 fn test_optional_key_access_on_non_object(#[case] spec: ConnectSpec) {
3639 use serde_json_bytes::json;
3640
3641 let data_non_obj = json!({
3642 "user": "not an object"
3643 });
3644
3645 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3646 .unwrap()
3647 .apply_to(&data_non_obj);
3648 assert_eq!(errors.len(), 1);
3649 assert!(
3650 errors[0]
3651 .message()
3652 .contains("Property .profile not found in string")
3653 );
3654 assert_eq!(result, None);
3655 }
3656
3657 #[rstest]
3658 #[case::v0_3(ConnectSpec::V0_3)]
3659 #[case::v0_4(ConnectSpec::V0_4)]
3660 fn test_optional_key_access_with_missing_property(#[case] spec: ConnectSpec) {
3661 use serde_json_bytes::json;
3662
3663 let data = json!({
3664 "user": {
3665 "other": "value"
3666 }
3667 });
3668
3669 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile.name", spec)
3670 .unwrap()
3671 .apply_to(&data);
3672 assert_eq!(errors.len(), 1);
3673 assert!(
3674 errors[0]
3675 .message()
3676 .contains("Property .profile not found in object")
3677 );
3678 assert_eq!(result, None);
3679 }
3680
3681 #[rstest]
3682 #[case::v0_3(ConnectSpec::V0_3)]
3683 #[case::v0_4(ConnectSpec::V0_4)]
3684 fn test_chained_optional_key_access(#[case] spec: ConnectSpec) {
3685 use serde_json_bytes::json;
3686
3687 let data = json!({
3688 "user": {
3689 "profile": {
3690 "name": "Alice"
3691 }
3692 }
3693 });
3694
3695 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
3696 .unwrap()
3697 .apply_to(&data);
3698 assert!(errors.is_empty());
3699 assert_eq!(result, Some(json!("Alice")));
3700 }
3701
3702 #[rstest]
3703 #[case::v0_3(ConnectSpec::V0_3)]
3704 #[case::v0_4(ConnectSpec::V0_4)]
3705 fn test_chained_optional_access_with_null_in_middle(#[case] spec: ConnectSpec) {
3706 use serde_json_bytes::json;
3707
3708 let data_partial_null = json!({
3709 "user": {
3710 "profile": null
3711 }
3712 });
3713
3714 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile?.name", spec)
3715 .unwrap()
3716 .apply_to(&data_partial_null);
3717 assert!(errors.is_empty());
3718 assert_eq!(result, None);
3719 }
3720
3721 #[rstest]
3722 #[case::v0_3(ConnectSpec::V0_3)]
3723 #[case::v0_4(ConnectSpec::V0_4)]
3724 fn test_optional_method_on_null(#[case] spec: ConnectSpec) {
3725 use serde_json_bytes::json;
3726
3727 let data = json!({
3728 "items": null
3729 });
3730
3731 let (result, errors) = JSONSelection::parse_with_spec("$.items?->first", spec)
3732 .unwrap()
3733 .apply_to(&data);
3734 assert!(errors.is_empty());
3735 assert_eq!(result, None);
3736 }
3737
3738 #[rstest]
3739 #[case::v0_3(ConnectSpec::V0_3)]
3740 #[case::v0_4(ConnectSpec::V0_4)]
3741 fn test_optional_method_with_valid_method(#[case] spec: ConnectSpec) {
3742 use serde_json_bytes::json;
3743
3744 let data = json!({
3745 "values": [1, 2, 3]
3746 });
3747
3748 let (result, errors) = JSONSelection::parse_with_spec("$.values?->first", spec)
3749 .unwrap()
3750 .apply_to(&data);
3751 assert!(errors.is_empty());
3752 assert_eq!(result, Some(json!(1)));
3753 }
3754
3755 #[rstest]
3756 #[case::v0_3(ConnectSpec::V0_3)]
3757 #[case::v0_4(ConnectSpec::V0_4)]
3758 fn test_optional_method_with_unknown_method(#[case] spec: ConnectSpec) {
3759 use serde_json_bytes::json;
3760
3761 let data = json!({
3762 "values": [1, 2, 3]
3763 });
3764
3765 let (result, errors) = JSONSelection::parse_with_spec("$.values?->length", spec)
3766 .unwrap()
3767 .apply_to(&data);
3768 assert_eq!(errors.len(), 1);
3769 assert!(errors[0].message().contains("Method ->length not found"));
3770 assert_eq!(result, None);
3771 }
3772
3773 #[rstest]
3774 #[case::v0_3(ConnectSpec::V0_3)]
3775 #[case::v0_4(ConnectSpec::V0_4)]
3776 fn test_optional_chaining_with_subselection_on_valid_data(#[case] spec: ConnectSpec) {
3777 use serde_json_bytes::json;
3778
3779 let data = json!({
3780 "user": {
3781 "profile": {
3782 "name": "Alice",
3783 "age": 30,
3784 "email": "alice@example.com"
3785 }
3786 }
3787 });
3788
3789 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
3790 .unwrap()
3791 .apply_to(&data);
3792 assert!(errors.is_empty());
3793 assert_eq!(
3794 result,
3795 Some(json!({
3796 "name": "Alice",
3797 "age": 30
3798 }))
3799 );
3800 }
3801
3802 #[rstest]
3803 #[case::v0_3(ConnectSpec::V0_3)]
3804 #[case::v0_4(ConnectSpec::V0_4)]
3805 fn test_optional_chaining_with_subselection_on_null_data(#[case] spec: ConnectSpec) {
3806 use serde_json_bytes::json;
3807
3808 let data_null = json!({
3809 "user": null
3810 });
3811
3812 let (result, errors) = JSONSelection::parse_with_spec("$.user?.profile { name age }", spec)
3813 .unwrap()
3814 .apply_to(&data_null);
3815 assert!(errors.is_empty());
3816 assert_eq!(result, None);
3817 }
3818
3819 #[rstest]
3820 #[case::v0_3(ConnectSpec::V0_3)]
3821 #[case::v0_4(ConnectSpec::V0_4)]
3822 fn test_mixed_regular_and_optional_chaining_working_case(#[case] spec: ConnectSpec) {
3823 use serde_json_bytes::json;
3824
3825 let data = json!({
3826 "response": {
3827 "data": {
3828 "user": {
3829 "profile": {
3830 "name": "Bob"
3831 }
3832 }
3833 }
3834 }
3835 });
3836
3837 let (result, errors) =
3838 JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
3839 .unwrap()
3840 .apply_to(&data);
3841 assert!(errors.is_empty());
3842 assert_eq!(result, Some(json!("Bob")));
3843 }
3844
3845 #[rstest]
3846 #[case::v0_3(ConnectSpec::V0_3)]
3847 #[case::v0_4(ConnectSpec::V0_4)]
3848 fn test_mixed_regular_and_optional_chaining_with_null(#[case] spec: ConnectSpec) {
3849 use serde_json_bytes::json;
3850
3851 let data_null_data = json!({
3852 "response": {
3853 "data": null
3854 }
3855 });
3856
3857 let (result, errors) =
3858 JSONSelection::parse_with_spec("$.response.data?.user.profile.name", spec)
3859 .unwrap()
3860 .apply_to(&data_null_data);
3861 assert!(errors.is_empty());
3862 assert_eq!(result, None);
3863 }
3864
3865 #[rstest]
3866 #[case::v0_3(ConnectSpec::V0_3)]
3867 #[case::v0_4(ConnectSpec::V0_4)]
3868 fn test_optional_selection_set_with_valid_data(#[case] spec: ConnectSpec) {
3869 use serde_json_bytes::json;
3870
3871 let data = json!({
3872 "user": {
3873 "id": 123,
3874 "name": "Alice"
3875 }
3876 });
3877
3878 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3879 .unwrap()
3880 .apply_to(&data);
3881 assert_eq!(
3882 result,
3883 Some(json!({
3884 "id": 123,
3885 "name": "Alice"
3886 }))
3887 );
3888 assert_eq!(errors, vec![]);
3889 }
3890
3891 #[rstest]
3892 #[case::v0_3(ConnectSpec::V0_3)]
3893 #[case::v0_4(ConnectSpec::V0_4)]
3894 fn test_optional_selection_set_with_null_data(#[case] spec: ConnectSpec) {
3895 use serde_json_bytes::json;
3896
3897 let data = json!({
3898 "user": null
3899 });
3900
3901 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3902 .unwrap()
3903 .apply_to(&data);
3904 assert_eq!(result, None);
3905 assert_eq!(errors, vec![]);
3906 }
3907
3908 #[rstest]
3909 #[case::v0_3(ConnectSpec::V0_3)]
3910 #[case::v0_4(ConnectSpec::V0_4)]
3911 fn test_optional_selection_set_with_missing_property(#[case] spec: ConnectSpec) {
3912 use serde_json_bytes::json;
3913
3914 let data = json!({
3915 "other": "value"
3916 });
3917
3918 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3919 .unwrap()
3920 .apply_to(&data);
3921 assert_eq!(result, None);
3922 assert_eq!(errors.len(), 0);
3923 }
3924
3925 #[rstest]
3926 #[case::v0_3(ConnectSpec::V0_3)]
3927 #[case::v0_4(ConnectSpec::V0_4)]
3928 fn test_optional_selection_set_with_non_object(#[case] spec: ConnectSpec) {
3929 use serde_json_bytes::json;
3930
3931 let data = json!({
3932 "user": "not an object"
3933 });
3934
3935 let (result, errors) = JSONSelection::parse_with_spec("$.user ?{ id name }", spec)
3936 .unwrap()
3937 .apply_to(&data);
3938 assert_eq!(result, Some(json!("not an object")));
3941 assert_eq!(errors.len(), 2);
3942 assert!(
3943 errors[0]
3944 .message()
3945 .contains("Property .id not found in string")
3946 );
3947 assert!(
3948 errors[1]
3949 .message()
3950 .contains("Property .name not found in string")
3951 );
3952 }
3953
3954 #[test]
3955 fn test_optional_field_selections() {
3956 let spec = ConnectSpec::V0_3;
3957 let author_selection = selection!("author? { age middleName? }", spec);
3958 assert_debug_snapshot!(author_selection);
3959 assert_eq!(
3960 author_selection.pretty_print(),
3961 "author? { age middleName? }",
3962 );
3963 assert_eq!(
3964 author_selection.shape().pretty_print(),
3965 r#"{ author: One<
3966 {
3967 age: $root.*.author?.*.age,
3968 middleName: $root.*.author?.*.middleName?,
3969 },
3970 None,
3971 > }"#,
3972 );
3973 }
3974
3975 #[test]
3976 fn test_optional_input_shape_with_selection() {
3977 let spec = ConnectSpec::V0_3;
3978 let optional_author_shape_selection =
3979 selection!("unreliableAuthor { age middleName? }", spec);
3980
3981 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
3982 .with_spec(spec)
3983 .with_named_shapes([(
3984 "$root".to_string(),
3985 Shape::record(
3986 {
3987 let mut map = Shape::empty_map();
3988 map.insert(
3989 "unreliableAuthor".to_string(),
3990 Shape::one(
3991 [
3992 Shape::record(
3993 {
3994 let mut map = Shape::empty_map();
3995 map.insert("age".to_string(), Shape::int([]));
3996 map.insert(
3997 "middleName".to_string(),
3998 Shape::one([Shape::string([]), Shape::none()], []),
3999 );
4000 map
4001 },
4002 [],
4003 ),
4004 Shape::none(),
4005 ],
4006 [],
4007 ),
4008 );
4009 map
4010 },
4011 [],
4012 ),
4013 )]);
4014
4015 assert_eq!(
4016 optional_author_shape_selection
4017 .compute_output_shape(
4018 &shape_context,
4019 shape_context.named_shapes().get("$root").unwrap().clone(),
4020 )
4021 .pretty_print(),
4022 "{ unreliableAuthor: One<{ age: Int, middleName: One<String, None> }, None> }",
4023 );
4024 }
4025
4026 #[rstest]
4092 #[case::v0_3(ConnectSpec::V0_3)]
4093 #[case::v0_4(ConnectSpec::V0_4)]
4094 fn test_nested_optional_selection_sets(#[case] spec: ConnectSpec) {
4095 use serde_json_bytes::json;
4096
4097 let data = json!({
4098 "user": {
4099 "profile": {
4100 "name": "Alice",
4101 "email": "alice@example.com"
4102 }
4103 }
4104 });
4105
4106 let (result, errors) =
4107 JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4108 .unwrap()
4109 .apply_to(&data);
4110 assert_eq!(
4111 result,
4112 Some(json!({
4113 "name": "Alice",
4114 "email": "alice@example.com"
4115 }))
4116 );
4117 assert_eq!(errors, vec![]);
4118
4119 let data_with_null_profile = json!({
4121 "user": {
4122 "profile": null
4123 }
4124 });
4125
4126 let (result, errors) =
4127 JSONSelection::parse_with_spec("$.user.profile ?{ name email }", spec)
4128 .unwrap()
4129 .apply_to(&data_with_null_profile);
4130 assert_eq!(result, None);
4131 assert_eq!(errors, vec![]);
4132 }
4133
4134 #[rstest]
4135 #[case::v0_3(ConnectSpec::V0_3)]
4136 #[case::v0_4(ConnectSpec::V0_4)]
4137 fn test_mixed_optional_selection_and_optional_chaining(#[case] spec: ConnectSpec) {
4138 use serde_json_bytes::json;
4139
4140 let data = json!({
4141 "user": {
4142 "id": 123,
4143 "profile": null
4144 }
4145 });
4146
4147 let (result, errors) =
4148 JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4149 .unwrap()
4150 .apply_to(&data);
4151 assert_eq!(
4152 result,
4153 Some(json!({
4154 "id": 123
4155 }))
4156 );
4157 assert_eq!(errors, vec![]);
4158
4159 let data_no_user = json!({
4161 "other": "value"
4162 });
4163
4164 let (result, errors) =
4165 JSONSelection::parse_with_spec("$.user ?{ id profileName: profile?.name }", spec)
4166 .unwrap()
4167 .apply_to(&data_no_user);
4168 assert_eq!(result, None);
4169 assert_eq!(errors.len(), 0);
4170 }
4171
4172 #[rstest]
4173 #[case::v0_3(ConnectSpec::V0_3)]
4174 #[case::v0_4(ConnectSpec::V0_4)]
4175 fn test_optional_selection_set_parsing(#[case] spec: ConnectSpec) {
4176 let selection = JSONSelection::parse_with_spec("$.user? { id name }", spec).unwrap();
4178 assert_eq!(selection.pretty_print(), "$.user? { id name }");
4179
4180 let selection = JSONSelection::parse_with_spec("$.user.profile? { name }", spec).unwrap();
4182 assert_eq!(selection.pretty_print(), "$.user.profile? { name }");
4183
4184 let selection =
4186 JSONSelection::parse_with_spec("$.user? { id profile { name } }", spec).unwrap();
4187 assert_eq!(selection.pretty_print(), "$.user? { id profile { name } }");
4188 }
4189
4190 #[rstest]
4191 #[case::v0_3(ConnectSpec::V0_3)]
4192 #[case::v0_4(ConnectSpec::V0_4)]
4193 fn test_optional_selection_set_with_arrays(#[case] spec: ConnectSpec) {
4194 use serde_json_bytes::json;
4195
4196 let data = json!({
4197 "users": [
4198 {
4199 "id": 1,
4200 "name": "Alice"
4201 },
4202 null,
4203 {
4204 "id": 3,
4205 "name": "Charlie"
4206 }
4207 ]
4208 });
4209
4210 let (result, errors) = JSONSelection::parse_with_spec("$.users ?{ id name }", spec)
4211 .unwrap()
4212 .apply_to(&data);
4213 assert_eq!(
4214 result,
4215 Some(json!([
4216 {
4217 "id": 1,
4218 "name": "Alice"
4219 },
4220 null,
4221 {
4222 "id": 3,
4223 "name": "Charlie"
4224 }
4225 ]))
4226 );
4227
4228 assert_eq!(errors.len(), 2);
4229 assert!(
4230 errors[0]
4231 .message()
4232 .contains("Property .id not found in null")
4233 );
4234 assert!(
4235 errors[1]
4236 .message()
4237 .contains("Property .name not found in null")
4238 );
4239 }
4240
4241 #[test]
4772 fn null_coalescing_should_return_left_when_left_not_null() {
4773 let spec = ConnectSpec::V0_3;
4774 assert_eq!(
4775 selection!("$('Foo' ?? 'Bar')", spec).apply_to(&json!({})),
4776 (Some(json!("Foo")), vec![]),
4777 );
4778 }
4779
4780 #[test]
4781 fn null_coalescing_should_return_right_when_left_is_null() {
4782 let spec = ConnectSpec::V0_3;
4783 assert_eq!(
4784 selection!("$(null ?? 'Bar')", spec).apply_to(&json!({})),
4785 (Some(json!("Bar")), vec![]),
4786 );
4787 }
4788
4789 #[test]
4790 fn none_coalescing_should_return_left_when_left_not_none() {
4791 let spec = ConnectSpec::V0_3;
4792 assert_eq!(
4793 selection!("$('Foo' ?! 'Bar')", spec).apply_to(&json!({})),
4794 (Some(json!("Foo")), vec![]),
4795 );
4796 }
4797
4798 #[test]
4799 fn none_coalescing_should_preserve_null_when_left_is_null() {
4800 let spec = ConnectSpec::V0_3;
4801 assert_eq!(
4802 selection!("$(null ?! 'Bar')", spec).apply_to(&json!({})),
4803 (Some(json!(null)), vec![]),
4804 );
4805 }
4806
4807 #[test]
4808 fn nullish_coalescing_should_return_final_null() {
4809 let spec = ConnectSpec::V0_3;
4810 assert_eq!(
4811 selection!("$(missing ?? null)", spec).apply_to(&json!({})),
4812 (Some(json!(null)), vec![]),
4813 );
4814 assert_eq!(
4815 selection!("$(missing ?! null)", spec).apply_to(&json!({})),
4816 (Some(json!(null)), vec![]),
4817 );
4818 }
4819
4820 #[test]
4821 fn nullish_coalescing_should_return_final_none() {
4822 let spec = ConnectSpec::V0_3;
4823 assert_eq!(
4824 selection!("$(missing ?? also_missing)", spec).apply_to(&json!({})),
4825 (
4826 None,
4827 vec![
4828 ApplyToError::new(
4829 "Property .missing not found in object".to_string(),
4830 vec![json!("missing")],
4831 Some(2..9),
4832 spec,
4833 ),
4834 ApplyToError::new(
4835 "Property .also_missing not found in object".to_string(),
4836 vec![json!("also_missing")],
4837 Some(13..25),
4838 spec,
4839 ),
4840 ]
4841 ),
4842 );
4843 assert_eq!(
4844 selection!("maybe: $(missing ?! also_missing)", spec).apply_to(&json!({})),
4845 (
4846 Some(json!({})),
4847 vec![
4848 ApplyToError::new(
4849 "Property .missing not found in object".to_string(),
4850 vec![json!("missing")],
4851 Some(9..16),
4852 spec,
4853 ),
4854 ApplyToError::new(
4855 "Property .also_missing not found in object".to_string(),
4856 vec![json!("also_missing")],
4857 Some(20..32),
4858 spec,
4859 ),
4860 ]
4861 ),
4862 );
4863 }
4864
4865 #[test]
4866 fn coalescing_operators_should_return_earlier_values_if_later_missing() {
4867 let spec = ConnectSpec::V0_3;
4868 assert_eq!(
4869 selection!("$(1234 ?? missing)", spec).apply_to(&json!({})),
4870 (Some(json!(1234)), vec![]),
4871 );
4872 assert_eq!(
4873 selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": 1234 })),
4874 (Some(json!(1234)), vec![]),
4875 );
4876 assert_eq!(
4877 selection!("$(item ?? missing)", spec).apply_to(&json!({ "item": null })),
4878 (
4879 None,
4880 vec![ApplyToError::new(
4881 "Property .missing not found in object".to_string(),
4882 vec![json!("missing")],
4883 Some(10..17),
4884 spec,
4885 )]
4886 ),
4887 );
4888 assert_eq!(
4889 selection!("$(null ?! missing)", spec).apply_to(&json!({})),
4890 (Some(json!(null)), vec![]),
4891 );
4892 assert_eq!(
4893 selection!("$(item ?! missing)", spec).apply_to(&json!({ "item": null })),
4894 (Some(json!(null)), vec![]),
4895 );
4896 }
4897
4898 #[test]
4899 fn null_coalescing_should_chain_left_to_right_when_multiple_nulls() {
4900 let spec = ConnectSpec::V0_3;
4902 assert_eq!(
4903 selection!("$(null ?? null ?? 'Bar')", spec).apply_to(&json!({})),
4904 (Some(json!("Bar")), vec![]),
4905 );
4906 }
4907
4908 #[test]
4909 fn null_coalescing_should_stop_at_first_non_null_when_chaining() {
4910 let spec = ConnectSpec::V0_3;
4911 assert_eq!(
4912 selection!("$('Foo' ?? null ?? 'Bar')", spec).apply_to(&json!({})),
4913 (Some(json!("Foo")), vec![]),
4914 );
4915 }
4916
4917 #[test]
4918 fn null_coalescing_should_fallback_when_field_is_null() {
4919 let spec = ConnectSpec::V0_3;
4920 let data = json!({"field1": null, "field2": "value2"});
4921 assert_eq!(
4922 selection!("$($.field1 ?? $.field2)", spec).apply_to(&data),
4923 (Some(json!("value2")), vec![]),
4924 );
4925 }
4926
4927 #[test]
4928 fn null_coalescing_should_use_literal_fallback_when_all_fields_null() {
4929 let spec = ConnectSpec::V0_3;
4930 let data = json!({"field1": null, "field3": null});
4931 assert_eq!(
4932 selection!("$($.field1 ?? $.field3 ?? 'fallback')", spec).apply_to(&data),
4933 (Some(json!("fallback")), vec![]),
4934 );
4935 }
4936
4937 #[test]
4938 fn none_coalescing_should_preserve_null_field() {
4939 let spec = ConnectSpec::V0_3;
4940 let data = json!({"nullField": null});
4941 assert_eq!(
4942 selection!("$($.nullField ?! 'fallback')", spec).apply_to(&data),
4943 (Some(json!(null)), vec![]),
4944 );
4945 }
4946
4947 #[test]
4948 fn none_coalescing_should_replace_missing_field() {
4949 let spec = ConnectSpec::V0_3;
4950 let data = json!({"nullField": null});
4951 assert_eq!(
4952 selection!("$($.missingField ?! 'fallback')", spec).apply_to(&data),
4953 (Some(json!("fallback")), vec![]),
4954 );
4955 }
4956
4957 #[test]
4958 fn null_coalescing_should_replace_null_field() {
4959 let spec = ConnectSpec::V0_3;
4960 let data = json!({"nullField": null});
4961 assert_eq!(
4962 selection!("$($.nullField ?? 'fallback')", spec).apply_to(&data),
4963 (Some(json!("fallback")), vec![]),
4964 );
4965 }
4966
4967 #[test]
4968 fn null_coalescing_should_replace_missing_field() {
4969 let spec = ConnectSpec::V0_3;
4970 let data = json!({"nullField": null});
4971 assert_eq!(
4972 selection!("$($.missingField ?? 'fallback')", spec).apply_to(&data),
4973 (Some(json!("fallback")), vec![]),
4974 );
4975 }
4976
4977 #[test]
4978 fn null_coalescing_should_preserve_number_type() {
4979 let spec = ConnectSpec::V0_3;
4980 assert_eq!(
4981 selection!("$(null ?? 42)", spec).apply_to(&json!({})),
4982 (Some(json!(42)), vec![]),
4983 );
4984 }
4985
4986 #[test]
4987 fn null_coalescing_should_preserve_boolean_type() {
4988 let spec = ConnectSpec::V0_3;
4989 assert_eq!(
4990 selection!("$(null ?? true)", spec).apply_to(&json!({})),
4991 (Some(json!(true)), vec![]),
4992 );
4993 }
4994
4995 #[test]
4996 fn null_coalescing_should_preserve_object_type() {
4997 let spec = ConnectSpec::V0_3;
4998 assert_eq!(
4999 selection!("$(null ?? {'key': 'value'})", spec).apply_to(&json!({})),
5000 (Some(json!({"key": "value"})), vec![]),
5001 );
5002 }
5003
5004 #[test]
5005 fn null_coalescing_should_preserve_array_type() {
5006 let spec = ConnectSpec::V0_3;
5007 assert_eq!(
5008 selection!("$(null ?? [1, 2, 3])", spec).apply_to(&json!({})),
5009 (Some(json!([1, 2, 3])), vec![]),
5010 );
5011 }
5012
5013 #[test]
5014 fn null_coalescing_should_fallback_when_null_used_as_method_arg() {
5015 let spec = ConnectSpec::V0_3;
5016 assert_eq!(
5017 selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": null, "c": 5})),
5018 (Some(json!(10)), vec![]),
5019 );
5020 }
5021
5022 #[test]
5023 fn null_coalescing_should_fallback_when_none_used_as_method_arg() {
5024 let spec = ConnectSpec::V0_3;
5025 assert_eq!(
5026 selection!("$.a->add(missing ?? c)", spec)
5027 .apply_to(&json!({"a": 5, "b": null, "c": 5})),
5028 (Some(json!(10)), vec![]),
5029 );
5030 }
5031
5032 #[test]
5033 fn null_coalescing_should_not_fallback_when_not_null_used_as_method_arg() {
5034 let spec = ConnectSpec::V0_3;
5035 assert_eq!(
5036 selection!("$.a->add(b ?? c)", spec).apply_to(&json!({"a": 5, "b": 3, "c": 5})),
5037 (Some(json!(8)), vec![]),
5038 );
5039 }
5040
5041 #[test]
5042 fn null_coalescing_should_allow_multiple_method_args() {
5043 let spec = ConnectSpec::V0_3;
5044 let add_selection = selection!("a->add(b ?? c, missing ?! c)", spec);
5045 assert_eq!(
5046 add_selection.apply_to(&json!({ "a": 5, "b": 3, "c": 7 })),
5047 (Some(json!(15)), vec![]),
5048 );
5049 assert_eq!(
5050 add_selection.apply_to(&json!({ "a": 5, "b": null, "c": 7 })),
5051 (Some(json!(19)), vec![]),
5052 );
5053 }
5054
5055 #[test]
5141 fn wtf_operator_should_not_exclude_null_from_nullable_union_shape() {
5142 let spec = ConnectSpec::V0_3;
5143
5144 let nullish_selection = selection!("$($value ?? 'fallback')", spec);
5146 let wtf_selection = selection!("$($value ?! 'fallback')", spec);
5147
5148 let mut vars = IndexMap::default();
5149 vars.insert("$value".to_string(), json!(null));
5150
5151 assert_eq!(
5152 nullish_selection.apply_with_vars(&json!({}), &vars),
5153 (Some(json!("fallback")), vec![]),
5154 );
5155
5156 assert_eq!(
5157 wtf_selection.apply_with_vars(&json!({}), &vars),
5158 (Some(json!(null)), vec![]),
5159 );
5160
5161 let mut vars_with_string_value = IndexMap::default();
5162 vars_with_string_value.insert("$value".to_string(), json!("fine"));
5163
5164 assert_eq!(
5165 nullish_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5166 (Some(json!("fine")), vec![]),
5167 );
5168
5169 assert_eq!(
5170 wtf_selection.apply_with_vars(&json!({}), &vars_with_string_value),
5171 (Some(json!("fine")), vec![]),
5172 );
5173
5174 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5175 .with_spec(spec)
5176 .with_named_shapes([(
5177 "$value".to_string(),
5178 Shape::one([Shape::string([]), Shape::null([]), Shape::none()], []),
5179 )]);
5180
5181 assert_eq!(
5182 nullish_selection
5183 .compute_output_shape(&shape_context, Shape::none())
5186 .pretty_print(),
5187 "String",
5188 );
5189
5190 assert_eq!(
5191 wtf_selection
5192 .compute_output_shape(&shape_context, Shape::none())
5195 .pretty_print(),
5196 "One<String, null>",
5197 );
5198 }
5199
5200 #[test]
5201 fn question_operator_should_map_null_to_none() {
5202 let spec = ConnectSpec::V0_3;
5203
5204 let nullish_string_selection = selection!("$(stringOrNull?)", spec);
5205 assert_eq!(
5206 nullish_string_selection.apply_to(&json!({"stringOrNull": "a string"})),
5207 (Some(json!("a string")), vec![]),
5208 );
5209 assert_eq!(
5210 nullish_string_selection.apply_to(&json!({"stringOrNull": null})),
5211 (None, vec![]),
5212 );
5213 assert_eq!(
5214 nullish_string_selection.apply_to(&json!({})),
5215 (None, vec![]),
5216 );
5217
5218 let shape_context = {
5219 let mut named_shapes = IndexMap::default();
5220
5221 named_shapes.insert(
5222 "$root".to_string(),
5223 Shape::record(
5224 {
5225 let mut map = Shape::empty_map();
5226 map.insert(
5227 "stringOrNull".to_string(),
5228 Shape::one([Shape::string([]), Shape::null([])], []),
5229 );
5230 map
5231 },
5232 [],
5233 ),
5234 );
5235
5236 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5237 .with_spec(spec)
5238 .with_named_shapes(named_shapes)
5239 };
5240
5241 assert_eq!(
5242 nullish_string_selection
5243 .compute_output_shape(
5244 &shape_context,
5245 shape_context.named_shapes()["$root"].clone()
5246 )
5247 .pretty_print(),
5248 "One<String, None>",
5249 );
5250 }
5251
5252 #[test]
5253 fn question_operator_should_add_none_to_named_shapes() {
5254 let spec = ConnectSpec::V0_3;
5255
5256 let string_or_null_expr = selection!("$(stringOrNull?)", spec);
5257
5258 assert_eq!(
5259 string_or_null_expr.shape().pretty_print(),
5260 "$root.stringOrNull?",
5261 );
5262 }
5263
5264 #[test]
5265 fn question_operator_with_nested_objects() {
5266 let spec = ConnectSpec::V0_3;
5267
5268 let nested_selection = selection!("$(user?.profile?.name)", spec);
5269 assert_eq!(
5270 nested_selection.apply_to(&json!({"user": {"profile": {"name": "Alice"}}})),
5271 (Some(json!("Alice")), vec![]),
5272 );
5273 assert_eq!(
5274 nested_selection.apply_to(&json!({"user": null})),
5275 (None, vec![]),
5276 );
5277 assert_eq!(
5278 nested_selection.apply_to(&json!({"user": {"profile": null}})),
5279 (None, vec![]),
5280 );
5281 assert_eq!(nested_selection.apply_to(&json!({})), (None, vec![]));
5282 }
5283
5284 #[test]
5285 fn question_operator_with_nested_objects_shape() {
5286 let spec = ConnectSpec::V0_3;
5287
5288 let shape_context = {
5289 let mut named_shapes = IndexMap::default();
5290
5291 let person_shape = Shape::record(
5292 {
5293 let mut map = Shape::empty_map();
5294 map.insert("name".to_string(), Shape::string([]));
5295 map.insert("age".to_string(), Shape::int([]));
5296 map
5297 },
5298 [],
5299 );
5300
5301 named_shapes.insert(
5302 "$root".to_string(),
5303 Shape::record(
5304 {
5305 let mut map = Shape::empty_map();
5306 map.insert("person".to_string(), person_shape);
5307 map
5308 },
5309 [],
5310 ),
5311 );
5312
5313 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5314 .with_spec(spec)
5315 .with_named_shapes(named_shapes)
5316 };
5317
5318 let nested_selection = selection!("$(person?.name)", spec);
5319 assert_eq!(
5320 nested_selection
5321 .compute_output_shape(
5322 &shape_context,
5323 shape_context.named_shapes()["$root"].clone()
5324 )
5325 .pretty_print(),
5326 "One<String, None>",
5327 );
5328 }
5329
5330 #[test]
5331 fn question_operator_with_array_access() {
5332 let spec = ConnectSpec::V0_3;
5333
5334 let array_selection = selection!("$(items?->first?.name)", spec);
5335 assert_eq!(
5336 array_selection.apply_to(&json!({"items": [{"name": "first"}]})),
5337 (Some(json!("first")), vec![]),
5338 );
5339 assert_eq!(
5340 array_selection.apply_to(&json!({"items": []})),
5341 (None, vec![]),
5342 );
5343 assert_eq!(
5344 array_selection.apply_to(&json!({"items": null})),
5345 (None, vec![]),
5346 );
5347 assert_eq!(array_selection.apply_to(&json!({})), (None, vec![]));
5348 }
5349
5350 #[test]
5351 fn question_operator_with_union_shapes() {
5352 let spec = ConnectSpec::V0_3;
5353
5354 let shape_context = {
5355 let mut named_shapes = IndexMap::default();
5356
5357 named_shapes.insert(
5358 "$root".to_string(),
5359 Shape::record(
5360 {
5361 let mut map = Shape::empty_map();
5362 map.insert(
5363 "unionField".to_string(),
5364 Shape::one([Shape::string([]), Shape::int([]), Shape::null([])], []),
5365 );
5366 map
5367 },
5368 [],
5369 ),
5370 );
5371
5372 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5373 .with_spec(spec)
5374 .with_named_shapes(named_shapes)
5375 };
5376
5377 let union_selection = selection!("$(unionField?)", spec);
5378
5379 assert_eq!(
5380 union_selection
5381 .compute_output_shape(
5382 &shape_context,
5383 shape_context.named_shapes().get("$root").unwrap().clone(),
5384 )
5385 .pretty_print(),
5386 "One<String, Int, None>",
5387 );
5388 }
5389
5390 #[test]
5391 fn question_operator_with_union_shapes_non_nullable() {
5392 let spec = ConnectSpec::V0_3;
5393
5394 let shape_context = {
5395 let mut named_shapes = IndexMap::default();
5396
5397 named_shapes.insert(
5398 "$root".to_string(),
5399 Shape::record(
5400 {
5401 let mut map = Shape::empty_map();
5402 map.insert(
5403 "value".to_string(),
5404 Shape::one([Shape::string([]), Shape::int([])], []),
5405 );
5406 map
5407 },
5408 [],
5409 ),
5410 );
5411
5412 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5413 .with_spec(spec)
5414 .with_named_shapes(named_shapes)
5415 };
5416
5417 let union_selection = selection!("$(value?)", spec);
5418 assert_eq!(
5419 union_selection
5420 .compute_output_shape(
5421 &shape_context,
5422 shape_context.named_shapes()["$root"].clone()
5423 )
5424 .pretty_print(),
5425 "One<String, Int>",
5426 );
5427 }
5428
5429 #[test]
5430 fn question_operator_with_error_shapes() {
5431 let spec = ConnectSpec::V0_3;
5432
5433 let shape_context = {
5434 let mut named_shapes = IndexMap::default();
5435
5436 named_shapes.insert(
5437 "$root".to_string(),
5438 Shape::record(
5439 {
5440 let mut map = Shape::empty_map();
5441 map.insert(
5442 "errorField".to_string(),
5443 Shape::error_with_partial(
5444 "Test error".to_string(),
5445 Shape::one([Shape::string([]), Shape::null([])], []),
5446 [],
5447 ),
5448 );
5449 map
5450 },
5451 [],
5452 ),
5453 );
5454
5455 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5456 .with_spec(spec)
5457 .with_named_shapes(named_shapes)
5458 };
5459
5460 let error_selection = selection!("$(errorField?)", spec);
5461
5462 let result_shape = error_selection.compute_output_shape(
5463 &shape_context,
5464 shape_context.named_shapes().get("$root").unwrap().clone(),
5465 );
5466
5467 assert!(result_shape.pretty_print().contains("Error"));
5469 assert!(result_shape.pretty_print().contains("None"));
5470 }
5471
5472 #[test]
5473 fn question_operator_with_simple_error_shapes() {
5474 let spec = ConnectSpec::V0_3;
5475
5476 let shape_context = {
5477 let mut named_shapes = IndexMap::default();
5478
5479 named_shapes.insert(
5480 "$root".to_string(),
5481 Shape::record(
5482 {
5483 let mut map = Shape::empty_map();
5484 map.insert(
5485 "error".to_string(),
5486 Shape::error("Something went wrong".to_string(), []),
5487 );
5488 map
5489 },
5490 [],
5491 ),
5492 );
5493
5494 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5495 .with_spec(spec)
5496 .with_named_shapes(named_shapes)
5497 };
5498
5499 let error_selection = selection!("$(error?)", spec);
5500 assert_eq!(
5501 error_selection
5502 .compute_output_shape(
5503 &shape_context,
5504 shape_context.named_shapes()["$root"].clone()
5505 )
5506 .pretty_print(),
5507 "Error<\"Something went wrong\">",
5508 );
5509 }
5510
5511 #[test]
5512 fn question_operator_with_all_shapes() {
5513 let spec = ConnectSpec::V0_3;
5514
5515 let shape_context = {
5516 let mut named_shapes = IndexMap::default();
5517
5518 named_shapes.insert(
5519 "$root".to_string(),
5520 Shape::record(
5521 {
5522 let mut map = Shape::empty_map();
5523 map.insert(
5524 "allField".to_string(),
5525 Shape::all(
5526 [
5527 Shape::string([]),
5528 Shape::one([Shape::string([]), Shape::null([])], []),
5529 ],
5530 [],
5531 ),
5532 );
5533 map
5534 },
5535 [],
5536 ),
5537 );
5538
5539 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5540 .with_spec(spec)
5541 .with_named_shapes(named_shapes)
5542 };
5543
5544 let all_selection = selection!("$(allField?)", spec);
5545
5546 assert_eq!(
5547 all_selection
5548 .compute_output_shape(
5549 &shape_context,
5550 shape_context.named_shapes().get("$root").unwrap().clone(),
5551 )
5552 .pretty_print(),
5553 "One<String, None>",
5554 );
5555 }
5556
5557 #[test]
5558 fn question_operator_with_simple_all_shapes() {
5559 let spec = ConnectSpec::V0_3;
5560
5561 let shape_context = {
5562 let mut named_shapes = IndexMap::default();
5563
5564 named_shapes.insert(
5565 "$root".to_string(),
5566 Shape::record(
5567 {
5568 let mut map = Shape::empty_map();
5569 map.insert(
5570 "intersection".to_string(),
5571 Shape::all([Shape::string([]), Shape::int([])], []),
5572 );
5573 map
5574 },
5575 [],
5576 ),
5577 );
5578
5579 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5580 .with_spec(spec)
5581 .with_named_shapes(named_shapes)
5582 };
5583
5584 let all_selection = selection!("$(intersection?)", spec);
5585 assert_eq!(
5586 all_selection
5587 .compute_output_shape(
5588 &shape_context,
5589 shape_context.named_shapes()["$root"].clone()
5590 )
5591 .pretty_print(),
5592 "All<String, Int>",
5593 );
5594 }
5595
5596 #[test]
5597 fn question_operator_preserves_non_null_shapes() {
5598 let spec = ConnectSpec::V0_3;
5599
5600 let shape_context = {
5601 let mut named_shapes = IndexMap::default();
5602
5603 named_shapes.insert(
5604 "$root".to_string(),
5605 Shape::record(
5606 {
5607 let mut map = Shape::empty_map();
5608 map.insert("nonNullString".to_string(), Shape::string([]));
5609 map
5610 },
5611 [],
5612 ),
5613 );
5614
5615 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5616 .with_spec(spec)
5617 .with_named_shapes(named_shapes)
5618 };
5619
5620 let non_null_selection = selection!("$(nonNullString?)", spec);
5621
5622 assert_eq!(
5623 non_null_selection
5624 .compute_output_shape(
5625 &shape_context,
5626 shape_context.named_shapes().get("$root").unwrap().clone(),
5627 )
5628 .pretty_print(),
5629 "String",
5630 );
5631 }
5632
5633 #[test]
5634 fn question_operator_with_multiple_operators_in_chain() {
5635 let spec = ConnectSpec::V0_3;
5636
5637 let mixed_chain_selection = selection!("$(field? ?? 'default')", spec);
5639 assert_eq!(
5640 mixed_chain_selection.apply_to(&json!({"field": "value"})),
5641 (Some(json!("value")), vec![]),
5642 );
5643 assert_eq!(
5644 mixed_chain_selection.apply_to(&json!({"field": null})),
5645 (Some(json!("default")), vec![]),
5646 );
5647 assert_eq!(
5648 mixed_chain_selection.apply_to(&json!({})),
5649 (Some(json!("default")), vec![]),
5650 );
5651 }
5652
5653 #[test]
5654 fn question_operator_chained_shape() {
5655 let spec = ConnectSpec::V0_3;
5656
5657 let shape_context = {
5658 let mut named_shapes = IndexMap::default();
5659
5660 named_shapes.insert(
5661 "$root".to_string(),
5662 Shape::record(
5663 {
5664 let mut map = Shape::empty_map();
5665 map.insert(
5666 "level1".to_string(),
5667 Shape::record(
5668 {
5669 let mut inner_map = Shape::empty_map();
5670 inner_map.insert(
5671 "level2".to_string(),
5672 Shape::record(
5673 {
5674 let mut inner_inner_map = Shape::empty_map();
5675 inner_inner_map
5676 .insert("value".to_string(), Shape::string([]));
5677 inner_inner_map
5678 },
5679 [],
5680 ),
5681 );
5682 inner_map
5683 },
5684 [],
5685 ),
5686 );
5687 map
5688 },
5689 [],
5690 ),
5691 );
5692
5693 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5694 .with_spec(spec)
5695 .with_named_shapes(named_shapes)
5696 };
5697
5698 let chained_selection = selection!("$(level1?.level2?.value)", spec);
5699 assert_eq!(
5700 chained_selection
5701 .compute_output_shape(
5702 &shape_context,
5703 shape_context.named_shapes()["$root"].clone()
5704 )
5705 .pretty_print(),
5706 "One<String, None>",
5707 );
5708 }
5709
5710 #[test]
5711 fn question_operator_direct_null_input_shape() {
5712 let spec = ConnectSpec::V0_3;
5713
5714 let shape_context = {
5715 let mut named_shapes = IndexMap::default();
5716
5717 named_shapes.insert("$root".to_string(), Shape::null([]));
5718
5719 ShapeContext::new(SourceId::Other("JSONSelection".into()))
5720 .with_spec(spec)
5721 .with_named_shapes(named_shapes)
5722 };
5723
5724 let null_selection = selection!("$root?", spec);
5725
5726 assert_eq!(
5727 null_selection
5728 .compute_output_shape(
5729 &shape_context,
5730 shape_context.named_shapes().get("$root").unwrap().clone(),
5731 )
5732 .pretty_print(),
5733 "None",
5734 );
5735 }
5736
5737 #[test]
5738 fn test_unknown_name() {
5739 let spec = ConnectSpec::V0_3;
5740 let sel = selection!("book.author? { name age? }", spec);
5741 assert_eq!(
5742 sel.shape().pretty_print(),
5743 r#"One<
5744 {
5745 age: $root.book.author?.*.age?,
5746 name: $root.book.author?.*.name,
5747 },
5748 None,
5749>"#,
5750 );
5751 }
5752
5753 #[test]
5754 fn test_nullish_coalescing_shape() {
5755 let spec = ConnectSpec::V0_3;
5756 let sel = selection!("$(a ?? b ?? c)", spec);
5757 assert_eq!(
5758 sel.shape().pretty_print(),
5759 "One<$root.a?!, $root.b?!, $root.c>",
5760 );
5761
5762 let mut named_shapes = IndexMap::default();
5763 named_shapes.insert(
5764 "$root".to_string(),
5765 Shape::record(
5766 {
5767 let mut map = Shape::empty_map();
5768 map.insert(
5769 "a".to_string(),
5770 Shape::one([Shape::string([]), Shape::null([])], []),
5771 );
5772 map.insert("b".to_string(), Shape::string([]));
5773 map.insert("c".to_string(), Shape::int([]));
5774 map
5775 },
5776 [],
5777 ),
5778 );
5779
5780 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5781 .with_spec(spec)
5782 .with_named_shapes(named_shapes);
5783
5784 assert_eq!(
5785 sel.compute_output_shape(
5786 &shape_context,
5787 shape_context.named_shapes().get("$root").unwrap().clone(),
5788 )
5789 .pretty_print(),
5790 "One<String, Int>",
5791 );
5792 }
5793
5794 #[test]
5795 fn test_none_coalescing_shape() {
5796 let spec = ConnectSpec::V0_3;
5797 let sel = selection!("$(a ?! b ?! c)", spec);
5798 assert_eq!(
5799 sel.shape().pretty_print(),
5800 "One<$root.a!, $root.b!, $root.c>",
5801 );
5802
5803 let mut named_shapes = IndexMap::default();
5804 named_shapes.insert(
5805 "$root".to_string(),
5806 Shape::record(
5807 {
5808 let mut map = Shape::empty_map();
5809 map.insert(
5810 "a".to_string(),
5811 Shape::one([Shape::string([]), Shape::null([])], []),
5812 );
5813 map.insert(
5814 "b".to_string(),
5815 Shape::one([Shape::string([]), Shape::none()], []),
5816 );
5817 map.insert("c".to_string(), Shape::null([]));
5818 map
5819 },
5820 [],
5821 ),
5822 );
5823
5824 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5825 .with_spec(spec)
5826 .with_named_shapes(named_shapes);
5827
5828 assert_eq!(
5829 sel.compute_output_shape(
5830 &shape_context,
5831 shape_context.named_shapes().get("$root").unwrap().clone(),
5832 )
5833 .pretty_print(),
5834 "One<String, null>",
5835 );
5836 }
5837
5838 #[test]
5839 fn test_none_coalescing_with_literal_fallback() {
5840 let spec = ConnectSpec::V0_3;
5841
5842 let shape_context = ShapeContext::new(SourceId::Other("JSONSelection".into()))
5843 .with_spec(spec)
5844 .with_named_shapes([(
5845 "$root".to_string(),
5846 Shape::record(
5847 {
5848 let mut map = Shape::empty_map();
5849 map.insert(
5850 "optional".to_string(),
5851 Shape::one([Shape::string([]), Shape::none()], []),
5852 );
5853 map
5854 },
5855 [],
5856 ),
5857 )]);
5858
5859 let none_coalesce = selection!("$(optional ?! 'fallback')", spec);
5860 assert_eq!(
5861 none_coalesce
5862 .compute_output_shape(
5863 &shape_context,
5864 shape_context.named_shapes()["$root"].clone()
5865 )
5866 .pretty_print(),
5867 "String",
5868 );
5869 }
5870
5871 #[test]
5877 fn test_multiple_spreads_shape_with_conflicting_typename() {
5878 let spec = ConnectSpec::V0_4;
5879
5880 let selection = selection!(
5882 r#"
5883 upc
5884 ... category->match(
5885 ['book', { __typename: $('Book'), title: $.title }],
5886 ['film', { __typename: $('Film'), director: $.director }],
5887 [@, null]
5888 )
5889 ... format->match(
5890 ['digital', { __typename: $('Digital'), downloadUrl: $.url }],
5891 ['physical', { __typename: $('Physical'), weight: $.weight }],
5892 [@, null]
5893 )
5894 "#,
5895 spec
5896 );
5897
5898 assert_eq!(
5900 selection.shape().pretty_print(),
5901 r#"One<
5902 {
5903 __typename: All<"Book", "Digital">,
5904 downloadUrl: $root.*.url,
5905 title: $root.*.title,
5906 upc: $root.*.upc,
5907 },
5908 {
5909 __typename: All<"Book", "Physical">,
5910 title: $root.*.title,
5911 upc: $root.*.upc,
5912 weight: $root.*.weight,
5913 },
5914 null,
5915 {
5916 __typename: All<"Film", "Digital">,
5917 director: $root.*.director,
5918 downloadUrl: $root.*.url,
5919 upc: $root.*.upc,
5920 },
5921 {
5922 __typename: All<"Film", "Physical">,
5923 director: $root.*.director,
5924 upc: $root.*.upc,
5925 weight: $root.*.weight,
5926 },
5927>"#,
5928 );
5929 }
5930
5931 #[test]
5936 fn test_multiple_spreads_shape_without_typename_conflict() {
5937 let spec = ConnectSpec::V0_4;
5938
5939 let selection = selection!(
5941 r#"
5942 upc
5943 ... category->match(
5944 ['book', { __typename: $('Book'), title: $.title }],
5945 ['film', { __typename: $('Film'), director: $.director }],
5946 [@, null]
5947 )
5948 ... format->match(
5949 ['digital', { downloadUrl: $.url }],
5950 ['physical', { weight: $.weight }],
5951 [@, null]
5952 )
5953 "#,
5954 spec
5955 );
5956
5957 assert_eq!(
5959 selection.shape().pretty_print(),
5960 r#"One<
5961 {
5962 __typename: "Book",
5963 downloadUrl: $root.*.url,
5964 title: $root.*.title,
5965 upc: $root.*.upc,
5966 },
5967 {
5968 __typename: "Book",
5969 title: $root.*.title,
5970 upc: $root.*.upc,
5971 weight: $root.*.weight,
5972 },
5973 null,
5974 {
5975 __typename: "Film",
5976 director: $root.*.director,
5977 downloadUrl: $root.*.url,
5978 upc: $root.*.upc,
5979 },
5980 {
5981 __typename: "Film",
5982 director: $root.*.director,
5983 upc: $root.*.upc,
5984 weight: $root.*.weight,
5985 },
5986>"#,
5987 );
5988 }
5989
5990 #[test]
5992 fn test_single_spread_shape() {
5993 let spec = ConnectSpec::V0_4;
5994
5995 let selection = selection!(
5996 r#"
5997 upc
5998 ... category->match(
5999 ['book', { __typename: $('Book'), title: $.title }],
6000 ['film', { __typename: $('Film'), director: $.director }],
6001 [@, null]
6002 )
6003 "#,
6004 spec
6005 );
6006
6007 assert_eq!(
6009 selection.shape().pretty_print(),
6010 r#"One<
6011 {
6012 __typename: "Book",
6013 title: $root.*.title,
6014 upc: $root.*.upc,
6015 },
6016 {
6017 __typename: "Film",
6018 director: $root.*.director,
6019 upc: $root.*.upc,
6020 },
6021 null,
6022>"#,
6023 );
6024 }
6025
6026 #[test]
6031 fn test_multiple_spreads_same_typename_no_conflict() {
6032 let spec = ConnectSpec::V0_4;
6033
6034 let selection = selection!(
6036 r#"
6037 upc
6038 ... category->match(
6039 ['book', { __typename: $('Book'), title: $.title }],
6040 ['film', { __typename: $('Film'), director: $.director }],
6041 [@, null]
6042 )
6043 ... category->match(
6044 ['book', { __typename: $('Book'), author: $.author }]
6045 )
6046 "#,
6047 spec
6048 );
6049
6050 assert_eq!(
6056 selection.shape().pretty_print(),
6057 r#"One<
6058 {
6059 __typename: "Book",
6060 author: $root.*.author,
6061 title: $root.*.title,
6062 upc: $root.*.upc,
6063 },
6064 {
6065 __typename: "Book",
6066 title: $root.*.title,
6067 upc: $root.*.upc,
6068 },
6069 {
6070 __typename: All<"Film", "Book">,
6071 author: $root.*.author,
6072 director: $root.*.director,
6073 upc: $root.*.upc,
6074 },
6075 {
6076 __typename: "Film",
6077 director: $root.*.director,
6078 upc: $root.*.upc,
6079 },
6080 null,
6081>"#,
6082 );
6083 }
6084
6085 #[test]
6087 fn test_multiple_spreads_field_shadowing() {
6088 let spec = ConnectSpec::V0_4;
6089
6090 let selection = selection!(
6092 r#"
6093 upc
6094 ... category->match(
6095 ['book', { __typename: $('Book'), greeting: $('hello') }],
6096 [@, null]
6097 )
6098 ... category->match(
6099 ['book', { __typename: $('Book'), greeting: $('goodbye') }]
6100 )
6101 "#,
6102 spec
6103 );
6104
6105 assert_eq!(
6109 selection.shape().pretty_print(),
6110 r#"One<
6111 {
6112 __typename: "Book",
6113 greeting: All<"hello", "goodbye">,
6114 upc: $root.*.upc,
6115 },
6116 { __typename: "Book", greeting: "hello", upc: $root.*.upc },
6117 null,
6118>"#,
6119 );
6120 }
6121
6122 #[test]
6124 fn test_second_spread_no_typename_adds_to_all() {
6125 let spec = ConnectSpec::V0_4;
6126
6127 let selection = selection!(
6130 r#"
6131 upc
6132 ... category->match(
6133 ['book', { __typename: $('Book') }],
6134 ['film', { __typename: $('Film') }],
6135 [@, null]
6136 )
6137 ... format->match(
6138 ['digital', { isDigital: $(true) }],
6139 [@, null]
6140 )
6141 "#,
6142 spec
6143 );
6144
6145 assert_eq!(
6147 selection.shape().pretty_print(),
6148 r#"One<
6149 { __typename: "Book", isDigital: true, upc: $root.*.upc },
6150 null,
6151 { __typename: "Film", isDigital: true, upc: $root.*.upc },
6152>"#,
6153 );
6154 }
6155
6156 #[test]
6164 fn test_spread_cartesian_product_conservative() {
6165 let spec = ConnectSpec::V0_4;
6166
6167 let selection = selection!(
6169 r#"
6170 upc
6171 ... category->match(
6172 ['book', { __typename: $('Book'), title: $.title }],
6173 ['film', { __typename: $('Film'), director: $.director }],
6174 [@, null]
6175 )
6176 ... category->match(
6177 ['book', { extraField: $('only for books') }]
6178 )
6179 "#,
6180 spec
6181 );
6182
6183 assert_eq!(
6189 selection.shape().pretty_print(),
6190 r#"One<
6191 {
6192 __typename: "Book",
6193 extraField: "only for books",
6194 title: $root.*.title,
6195 upc: $root.*.upc,
6196 },
6197 {
6198 __typename: "Book",
6199 title: $root.*.title,
6200 upc: $root.*.upc,
6201 },
6202 {
6203 __typename: "Film",
6204 director: $root.*.director,
6205 extraField: "only for books",
6206 upc: $root.*.upc,
6207 },
6208 {
6209 __typename: "Film",
6210 director: $root.*.director,
6211 upc: $root.*.upc,
6212 },
6213 null,
6214>"#,
6215 );
6216 }
6217
6218 #[test]
6224 fn test_multiple_spreads_runtime_both_match_book() {
6225 let spec = ConnectSpec::V0_4;
6226 let selection = selection!(
6227 r#"
6228 upc
6229 ... category->match(
6230 ['book', { __typename: $('Book'), title: $.title }],
6231 ['film', { __typename: $('Film'), director: $.director }],
6232 [@, null]
6233 )
6234 ... category->match(
6235 ['book', { __typename: $('Book'), author: $.author }]
6236 )
6237 "#,
6238 spec
6239 );
6240
6241 let book_data = json!({
6243 "upc": "123",
6244 "category": "book",
6245 "title": "Great Gatsby",
6246 "author": "Fitzgerald"
6247 });
6248 let (result, errors) = selection.apply_to(&book_data);
6249 assert_eq!(errors, vec![]);
6250 assert_eq!(
6251 result,
6252 Some(json!({
6253 "upc": "123",
6254 "__typename": "Book",
6255 "title": "Great Gatsby",
6256 "author": "Fitzgerald"
6257 }))
6258 );
6259 }
6260
6261 #[test]
6263 fn test_multiple_spreads_runtime_film_no_second_match() {
6264 let spec = ConnectSpec::V0_4;
6265 let selection = selection!(
6266 r#"
6267 upc
6268 ... category->match(
6269 ['book', { __typename: $('Book'), title: $.title }],
6270 ['film', { __typename: $('Film'), director: $.director }],
6271 [@, null]
6272 )
6273 ... category->match(
6274 ['book', { __typename: $('Book'), author: $.author }]
6275 )
6276 "#,
6277 spec
6278 );
6279
6280 let film_data = json!({
6282 "upc": "456",
6283 "category": "film",
6284 "director": "Nolan"
6285 });
6286 let (result, errors) = selection.apply_to(&film_data);
6287 assert!(!errors.is_empty());
6289 assert_eq!(
6290 result,
6291 Some(json!({
6292 "upc": "456",
6293 "__typename": "Film",
6294 "director": "Nolan"
6295 }))
6296 );
6297 }
6298
6299 #[test]
6301 fn test_multiple_spreads_runtime_field_shadowing() {
6302 let spec = ConnectSpec::V0_4;
6303 let selection = selection!(
6304 r#"
6305 upc
6306 ... category->match(
6307 ['book', { __typename: $('Book'), greeting: $('hello') }],
6308 [@, null]
6309 )
6310 ... category->match(
6311 ['book', { __typename: $('Book'), greeting: $('goodbye') }]
6312 )
6313 "#,
6314 spec
6315 );
6316
6317 let book_data = json!({
6319 "upc": "789",
6320 "category": "book"
6321 });
6322 let (result, errors) = selection.apply_to(&book_data);
6323 assert_eq!(errors, vec![]);
6324 assert_eq!(
6326 result,
6327 Some(json!({
6328 "upc": "789",
6329 "__typename": "Book",
6330 "greeting": "goodbye"
6331 }))
6332 );
6333 }
6334
6335 #[test]
6337 fn test_multiple_spreads_runtime_no_typename_adds_to_all() {
6338 let spec = ConnectSpec::V0_4;
6339 let selection = selection!(
6340 r#"
6341 upc
6342 ... category->match(
6343 ['book', { __typename: $('Book') }],
6344 ['film', { __typename: $('Film') }],
6345 [@, null]
6346 )
6347 ... format->match(
6348 ['digital', { isDigital: $(true) }],
6349 [@, null]
6350 )
6351 "#,
6352 spec
6353 );
6354
6355 let book_digital = json!({
6357 "upc": "111",
6358 "category": "book",
6359 "format": "digital"
6360 });
6361 let (result, errors) = selection.apply_to(&book_digital);
6362 assert_eq!(errors, vec![]);
6363 assert_eq!(
6364 result,
6365 Some(json!({
6366 "upc": "111",
6367 "__typename": "Book",
6368 "isDigital": true
6369 }))
6370 );
6371
6372 let film_digital = json!({
6374 "upc": "222",
6375 "category": "film",
6376 "format": "digital"
6377 });
6378 let (result, errors) = selection.apply_to(&film_digital);
6379 assert_eq!(errors, vec![]);
6380 assert_eq!(
6381 result,
6382 Some(json!({
6383 "upc": "222",
6384 "__typename": "Film",
6385 "isDigital": true
6386 }))
6387 );
6388
6389 let book_physical = json!({
6392 "upc": "333",
6393 "category": "book",
6394 "format": "physical"
6395 });
6396 let (result, errors) = selection.apply_to(&book_physical);
6397 assert_eq!(errors, vec![]);
6398 assert_eq!(result, Some(json!(null)));
6400 }
6401
6402 #[test]
6404 fn test_multiple_spreads_runtime_no_match_returns_null() {
6405 let spec = ConnectSpec::V0_4;
6406 let selection = selection!(
6407 r#"
6408 upc
6409 ... category->match(
6410 ['book', { __typename: $('Book'), title: $.title }],
6411 ['film', { __typename: $('Film'), director: $.director }],
6412 [@, null]
6413 )
6414 "#,
6415 spec
6416 );
6417
6418 let unknown_data = json!({
6420 "upc": "999",
6421 "category": "unknown"
6422 });
6423 let (result, errors) = selection.apply_to(&unknown_data);
6424 assert_eq!(errors, vec![]);
6425 assert_eq!(result, Some(json!(null)));
6426 }
6427}