apollo_federation/connectors/json_selection/
pretty.rs1use 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
29pub(crate) trait PrettyPrintable {
34 fn pretty_print(&self) -> String {
36 self.pretty_print_with_indentation(false, 0)
37 }
38
39 fn pretty_print_with_indentation(&self, inline: bool, indentation: usize) -> String;
44}
45
46fn 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 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 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 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 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 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 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 "cool",
423 "cool: beans",
424 "cool: beans {\n whoa\n}",
425 "cool: one.two.three",
427 r#"cool: "b e a n s""#,
429 "cool: \"b e a n s\" {\n a\n b\n}",
430 "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 "$.one.two.three",
449 "$this.a.b",
450 "$this.id.first {\n username\n}",
451 "$.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 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}