apollo_federation/connectors/json_selection/
pretty.rs1use 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(&self.pretty_print())
27 }
28}
29
30pub(crate) trait PrettyPrintable {
35 fn pretty_print(&self) -> String {
37 self.pretty_print_with_indentation(false, 0)
38 }
39
40 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String;
45}
46
47fn indent_chars(indent: usize) -> String {
49 " ".repeat(indent)
50}
51
52impl PrettyPrintable for JSONSelection {
53 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
54 match &self.inner {
55 TopLevelSelection::Named(named) => named.print_subselections(inline, indentation),
56 TopLevelSelection::Path(path) => {
57 path.pretty_print_with_indentation(inline, indentation)
58 }
59 }
60 }
61}
62
63impl PrettyPrintable for SubSelection {
64 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
65 let mut result = String::new();
66
67 result.push('{');
68
69 if self.selections.is_empty() {
70 result.push('}');
71 return result;
72 }
73
74 if inline {
75 result.push(' ');
76 } else {
77 result.push('\n');
78 result.push_str(indent_chars(indentation + 1).as_str());
79 }
80
81 result.push_str(&self.print_subselections(inline, indentation + 1));
82
83 if inline {
84 result.push(' ');
85 } else {
86 result.push('\n');
87 result.push_str(indent_chars(indentation).as_str());
88 }
89
90 result.push('}');
91
92 result
93 }
94}
95
96impl SubSelection {
97 fn print_subselections(&self, inline: bool, indentation: usize) -> String {
99 let separator = if inline {
100 ' '.to_string()
101 } else {
102 format!("\n{}", indent_chars(indentation))
103 };
104
105 self.selections
106 .iter()
107 .map(|s| s.pretty_print_with_indentation(inline, indentation))
108 .join(separator.as_str())
109 }
110}
111
112impl PrettyPrintable for PathSelection {
113 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
114 let inner = self.path.pretty_print_with_indentation(inline, indentation);
115 let leading_space_count = inner.chars().take_while(|c| *c == ' ').count();
123 let suffix = inner[leading_space_count..].to_string();
124 if let Some(after_dot) = suffix.strip_prefix('.') {
125 format!("{}{}", " ".repeat(leading_space_count), after_dot)
127 } else {
128 inner
129 }
130 }
131}
132
133impl PrettyPrintable for PathList {
134 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
135 let mut result = String::new();
136
137 match self {
138 Self::Var(var, tail) => {
139 let rest = tail.pretty_print_with_indentation(inline, indentation);
140 result.push_str(var.as_str());
141 result.push_str(rest.as_str());
142 }
143 Self::Key(key, tail) => {
144 result.push('.');
145 result.push_str(key.pretty_print().as_str());
146 let rest = tail.pretty_print_with_indentation(inline, indentation);
147 result.push_str(rest.as_str());
148 }
149 Self::Expr(expr, tail) => {
150 let rest = tail.pretty_print_with_indentation(inline, indentation);
151 result.push_str("$(");
152 result.push_str(
153 expr.pretty_print_with_indentation(inline, indentation)
154 .as_str(),
155 );
156 result.push(')');
157 result.push_str(rest.as_str());
158 }
159 Self::Method(method, args, tail) => {
160 result.push_str("->");
161 result.push_str(method.as_str());
162 if let Some(args) = args {
163 result.push_str(
164 args.pretty_print_with_indentation(inline, indentation)
165 .as_str(),
166 );
167 }
168 result.push_str(
169 tail.pretty_print_with_indentation(inline, indentation)
170 .as_str(),
171 );
172 }
173 Self::Question(tail) => {
174 result.push('?');
175 let rest = tail.pretty_print_with_indentation(true, indentation);
176 result.push_str(rest.as_str());
177 }
178 Self::Selection(sub) => {
179 let sub = sub.pretty_print_with_indentation(inline, indentation);
180 result.push(' ');
181 result.push_str(sub.as_str());
182 }
183 Self::Empty => {}
184 }
185
186 result
187 }
188}
189
190impl PrettyPrintable for MethodArgs {
191 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
192 let printed_args: Vec<String> = self
193 .args
194 .iter()
195 .map(|arg| arg.pretty_print_with_indentation(inline, indentation + 1))
196 .collect();
197
198 let would_break = if inline {
202 self.args
203 .iter()
204 .any(|arg| arg.pretty_print_with_indentation(false, 0).contains('\n'))
205 } else {
206 printed_args.iter().any(|a| a.contains('\n'))
207 };
208
209 if !inline && would_break {
210 let indent = indent_chars(indentation + 1);
211 let separator = format!(",\n{indent}");
212 let joined = printed_args.iter().map(String::as_str).join(&separator);
213 format!("(\n{indent}{joined}\n{})", indent_chars(indentation))
214 } else if would_break {
215 let joined = printed_args.iter().map(String::as_str).join(", ");
216 format!("( {joined} )")
217 } else {
218 let joined = printed_args.iter().map(String::as_str).join(", ");
219 format!("({joined})")
220 }
221 }
222}
223
224impl LitExpr {
225 fn is_shorthand_property(key: &WithRange<Key>, value: &WithRange<LitExpr>) -> bool {
226 let Key::Field(key_name) = key.as_ref() else {
227 return false;
228 };
229 let LitExpr::Path(PathSelection { path }) = value.as_ref() else {
230 return false;
231 };
232 let PathList::Key(path_key, tail) = path.as_ref() else {
233 return false;
234 };
235 let tail_is_simple = match tail.as_ref() {
236 PathList::Empty => true,
237 PathList::Question(inner) => matches!(inner.as_ref(), PathList::Empty),
239 PathList::Selection(_) => true,
241 _ => false,
242 };
243 tail_is_simple && path_key.as_str() == key_name
244 }
245}
246
247impl PrettyPrintable for LitExpr {
248 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
249 let mut result = String::new();
250
251 match self {
252 Self::String(s) => {
253 let safely_quoted = serde_json_bytes::Value::String(s.clone().into()).to_string();
254 result.push_str(safely_quoted.as_str());
255 }
256 Self::Number(n) => result.push_str(n.to_string().as_str()),
257 Self::Bool(b) => result.push_str(b.to_string().as_str()),
258 Self::Null => result.push_str("null"),
259 Self::Object(map) => {
260 result.push('{');
261
262 if map.is_empty() {
263 result.push('}');
264 return result;
265 }
266
267 let mut is_first = true;
268 for (key, value) in map {
269 if is_first {
270 is_first = false;
271 } else {
272 result.push(',');
273 }
274
275 if inline {
276 result.push(' ');
277 } else {
278 result.push('\n');
279 result.push_str(indent_chars(indentation + 1).as_str());
280 }
281
282 if Self::is_shorthand_property(key, value) {
283 result.push_str(
286 value
287 .pretty_print_with_indentation(inline, indentation + 1)
288 .as_str(),
289 );
290 } else {
291 result.push_str(key.pretty_print().as_str());
292 result.push_str(": ");
293 result.push_str(
294 value
295 .pretty_print_with_indentation(inline, indentation + 1)
296 .as_str(),
297 );
298 }
299 }
300
301 if inline {
302 result.push(' ');
303 } else {
304 result.push('\n');
305 result.push_str(indent_chars(indentation).as_str());
306 }
307
308 result.push('}');
309 }
310 Self::Array(vec) => {
311 result.push('[');
312 let mut is_first = true;
313 for value in vec {
314 if is_first {
315 is_first = false;
316 } else {
317 result.push_str(", ");
318 }
319 result.push_str(
320 value
321 .pretty_print_with_indentation(inline, indentation)
322 .as_str(),
323 );
324 }
325 result.push(']');
326 }
327 Self::Path(path) => {
328 result.push_str(
329 path.pretty_print_with_indentation(inline, indentation)
330 .as_str(),
331 );
332 }
333 Self::LitPath(literal, subpath) => {
334 result.push_str(
335 literal
336 .pretty_print_with_indentation(inline, indentation)
337 .as_str(),
338 );
339 result.push_str(
340 subpath
341 .pretty_print_with_indentation(inline, indentation)
342 .as_str(),
343 );
344 }
345 Self::OpChain(op, operands) => {
346 let op_str = match op.as_ref() {
347 LitOp::NullishCoalescing => " ?? ",
348 LitOp::NoneCoalescing => " ?! ",
349 };
350
351 for (i, operand) in operands.iter().enumerate() {
352 if i > 0 {
353 result.push_str(op_str);
354 }
355 result.push_str(
356 operand
357 .pretty_print_with_indentation(inline, indentation)
358 .as_str(),
359 );
360 }
361 }
362 }
363
364 result
365 }
366}
367
368impl PrettyPrintable for NamedSelection {
369 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
370 let mut result = String::new();
371
372 match &self.prefix {
373 NamingPrefix::None => {}
374 NamingPrefix::Alias(alias) => {
375 result.push_str(alias.pretty_print().as_str());
376 result.push(' ');
377 }
378 NamingPrefix::Spread(token_range) => {
379 if token_range.is_some() {
380 result.push_str("... ");
381 }
382 }
383 };
384
385 let pretty_path = self.path.pretty_print_with_indentation(inline, indentation);
389 result.push_str(pretty_path.trim_start());
390
391 result
392 }
393}
394
395impl PrettyPrintable for Alias {
396 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
397 let mut result = String::new();
398
399 let name = self.name.pretty_print_with_indentation(inline, indentation);
400 result.push_str(name.as_str());
401 result.push(':');
402
403 result
404 }
405}
406
407impl PrettyPrintable for Key {
408 fn pretty_print_with_indentation(&self, _inline: bool, _indentation: usize) -> String {
409 match self {
410 Self::Field(name) => name.clone(),
411 Self::Quoted(name) => serde_json_bytes::Value::String(name.as_str().into()).to_string(),
412 }
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use crate::connectors::JSONSelection;
419 use crate::connectors::PathSelection;
420 use crate::connectors::SubSelection;
421 use crate::connectors::json_selection::NamedSelection;
422 use crate::connectors::json_selection::PrettyPrintable;
423 use crate::connectors::json_selection::location::new_span;
424 use crate::connectors::json_selection::pretty::indent_chars;
425 use crate::connectors::spec::ConnectSpec;
426 use crate::selection;
427
428 fn test_permutations(selection: impl PrettyPrintable, expected: &str) {
430 let indentation = 4;
431 let expected_indented = expected
432 .lines()
433 .map(|line| format!("{}{line}", indent_chars(indentation)))
434 .collect::<Vec<_>>()
435 .join("\n");
436 let expected_indented = expected_indented.trim_start();
437
438 let prettified = selection.pretty_print();
439 assert_eq!(
440 prettified, expected,
441 "pretty printing did not match: {prettified} != {expected}"
442 );
443
444 let prettified_inline = selection.pretty_print_with_indentation(true, indentation);
445 let expected_inline = collapse_spaces(expected);
446 assert_eq!(
447 prettified_inline.trim_start(),
448 expected_inline.trim_start(),
449 "pretty printing inline did not match: {prettified_inline} != {}",
450 expected_indented.trim_start()
451 );
452
453 let prettified_indented = selection.pretty_print_with_indentation(false, indentation);
454 assert_eq!(
455 prettified_indented, expected_indented,
456 "pretty printing indented did not match: {prettified_indented} != {expected_indented}"
457 );
458 }
459
460 fn collapse_spaces(s: impl Into<String>) -> String {
461 let pattern = regex::Regex::new(r"\s+").expect("valid regex");
462 pattern.replace_all(s.into().as_str(), " ").to_string()
463 }
464
465 #[test]
466 fn it_prints_a_named_selection() {
467 let selections = [
468 "cool",
470 "cool: beans",
471 "cool: beans {\n whoa\n}",
472 "cool: one.two.three",
474 r#"cool: "b e a n s""#,
476 "cool: \"b e a n s\" {\n a\n b\n}",
477 "cool: {\n a\n b\n}",
479 ];
480 for selection in selections {
481 let (unmatched, named_selection) = NamedSelection::parse(new_span(selection)).unwrap();
482 assert!(
483 unmatched.is_empty(),
484 "static named selection was not fully parsed: '{selection}' ({named_selection:?}) had unmatched '{unmatched}'"
485 );
486
487 test_permutations(named_selection, selection);
488 }
489 }
490
491 #[test]
492 fn it_prints_a_path_selection() {
493 let paths = [
494 "$.one.two.three",
496 "$this.a.b",
497 "$this.id.first {\n username\n}",
498 "$.first",
500 "a.b.c.d.e",
501 "one.two.three {\n a\n b\n}",
502 "$.single {\n x\n}",
503 "results->slice($(-1)->mul($args.suffixLength))",
504 "$(1234)->add($(5678)->mul(2))",
505 "$(true)->and($(false)->not)",
506 "$(12345678987654321)->div(111111111)->eq(111111111)",
507 "$(\"Product\")->slice(0, $(4)->mul(-1))->eq(\"Pro\")",
508 "$($args.unnecessary.parens)->eq(42)",
509 ];
510 for path in paths {
511 let (unmatched, path_selection) = PathSelection::parse(new_span(path)).unwrap();
512 assert!(
513 unmatched.is_empty(),
514 "static path was not fully parsed: '{path}' ({path_selection:?}) had unmatched '{unmatched}'"
515 );
516
517 test_permutations(path_selection, path);
518 }
519 }
520
521 #[test]
522 fn it_prints_a_sub_selection() {
523 let sub = "{\n a\n b\n}";
524 let (unmatched, sub_selection) = SubSelection::parse(new_span(sub)).unwrap();
525 assert!(
526 unmatched.is_empty(),
527 "static path was not fully parsed: '{sub}' ({sub_selection:?}) had unmatched '{unmatched}'"
528 );
529
530 test_permutations(sub_selection, sub);
531 }
532
533 #[test]
534 fn it_prints_an_inline_path_with_subselection() {
535 let source = "before\nsome.path {\n inline\n me\n}\nafter";
541 let sel = JSONSelection::parse(source).unwrap();
542 test_permutations(sel, source);
543 }
544
545 #[test]
546 fn it_prints_a_nested_sub_selection() {
547 let sub = "{
548 a {
549 b {
550 c
551 }
552 }
553 }";
554 let sub_indented = "{\n a {\n b {\n c\n }\n }\n}";
555 let sub_super_indented = " {\n a {\n b {\n c\n }\n }\n }";
556
557 let (unmatched, sub_selection) = SubSelection::parse(new_span(sub)).unwrap();
558
559 assert!(
560 unmatched.is_empty(),
561 "static nested sub was not fully parsed: '{sub}' ({sub_selection:?}) had unmatched '{unmatched}'"
562 );
563
564 let pretty = sub_selection.pretty_print();
565 assert_eq!(
566 pretty, sub_indented,
567 "nested sub pretty printing did not match: {pretty} != {sub_indented}"
568 );
569
570 let pretty = sub_selection.pretty_print_with_indentation(false, 4);
571 assert_eq!(
572 pretty,
573 sub_super_indented.trim_start(),
574 "nested inline sub pretty printing did not match: {pretty} != {}",
575 sub_super_indented.trim_start()
576 );
577 }
578
579 #[test]
580 fn it_prints_root_selection() {
581 let root_selection = JSONSelection::parse("id name").unwrap();
582 test_permutations(root_selection, "id\nname");
583 }
584
585 #[test]
586 fn it_reprints_shorthand_properties() {
587 let expected = r#"
588upc
589... category->match(
590 ["book", {
591 __typename: "Book",
592 title,
593 author {
594 id
595 }
596 }],
597 ["film", $ {
598 __typename: $("Film")
599 title
600 director {
601 id
602 }
603 }],
604 [@, null]
605)"#
606 .trim_start();
607
608 let sel = selection!(&expected, ConnectSpec::V0_4);
609 crate::assert_debug_snapshot!(&sel);
610
611 test_permutations(sel, expected);
612 }
613}