1use itertools::Itertools;
9
10use super::lit_expr::LitExpr;
11use super::lit_expr::LitOp;
12use super::location::WithRange;
13use super::parser::Alias;
14use super::parser::Key;
15use crate::connectors::json_selection::JSONSelection;
16use crate::connectors::json_selection::MethodArgs;
17use crate::connectors::json_selection::NamedSelection;
18use crate::connectors::json_selection::NamingPrefix;
19use crate::connectors::json_selection::PathList;
20use crate::connectors::json_selection::PathSelection;
21use crate::connectors::json_selection::SubSelection;
22use crate::connectors::json_selection::TopLevelSelection;
23
24impl std::fmt::Display for JSONSelection {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.write_str(&PrettyPrintable::pretty_print(self))
27 }
28}
29
30pub(crate) trait PrettyPrintable {
40 fn pretty_print(&self) -> String {
42 self.pretty_print_with_indentation(false, 0)
43 }
44
45 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String;
50}
51
52fn indent_chars(indent: usize) -> String {
54 " ".repeat(indent)
55}
56
57impl PrettyPrintable for JSONSelection {
58 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
59 match &self.inner {
60 TopLevelSelection::Named(named) => named.print_subselections(inline, indentation),
61 TopLevelSelection::Value(lit) => lit.pretty_print_with_indentation(inline, indentation),
62 }
63 }
64}
65
66impl PrettyPrintable for SubSelection {
67 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
68 let mut result = String::new();
69
70 result.push('{');
71
72 if self.selections.is_empty() {
73 result.push('}');
74 return result;
75 }
76
77 if inline {
78 result.push(' ');
79 } else {
80 result.push('\n');
81 result.push_str(indent_chars(indentation + 1).as_str());
82 }
83
84 result.push_str(&self.print_subselections(inline, indentation + 1));
85
86 if inline {
87 result.push(' ');
88 } else {
89 result.push('\n');
90 result.push_str(indent_chars(indentation).as_str());
91 }
92
93 result.push('}');
94
95 result
96 }
97}
98
99impl SubSelection {
100 fn print_subselections(&self, inline: bool, indentation: usize) -> String {
102 let separator = if inline {
103 ' '.to_string()
104 } else {
105 format!("\n{}", indent_chars(indentation))
106 };
107
108 self.selections
109 .iter()
110 .map(|s| s.pretty_print_with_indentation(inline, indentation))
111 .join(separator.as_str())
112 }
113}
114
115impl PrettyPrintable for PathSelection {
116 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
117 let inner = self.path.pretty_print_with_indentation(inline, indentation);
118 let leading_space_count = inner.chars().take_while(|c| *c == ' ').count();
126 let suffix = inner[leading_space_count..].to_string();
127 if let Some(after_dot) = suffix.strip_prefix('.') {
128 format!("{}{}", " ".repeat(leading_space_count), after_dot)
130 } else {
131 inner
132 }
133 }
134}
135
136impl PrettyPrintable for PathList {
137 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
138 let mut result = String::new();
139
140 match self {
141 Self::Var(var, tail) => {
142 let rest = tail.pretty_print_with_indentation(inline, indentation);
143 result.push_str(var.as_str());
144 result.push_str(rest.as_str());
145 }
146 Self::Key(key, tail) => {
147 result.push('.');
148 result.push_str(key.pretty_print().as_str());
149 let rest = tail.pretty_print_with_indentation(inline, indentation);
150 result.push_str(rest.as_str());
151 }
152 Self::Expr(expr, tail) => {
153 let inner = expr.pretty_print_with_indentation(inline, indentation);
157 let rest = tail.pretty_print_with_indentation(inline, indentation);
158 result.push_str("$(");
159 result.push_str(inner.as_str());
160 result.push(')');
161 result.push_str(rest.as_str());
162 }
163 Self::Method(method, args, tail) => {
164 result.push_str("->");
165 result.push_str(method.as_str());
166 if let Some(args) = args {
167 result.push_str(
168 args.pretty_print_with_indentation(inline, indentation)
169 .as_str(),
170 );
171 }
172 result.push_str(
173 tail.pretty_print_with_indentation(inline, indentation)
174 .as_str(),
175 );
176 }
177 Self::Question(tail) => {
178 result.push('?');
179 let rest = tail.pretty_print_with_indentation(true, indentation);
180 result.push_str(rest.as_str());
181 }
182 Self::Selection(sub) => {
183 let sub = sub.pretty_print_with_indentation(inline, indentation);
184 result.push(' ');
185 result.push_str(sub.as_str());
186 }
187 Self::Empty => {}
188 }
189
190 result
191 }
192}
193
194impl PrettyPrintable for MethodArgs {
195 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
196 let printed_args: Vec<String> = self
197 .args
198 .iter()
199 .map(|arg| arg.pretty_print_with_indentation(inline, indentation + 1))
200 .collect();
201
202 let would_break = if inline {
206 self.args
207 .iter()
208 .any(|arg| arg.pretty_print_with_indentation(false, 0).contains('\n'))
209 } else {
210 printed_args.iter().any(|a| a.contains('\n'))
211 };
212
213 if !inline && would_break {
214 let indent = indent_chars(indentation + 1);
215 let separator = format!(",\n{indent}");
216 let joined = printed_args.iter().map(String::as_str).join(&separator);
217 format!("(\n{indent}{joined}\n{})", indent_chars(indentation))
218 } else if would_break {
219 let joined = printed_args.iter().map(String::as_str).join(", ");
220 format!("( {joined} )")
221 } else {
222 let joined = printed_args.iter().map(String::as_str).join(", ");
223 format!("({joined})")
224 }
225 }
226}
227
228impl LitExpr {
229 fn is_shorthand_property(key: &WithRange<Key>, value: &WithRange<LitExpr>) -> bool {
230 let Key::Field(key_name) = key.as_ref() else {
231 return false;
232 };
233 let LitExpr::Path(PathSelection { path }) = value.as_ref() else {
234 return false;
235 };
236 let PathList::Key(path_key, tail) = path.as_ref() else {
237 return false;
238 };
239 let tail_is_simple = match tail.as_ref() {
240 PathList::Empty => true,
241 PathList::Question(inner) => matches!(inner.as_ref(), PathList::Empty),
243 PathList::Selection(_) => true,
245 _ => false,
246 };
247 tail_is_simple && path_key.as_str() == key_name
248 }
249}
250
251impl PrettyPrintable for LitExpr {
252 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
253 let mut result = String::new();
254
255 match self {
256 Self::String(s) => {
257 let safely_quoted = serde_json_bytes::Value::String(s.clone().into()).to_string();
258 result.push_str(safely_quoted.as_str());
259 }
260 Self::Number(n) => result.push_str(n.to_string().as_str()),
261 Self::Bool(b) => result.push_str(b.to_string().as_str()),
262 Self::Null => result.push_str("null"),
263 Self::Object(sub) => {
264 result.push('{');
265
266 if sub.selections.is_empty() {
267 result.push('}');
268 return result;
269 }
270
271 let mut is_first = true;
272 for sel in &sub.selections {
273 if is_first {
274 is_first = false;
275 } else {
276 result.push(',');
277 }
278
279 if inline {
280 result.push(' ');
281 } else {
282 result.push('\n');
283 result.push_str(indent_chars(indentation + 1).as_str());
284 }
285
286 result.push_str(
287 sel.pretty_print_with_indentation(inline, indentation + 1)
288 .as_str(),
289 );
290 }
291
292 if inline {
293 result.push(' ');
294 } else {
295 result.push('\n');
296 result.push_str(indent_chars(indentation).as_str());
297 }
298
299 result.push('}');
300 }
301 Self::LegacyObject(map) => {
302 result.push('{');
303
304 if map.is_empty() {
305 result.push('}');
306 return result;
307 }
308
309 let mut is_first = true;
310 for (key, value) in map {
311 if is_first {
312 is_first = false;
313 } else {
314 result.push(',');
315 }
316
317 if inline {
318 result.push(' ');
319 } else {
320 result.push('\n');
321 result.push_str(indent_chars(indentation + 1).as_str());
322 }
323
324 if Self::is_shorthand_property(key, value) {
325 result.push_str(
328 value
329 .pretty_print_with_indentation(inline, indentation + 1)
330 .as_str(),
331 );
332 } else {
333 result.push_str(key.pretty_print().as_str());
334 result.push_str(": ");
335 result.push_str(
336 value
337 .pretty_print_with_indentation(inline, indentation + 1)
338 .as_str(),
339 );
340 }
341 }
342
343 if inline {
344 result.push(' ');
345 } else {
346 result.push('\n');
347 result.push_str(indent_chars(indentation).as_str());
348 }
349
350 result.push('}');
351 }
352 Self::Array(vec) => {
353 result.push('[');
354 let mut is_first = true;
355 for value in vec {
356 if is_first {
357 is_first = false;
358 } else {
359 result.push_str(", ");
360 }
361 result.push_str(
362 value
363 .pretty_print_with_indentation(inline, indentation)
364 .as_str(),
365 );
366 }
367 result.push(']');
368 }
369 Self::Path(path) => {
370 result.push_str(
371 path.pretty_print_with_indentation(inline, indentation)
372 .as_str(),
373 );
374 }
375 Self::LitPath(literal, subpath) => {
376 result.push_str(
377 literal
378 .pretty_print_with_indentation(inline, indentation)
379 .as_str(),
380 );
381 result.push_str(
382 subpath
383 .pretty_print_with_indentation(inline, indentation)
384 .as_str(),
385 );
386 }
387 Self::OpChain(op, operands) => {
388 let op_str = match op.as_ref() {
389 LitOp::NullishCoalescing => " ?? ",
390 LitOp::NoneCoalescing => " ?! ",
391 };
392
393 for (i, operand) in operands.iter().enumerate() {
394 if i > 0 {
395 result.push_str(op_str);
396 }
397 result.push_str(
398 operand
399 .pretty_print_with_indentation(inline, indentation)
400 .as_str(),
401 );
402 }
403 }
404 }
405
406 result
407 }
408}
409
410impl PrettyPrintable for NamedSelection {
411 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
412 let mut result = String::new();
413
414 let is_shorthand = if let NamingPrefix::Alias(alias) = &self.prefix
418 && let LitExpr::Path(path) = self.path.as_ref()
419 {
420 path.get_single_key()
421 .is_some_and(|key| alias.name.as_ref() == key.as_ref())
422 } else {
423 false
424 };
425
426 match &self.prefix {
427 NamingPrefix::None => {}
428 NamingPrefix::Alias(alias) => {
429 if !is_shorthand {
430 result.push_str(alias.pretty_print().as_str());
431 result.push(' ');
432 }
433 }
434 NamingPrefix::Spread(token_range) => {
435 if token_range.is_some() {
436 result.push_str("... ");
437 }
438 }
439 };
440
441 let pretty_path = self.path.pretty_print_with_indentation(inline, indentation);
445 result.push_str(pretty_path.trim_start());
446
447 result
448 }
449}
450
451impl PrettyPrintable for Alias {
452 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
453 let mut result = String::new();
454
455 let name = self.name.pretty_print_with_indentation(inline, indentation);
456 result.push_str(name.as_str());
457 result.push(':');
458
459 result
460 }
461}
462
463impl PrettyPrintable for Key {
464 fn pretty_print_with_indentation(&self, _inline: bool, _indentation: usize) -> String {
465 match self {
466 Self::Field(name) => name.clone(),
467 Self::Quoted(name) => serde_json_bytes::Value::String(name.as_str().into()).to_string(),
468 }
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use crate::connectors::JSONSelection;
475 use crate::connectors::PathSelection;
476 use crate::connectors::SubSelection;
477 use crate::connectors::json_selection::NamedSelection;
478 use crate::connectors::json_selection::PrettyPrintable;
479 use crate::connectors::json_selection::location::new_span;
480 use crate::connectors::json_selection::location::new_span_with_spec;
481 use crate::connectors::json_selection::pretty::indent_chars;
482 use crate::connectors::spec::ConnectSpec;
483 use crate::selection;
484
485 fn test_permutations(selection: impl PrettyPrintable, expected: &str) {
487 let indentation = 4;
488 let expected_indented = expected
489 .lines()
490 .map(|line| format!("{}{line}", indent_chars(indentation)))
491 .collect::<Vec<_>>()
492 .join("\n");
493 let expected_indented = expected_indented.trim_start();
494
495 let prettified = selection.pretty_print();
496 assert_eq!(
497 prettified, expected,
498 "pretty printing did not match: {prettified} != {expected}"
499 );
500
501 let prettified_inline = selection.pretty_print_with_indentation(true, indentation);
502 let expected_inline = collapse_spaces(expected);
503 assert_eq!(
504 prettified_inline.trim_start(),
505 expected_inline.trim_start(),
506 "pretty printing inline did not match: {prettified_inline} != {}",
507 expected_indented.trim_start()
508 );
509
510 let prettified_indented = selection.pretty_print_with_indentation(false, indentation);
511 assert_eq!(
512 prettified_indented, expected_indented,
513 "pretty printing indented did not match: {prettified_indented} != {expected_indented}"
514 );
515 }
516
517 fn collapse_spaces(s: impl Into<String>) -> String {
518 let pattern = regex::Regex::new(r"\s+").expect("valid regex");
519 pattern.replace_all(s.into().as_str(), " ").to_string()
520 }
521
522 #[test]
523 fn it_prints_a_named_selection() {
524 let selections = [
525 "cool",
527 "cool: beans",
528 "cool: beans {\n whoa\n}",
529 "cool: one.two.three",
531 r#"cool: "b e a n s""#,
533 "cool: \"b e a n s\" {\n a\n b\n}",
534 "cool: {\n a\n b\n}",
536 ];
537 for selection in selections {
538 let (unmatched, named_selection) = NamedSelection::parse(new_span(selection)).unwrap();
539 assert!(
540 unmatched.is_empty(),
541 "static named selection was not fully parsed: '{selection}' ({named_selection:?}) had unmatched '{unmatched}'"
542 );
543
544 test_permutations(named_selection, selection);
545 }
546 }
547
548 #[test]
549 fn it_prints_a_path_selection() {
550 let paths = [
554 "$.one.two.three",
556 "$this.a.b",
557 "$this.id.first {\n username\n}",
558 "$.first",
560 "a.b.c.d.e",
561 "one.two.three {\n a\n b\n}",
562 "$.single {\n x\n}",
563 "results->slice($(-1)->mul($args.suffixLength))",
564 "$(1234)->add($(5678)->mul(2))",
565 "$(true)->and($(false)->not)",
566 "$(12345678987654321)->div(111111111)->eq(111111111)",
567 "$(\"Product\")->slice(0, $(4)->mul(-1))->eq(\"Pro\")",
568 "$($args.unnecessary.parens)->eq(42)",
569 ];
570 for path in paths {
571 let (unmatched, path_selection) =
572 PathSelection::parse(new_span_with_spec(path, ConnectSpec::V0_2)).unwrap();
573 assert!(
574 unmatched.is_empty(),
575 "static path was not fully parsed: '{path}' ({path_selection:?}) had unmatched '{unmatched}'"
576 );
577
578 test_permutations(path_selection, path);
579 }
580 }
581
582 #[test]
583 fn it_prints_a_sub_selection() {
584 let sub = "{\n a\n b\n}";
585 let (unmatched, sub_selection) = SubSelection::parse(new_span(sub)).unwrap();
586 assert!(
587 unmatched.is_empty(),
588 "static path was not fully parsed: '{sub}' ({sub_selection:?}) had unmatched '{unmatched}'"
589 );
590
591 test_permutations(sub_selection, sub);
592 }
593
594 #[test]
595 fn it_prints_an_inline_path_with_subselection() {
596 let source = "before\nsome.path {\n inline\n me\n}\nafter";
602 let sel = JSONSelection::parse(source).unwrap();
603 test_permutations(sel, source);
604 }
605
606 #[test]
607 fn it_prints_a_nested_sub_selection() {
608 let sub = "{
609 a {
610 b {
611 c
612 }
613 }
614 }";
615 let sub_indented = "{\n a {\n b {\n c\n }\n }\n}";
616 let sub_super_indented = " {\n a {\n b {\n c\n }\n }\n }";
617
618 let (unmatched, sub_selection) = SubSelection::parse(new_span(sub)).unwrap();
619
620 assert!(
621 unmatched.is_empty(),
622 "static nested sub was not fully parsed: '{sub}' ({sub_selection:?}) had unmatched '{unmatched}'"
623 );
624
625 let pretty = sub_selection.pretty_print();
626 assert_eq!(
627 pretty, sub_indented,
628 "nested sub pretty printing did not match: {pretty} != {sub_indented}"
629 );
630
631 let pretty = sub_selection.pretty_print_with_indentation(false, 4);
632 assert_eq!(
633 pretty,
634 sub_super_indented.trim_start(),
635 "nested inline sub pretty printing did not match: {pretty} != {}",
636 sub_super_indented.trim_start()
637 );
638 }
639
640 #[test]
641 fn it_prints_root_selection() {
642 let root_selection = JSONSelection::parse("id name").unwrap();
643 test_permutations(root_selection, "id\nname");
644 }
645
646 fn assert_round_trip_v0_4(input: &str, canonical: &str) {
650 let first = selection!(input, ConnectSpec::V0_4);
651 let printed = first.pretty_print();
652 assert_eq!(
653 printed, canonical,
654 "V0_4 pretty-print of `{input}` gave `{printed}`, expected `{canonical}`",
655 );
656 let second = selection!(canonical, ConnectSpec::V0_4);
659 let second_printed = second.pretty_print();
660 assert_eq!(
661 printed, second_printed,
662 "re-parse of canonical V0_4 form did not reproduce the same AST",
663 );
664 }
665
666 #[test]
671 fn top_level_v0_4_number_literal() {
672 assert_round_trip_v0_4("$(1234)", "$(1234)");
673 }
674
675 #[test]
676 fn top_level_v0_4_negative_number_literal() {
677 assert_round_trip_v0_4("$(-1)", "$(-1)");
678 }
679
680 #[test]
681 fn top_level_v0_4_string_literal() {
682 assert_round_trip_v0_4(r#"$("hello")"#, r#"$("hello")"#);
683 }
684
685 #[test]
686 fn top_level_v0_4_boolean_literal() {
687 assert_round_trip_v0_4("$(true)", "$(true)");
688 }
689
690 #[test]
691 fn top_level_v0_4_null_literal() {
692 assert_round_trip_v0_4("$(null)", "$(null)");
693 }
694
695 #[test]
696 fn top_level_v0_4_array_literal() {
697 assert_round_trip_v0_4("$([1, 2, 3])", "$([1, 2, 3])");
698 }
699
700 #[test]
701 fn top_level_v0_4_number_with_method_chain() {
702 assert_round_trip_v0_4("$(1234)->add(1111)", "$(1234)->add(1111)");
703 }
704
705 #[test]
706 fn top_level_v0_4_negative_number_with_method_chain() {
707 assert_round_trip_v0_4("$(-1)->add(10)", "$(-1)->add(10)");
708 }
709
710 #[test]
711 fn top_level_v0_4_string_with_method_chain() {
712 assert_round_trip_v0_4(r#"$("abc")->first"#, r#"$("abc")->first"#);
713 }
714
715 #[test]
716 fn top_level_v0_4_array_with_method_chain() {
717 assert_round_trip_v0_4("$([1, 2, 3])->last", "$([1, 2, 3])->last");
718 }
719
720 #[test]
721 fn top_level_v0_4_object_literal_with_key_access() {
722 assert_round_trip_v0_4("$({ a: 1, b: 2 }.b)", "$({\n a: 1,\n b: 2\n}.b)");
726 }
727
728 #[test]
729 fn top_level_v0_4_operator_chain() {
730 assert_round_trip_v0_4(
731 r#"$($args.maybe ?? "fallback")"#,
732 r#"$($args.maybe ?? "fallback")"#,
733 );
734 }
735
736 #[test]
737 fn top_level_v0_4_nested_expr_path_preserves_wrappers() {
738 assert_round_trip_v0_4("$($(-1)->add($(10)))", "$($(-1)->add($(10)))");
742 }
743
744 #[test]
745 fn top_level_v0_4_bare_single_key_still_named() {
746 let sel = selection!("author", ConnectSpec::V0_4);
750 assert_eq!(sel.pretty_print(), "author");
751 assert!(
752 sel.next_subselection().is_some(),
753 "top-level `author` should remain a NamedSelectionList wrapping a SubSelection",
754 );
755 }
756
757 #[test]
758 fn it_reprints_shorthand_properties() {
759 let expected = r#"
760upc
761... category->match(
762 ["book", {
763 __typename: "Book",
764 title,
765 author {
766 id
767 }
768 }],
769 ["film", $ {
770 __typename: "Film"
771 title
772 director {
773 id
774 }
775 }],
776 [@, null]
777)"#
778 .trim_start();
779
780 let sel = selection!(&expected, ConnectSpec::V0_4);
781 crate::assert_debug_snapshot!(&sel);
782
783 test_permutations(sel, expected);
784 }
785}