Skip to main content

apollo_federation/connectors/json_selection/
pretty.rs

1//! Pretty printing utility methods
2//!
3//! Working with raw JSONSelections when doing snapshot testing is difficult to
4//! read and makes the snapshots themselves quite large. This module adds a new
5//! pretty printing trait which is then implemented on the various sub types
6//! of the JSONSelection tree.
7
8use itertools::Itertools;
9
10use super::lit_expr::LitExpr;
11use super::lit_expr::LitOp;
12use super::parser::Alias;
13use super::parser::Key;
14use crate::connectors::json_selection::JSONSelection;
15use crate::connectors::json_selection::MethodArgs;
16use crate::connectors::json_selection::NamedSelection;
17use crate::connectors::json_selection::NamingPrefix;
18use crate::connectors::json_selection::PathList;
19use crate::connectors::json_selection::PathSelection;
20use crate::connectors::json_selection::SubSelection;
21use crate::connectors::json_selection::TopLevelSelection;
22
23impl std::fmt::Display for JSONSelection {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.write_str(&self.pretty_print())
26    }
27}
28
29/// Pretty print trait
30///
31/// This trait marks a type as supporting pretty printing itself outside of a
32/// Display implementation, which might be more useful for snapshots.
33pub(crate) trait PrettyPrintable {
34    /// Pretty print the struct
35    fn pretty_print(&self) -> String {
36        self.pretty_print_with_indentation(false, 0)
37    }
38
39    /// Pretty print the struct, with indentation
40    ///
41    /// Each indentation level is marked with 2 spaces, with `inline` signifying
42    /// that the first line should be not indented.
43    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String;
44}
45
46/// Helper method to generate indentation
47fn indent_chars(indent: usize) -> String {
48    "  ".repeat(indent)
49}
50
51impl PrettyPrintable for JSONSelection {
52    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
53        match &self.inner {
54            TopLevelSelection::Named(named) => named.print_subselections(inline, indentation),
55            TopLevelSelection::Path(path) => {
56                path.pretty_print_with_indentation(inline, indentation)
57            }
58        }
59    }
60}
61
62impl PrettyPrintable for SubSelection {
63    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
64        let mut result = String::new();
65
66        result.push('{');
67
68        if self.selections.is_empty() {
69            result.push('}');
70            return result;
71        }
72
73        if inline {
74            result.push(' ');
75        } else {
76            result.push('\n');
77            result.push_str(indent_chars(indentation + 1).as_str());
78        }
79
80        result.push_str(&self.print_subselections(inline, indentation + 1));
81
82        if inline {
83            result.push(' ');
84        } else {
85            result.push('\n');
86            result.push_str(indent_chars(indentation).as_str());
87        }
88
89        result.push('}');
90
91        result
92    }
93}
94
95impl SubSelection {
96    /// Prints all of the selections in a subselection
97    fn print_subselections(&self, inline: bool, indentation: usize) -> String {
98        let separator = if inline {
99            ' '.to_string()
100        } else {
101            format!("\n{}", indent_chars(indentation))
102        };
103
104        self.selections
105            .iter()
106            .map(|s| s.pretty_print_with_indentation(inline, indentation))
107            .join(separator.as_str())
108    }
109}
110
111impl PrettyPrintable for PathSelection {
112    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
113        let inner = self.path.pretty_print_with_indentation(inline, indentation);
114        // Because we can't tell where PathList::Key elements appear in the path
115        // once we're inside PathList::pretty_print_with_indentation, we print
116        // all PathList::Key elements with a leading '.' character, but we
117        // remove the initial '.' if the path has more than one element, because
118        // then the leading '.' is not necessary to disambiguate the key from a
119        // field. To complicate matters further, inner may begin with spaces due
120        // to indentation.
121        let leading_space_count = inner.chars().take_while(|c| *c == ' ').count();
122        let suffix = inner[leading_space_count..].to_string();
123        if let Some(after_dot) = suffix.strip_prefix('.') {
124            // Strip the '.' but keep any leading spaces.
125            format!("{}{}", " ".repeat(leading_space_count), after_dot)
126        } else {
127            inner
128        }
129    }
130}
131
132impl PrettyPrintable for PathList {
133    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
134        let mut result = String::new();
135
136        match self {
137            Self::Var(var, tail) => {
138                let rest = tail.pretty_print_with_indentation(inline, indentation);
139                result.push_str(var.as_str());
140                result.push_str(rest.as_str());
141            }
142            Self::Key(key, tail) => {
143                result.push('.');
144                result.push_str(key.pretty_print().as_str());
145                let rest = tail.pretty_print_with_indentation(inline, indentation);
146                result.push_str(rest.as_str());
147            }
148            Self::Expr(expr, tail) => {
149                let rest = tail.pretty_print_with_indentation(inline, indentation);
150                result.push_str("$(");
151                result.push_str(
152                    expr.pretty_print_with_indentation(inline, indentation)
153                        .as_str(),
154                );
155                result.push(')');
156                result.push_str(rest.as_str());
157            }
158            Self::Method(method, args, tail) => {
159                result.push_str("->");
160                result.push_str(method.as_str());
161                if let Some(args) = args {
162                    result.push_str(
163                        args.pretty_print_with_indentation(inline, indentation)
164                            .as_str(),
165                    );
166                }
167                result.push_str(
168                    tail.pretty_print_with_indentation(inline, indentation)
169                        .as_str(),
170                );
171            }
172            Self::Question(tail) => {
173                result.push('?');
174                let rest = tail.pretty_print_with_indentation(true, indentation);
175                result.push_str(rest.as_str());
176            }
177            Self::Selection(sub) => {
178                let sub = sub.pretty_print_with_indentation(inline, indentation);
179                result.push(' ');
180                result.push_str(sub.as_str());
181            }
182            Self::Empty => {}
183        }
184
185        result
186    }
187}
188
189impl PrettyPrintable for MethodArgs {
190    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
191        let mut result = String::new();
192
193        result.push('(');
194
195        // TODO Break long argument lists across multiple lines, with indentation?
196        for (i, arg) in self.args.iter().enumerate() {
197            if i > 0 {
198                result.push_str(", ");
199            }
200            result.push_str(
201                arg.pretty_print_with_indentation(inline, indentation + 1)
202                    .as_str(),
203            );
204        }
205
206        result.push(')');
207
208        result
209    }
210}
211
212impl PrettyPrintable for LitExpr {
213    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
214        let mut result = String::new();
215
216        match self {
217            Self::String(s) => {
218                let safely_quoted = serde_json_bytes::Value::String(s.clone().into()).to_string();
219                result.push_str(safely_quoted.as_str());
220            }
221            Self::Number(n) => result.push_str(n.to_string().as_str()),
222            Self::Bool(b) => result.push_str(b.to_string().as_str()),
223            Self::Null => result.push_str("null"),
224            Self::Object(map) => {
225                result.push('{');
226
227                if map.is_empty() {
228                    result.push('}');
229                    return result;
230                }
231
232                let mut is_first = true;
233                for (key, value) in map {
234                    if is_first {
235                        is_first = false;
236                    } else {
237                        result.push(',');
238                    }
239
240                    if inline {
241                        result.push(' ');
242                    } else {
243                        result.push('\n');
244                        result.push_str(indent_chars(indentation + 1).as_str());
245                    }
246
247                    result.push_str(key.pretty_print().as_str());
248                    result.push_str(": ");
249                    result.push_str(
250                        value
251                            .pretty_print_with_indentation(inline, indentation + 1)
252                            .as_str(),
253                    );
254                }
255
256                if inline {
257                    result.push(' ');
258                } else {
259                    result.push('\n');
260                    result.push_str(indent_chars(indentation).as_str());
261                }
262
263                result.push('}');
264            }
265            Self::Array(vec) => {
266                result.push('[');
267                let mut is_first = true;
268                for value in vec {
269                    if is_first {
270                        is_first = false;
271                    } else {
272                        result.push_str(", ");
273                    }
274                    result.push_str(
275                        value
276                            .pretty_print_with_indentation(inline, indentation)
277                            .as_str(),
278                    );
279                }
280                result.push(']');
281            }
282            Self::Path(path) => {
283                result.push_str(
284                    path.pretty_print_with_indentation(inline, indentation)
285                        .as_str(),
286                );
287            }
288            Self::LitPath(literal, subpath) => {
289                result.push_str(
290                    literal
291                        .pretty_print_with_indentation(inline, indentation)
292                        .as_str(),
293                );
294                result.push_str(
295                    subpath
296                        .pretty_print_with_indentation(inline, indentation)
297                        .as_str(),
298                );
299            }
300            Self::OpChain(op, operands) => {
301                let op_str = match op.as_ref() {
302                    LitOp::NullishCoalescing => " ?? ",
303                    LitOp::NoneCoalescing => " ?! ",
304                };
305
306                for (i, operand) in operands.iter().enumerate() {
307                    if i > 0 {
308                        result.push_str(op_str);
309                    }
310                    result.push_str(
311                        operand
312                            .pretty_print_with_indentation(inline, indentation)
313                            .as_str(),
314                    );
315                }
316            }
317        }
318
319        result
320    }
321}
322
323impl PrettyPrintable for NamedSelection {
324    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
325        let mut result = String::new();
326
327        match &self.prefix {
328            NamingPrefix::None => {}
329            NamingPrefix::Alias(alias) => {
330                result.push_str(alias.pretty_print().as_str());
331                result.push(' ');
332            }
333            NamingPrefix::Spread(token_range) => {
334                if token_range.is_some() {
335                    result.push_str("... ");
336                }
337            }
338        };
339
340        // The .trim_start() handles the case when self.path is just a
341        // SubSelection (i.e., a NamedGroupSelection), since that PathList
342        // variant typically prints a single leading space.
343        let pretty_path = self.path.pretty_print_with_indentation(inline, indentation);
344        result.push_str(pretty_path.trim_start());
345
346        result
347    }
348}
349
350impl PrettyPrintable for Alias {
351    fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String {
352        let mut result = String::new();
353
354        let name = self.name.pretty_print_with_indentation(inline, indentation);
355        result.push_str(name.as_str());
356        result.push(':');
357
358        result
359    }
360}
361
362impl PrettyPrintable for Key {
363    fn pretty_print_with_indentation(&self, _inline: bool, _indentation: usize) -> String {
364        match self {
365            Self::Field(name) => name.clone(),
366            Self::Quoted(name) => serde_json_bytes::Value::String(name.as_str().into()).to_string(),
367        }
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use crate::connectors::JSONSelection;
374    use crate::connectors::PathSelection;
375    use crate::connectors::SubSelection;
376    use crate::connectors::json_selection::NamedSelection;
377    use crate::connectors::json_selection::PrettyPrintable;
378    use crate::connectors::json_selection::location::new_span;
379    use crate::connectors::json_selection::pretty::indent_chars;
380
381    // Test all valid pretty print permutations
382    fn test_permutations(selection: impl PrettyPrintable, expected: &str) {
383        let indentation = 4;
384        let expected_indented = expected
385            .lines()
386            .map(|line| format!("{}{line}", indent_chars(indentation)))
387            .collect::<Vec<_>>()
388            .join("\n");
389        let expected_indented = expected_indented.trim_start();
390
391        let prettified = selection.pretty_print();
392        assert_eq!(
393            prettified, expected,
394            "pretty printing did not match: {prettified} != {expected}"
395        );
396
397        let prettified_inline = selection.pretty_print_with_indentation(true, indentation);
398        let expected_inline = collapse_spaces(expected);
399        assert_eq!(
400            prettified_inline.trim_start(),
401            expected_inline.trim_start(),
402            "pretty printing inline did not match: {prettified_inline} != {}",
403            expected_indented.trim_start()
404        );
405
406        let prettified_indented = selection.pretty_print_with_indentation(false, indentation);
407        assert_eq!(
408            prettified_indented, expected_indented,
409            "pretty printing indented did not match: {prettified_indented} != {expected_indented}"
410        );
411    }
412
413    fn collapse_spaces(s: impl Into<String>) -> String {
414        let pattern = regex::Regex::new(r"\s+").expect("valid regex");
415        pattern.replace_all(s.into().as_str(), " ").to_string()
416    }
417
418    #[test]
419    fn it_prints_a_named_selection() {
420        let selections = [
421            // Field
422            "cool",
423            "cool: beans",
424            "cool: beans {\n  whoa\n}",
425            // Path
426            "cool: one.two.three",
427            // Quoted
428            r#"cool: "b e a n s""#,
429            "cool: \"b e a n s\" {\n  a\n  b\n}",
430            // Group
431            "cool: {\n  a\n  b\n}",
432        ];
433        for selection in selections {
434            let (unmatched, named_selection) = NamedSelection::parse(new_span(selection)).unwrap();
435            assert!(
436                unmatched.is_empty(),
437                "static named selection was not fully parsed: '{selection}' ({named_selection:?}) had unmatched '{unmatched}'"
438            );
439
440            test_permutations(named_selection, selection);
441        }
442    }
443
444    #[test]
445    fn it_prints_a_path_selection() {
446        let paths = [
447            // Var
448            "$.one.two.three",
449            "$this.a.b",
450            "$this.id.first {\n  username\n}",
451            // Key
452            "$.first",
453            "a.b.c.d.e",
454            "one.two.three {\n  a\n  b\n}",
455            "$.single {\n  x\n}",
456            "results->slice($(-1)->mul($args.suffixLength))",
457            "$(1234)->add($(5678)->mul(2))",
458            "$(true)->and($(false)->not)",
459            "$(12345678987654321)->div(111111111)->eq(111111111)",
460            "$(\"Product\")->slice(0, $(4)->mul(-1))->eq(\"Pro\")",
461            "$($args.unnecessary.parens)->eq(42)",
462        ];
463        for path in paths {
464            let (unmatched, path_selection) = PathSelection::parse(new_span(path)).unwrap();
465            assert!(
466                unmatched.is_empty(),
467                "static path was not fully parsed: '{path}' ({path_selection:?}) had unmatched '{unmatched}'"
468            );
469
470            test_permutations(path_selection, path);
471        }
472    }
473
474    #[test]
475    fn it_prints_a_sub_selection() {
476        let sub = "{\n  a\n  b\n}";
477        let (unmatched, sub_selection) = SubSelection::parse(new_span(sub)).unwrap();
478        assert!(
479            unmatched.is_empty(),
480            "static path was not fully parsed: '{sub}' ({sub_selection:?}) had unmatched '{unmatched}'"
481        );
482
483        test_permutations(sub_selection, sub);
484    }
485
486    #[test]
487    fn it_prints_an_inline_path_with_subselection() {
488        // This test ensures we do not print a leading ... before some.path,
489        // even though we will probably want to do so once ... and conditional
490        // selections are implemented. The printing of the leading ... was due
491        // to an incomplete removal of experimental support for conditional
492        // selections, which was put on hold to de-risk the GA release.
493        let source = "before\nsome.path {\n  inline\n  me\n}\nafter";
494        let sel = JSONSelection::parse(source).unwrap();
495        test_permutations(sel, source);
496    }
497
498    #[test]
499    fn it_prints_a_nested_sub_selection() {
500        let sub = "{
501          a {
502            b {
503              c
504            }
505          }
506        }";
507        let sub_indented = "{\n  a {\n    b {\n      c\n    }\n  }\n}";
508        let sub_super_indented = "        {\n          a {\n            b {\n              c\n            }\n          }\n        }";
509
510        let (unmatched, sub_selection) = SubSelection::parse(new_span(sub)).unwrap();
511
512        assert!(
513            unmatched.is_empty(),
514            "static nested sub was not fully parsed: '{sub}' ({sub_selection:?}) had unmatched '{unmatched}'"
515        );
516
517        let pretty = sub_selection.pretty_print();
518        assert_eq!(
519            pretty, sub_indented,
520            "nested sub pretty printing did not match: {pretty} != {sub_indented}"
521        );
522
523        let pretty = sub_selection.pretty_print_with_indentation(false, 4);
524        assert_eq!(
525            pretty,
526            sub_super_indented.trim_start(),
527            "nested inline sub pretty printing did not match: {pretty} != {}",
528            sub_super_indented.trim_start()
529        );
530    }
531
532    #[test]
533    fn it_prints_root_selection() {
534        let root_selection = JSONSelection::parse("id name").unwrap();
535        test_permutations(root_selection, "id\nname");
536    }
537}