Skip to main content

shape/
display.rs

1use std::fmt::Debug;
2use std::fmt::Display;
3use std::fmt::Write as _;
4
5use super::helpers::quote_string;
6use super::Shape;
7use super::ShapeCase;
8use crate::helpers::quote_non_identifier;
9use crate::name::Name;
10
11const UNIT_OF_INDENTATION: &str = "  ";
12const WRAP_COLUMN: usize = 60;
13
14impl Shape {
15    /// Returns a string representation of the [`Shape`], including any attached
16    /// errors as `(err "message")` suffix.
17    ///
18    /// Please note: this display format does not imply an input syntax or
19    /// parser for the shape language. To create new [`Shape`] elements, use the
20    /// various `Shape::*` helper functions.
21    #[must_use]
22    pub fn pretty_print(&self) -> String {
23        self.pretty_print_at_depth(0)
24    }
25
26    fn pretty_print_at_depth(&self, depth: usize) -> String {
27        let pretty_shape = self.pretty_print_recursive(depth, Shape::pretty_print_at_depth);
28        self.append_error_suffix(pretty_shape)
29    }
30
31    /// Returns a string representation of the [`Shape`] without any error
32    /// annotations. This is the most concise representation.
33    #[must_use]
34    pub fn pretty_print_without_errors(&self) -> String {
35        self.pretty_print_without_errors_at_depth(0)
36    }
37
38    fn pretty_print_without_errors_at_depth(&self, depth: usize) -> String {
39        self.pretty_print_recursive(depth, Shape::pretty_print_without_errors_at_depth)
40    }
41
42    /// Returns a string representation of the [`Shape`] with [`Name`] metadata
43    /// included as `<shape> (aka <name1>, <name2>)` and error metadata as
44    /// `(err "message")`. Errors are shown first, then names.
45    #[must_use]
46    pub fn pretty_print_with_names(&self) -> String {
47        self.pretty_print_with_names_at_depth(0)
48    }
49
50    fn pretty_print_with_names_at_depth(&self, depth: usize) -> String {
51        let pretty_shape =
52            self.pretty_print_recursive(depth, Shape::pretty_print_with_names_at_depth);
53
54        let mut result = pretty_shape;
55
56        // Append errors first
57        result = self.append_error_suffix(result);
58
59        // Then append names
60        if self.names().count() > 0 {
61            let joined = self
62                .names()
63                .map(Name::to_string)
64                .collect::<Vec<_>>()
65                .join(", ");
66            // Since the (aka ...) annotation trails multiline array and object
67            // shapes, it often fits neatly into the otherwise mostly empty line
68            // just after the closing ] or }.
69            result = format!("{result} (aka {joined})");
70        }
71
72        result
73    }
74
75    /// Helper to append `(err "msg1", "msg2")` suffix if shape has its own errors.
76    fn append_error_suffix(&self, pretty_shape: String) -> String {
77        let error_messages: Vec<_> = self
78            .own_errors()
79            .map(|e| quote_string(&e.message))
80            .collect();
81        if error_messages.is_empty() {
82            return pretty_shape;
83        }
84        format!("{pretty_shape} (err {})", error_messages.join(", "))
85    }
86
87    #[allow(clippy::too_many_lines)]
88    fn pretty_print_recursive(
89        &self,
90        depth: usize,
91        print: impl Fn(&Shape, usize) -> String,
92    ) -> String {
93        fn print_flexible_wrapper(
94            prefix: &str,
95            pretty_children: impl IntoIterator<Item = String>,
96            suffix: &str,
97            depth: usize,
98        ) -> String {
99            let pretty_children = pretty_children.into_iter().collect::<Vec<_>>();
100
101            // We may want to print the one-line version with some padding
102            // inside the prefix/suffix, but we don't want that for the
103            // multiline version, so we strip any padding before proceeding.
104            let trimmed_prefix = prefix.trim();
105            let trimmed_suffix = suffix.trim();
106
107            if pretty_children.is_empty() {
108                // If prefix was "{ " and suffix was " }", this prints "{}"
109                // without the spaces when pretty_children is empty.
110                return format!("{trimmed_prefix}{trimmed_suffix}");
111            }
112
113            if let (1, Some(only)) = (pretty_children.len(), pretty_children.first()) {
114                // If there is only one child, always print it without
115                // leading/trailing line breaks (though `prefix` and `suffix`
116                // may include padding spaces, and `only` may print with its own
117                // internal line breaks).
118                return format!("{prefix}{only}{suffix}");
119            }
120
121            if pretty_children
122                .iter()
123                .all(|pretty| pretty.lines().count() <= 1)
124            {
125                // If all the children are single-line, and they fit within
126                // WRAP_COLUMN columns, we may be able to print them all on one
127                // line. Note we are using the untrimmed prefix and suffix,
128                // since this is a place where padding spaces may be desired.
129                //
130                // By corollary, if the shape has any multiline children, the
131                // whole shape needs to be printed with line breaks. We never
132                // want to print mixed inline/multiline expressions, as in
133                //
134                //   [a, {
135                //     blah: 1
136                //   }, b, c, {
137                //     whatever: true
138                //   }]
139                //
140                // A human might be able to make this style readable, but
141                // machine formatters that allow this hybrid line style tend to
142                // produce unreadable monstrosities.
143                let one_line = format!("{prefix}{}{suffix}", pretty_children.join(", "));
144                if one_line.len() <= WRAP_COLUMN {
145                    return one_line;
146                }
147                // If trying to print all the children on one line would exceed
148                // WRAP_COLUMN columns, proceed to multi-line printing...
149            }
150
151            let mut result = format!("{trimmed_prefix}\n");
152            let child_indent = UNIT_OF_INDENTATION.repeat(depth + 1);
153
154            for pretty in &pretty_children {
155                let _ = writeln!(result, "{child_indent}{pretty},");
156            }
157
158            let suffix_indent = UNIT_OF_INDENTATION.repeat(depth);
159            let _ = write!(result, "{suffix_indent}{trimmed_suffix}");
160
161            result
162        }
163
164        match self.case() {
165            ShapeCase::Bool(Some(b)) => b.to_string(),
166            ShapeCase::Bool(None) => "Bool".to_string(),
167            ShapeCase::String(Some(s)) => quote_string(s.as_str()),
168            ShapeCase::String(None) => "String".to_string(),
169            ShapeCase::Int(Some(i)) => i.to_string(),
170            ShapeCase::Int(None) => "Int".to_string(),
171            ShapeCase::Float => "Float".to_string(),
172            ShapeCase::Null => "null".to_string(), // No typo: JSON null is lowercase.
173
174            ShapeCase::Unknown => "Unknown".to_string(),
175
176            // There may be some argument for using lower-case "none" here,
177            // since None is not a reserved/built-in GraphQL type name like
178            // Bool, String, Int, and Float are, so someone could define a
179            // custom GraphQL None type that would collide with this Shape name.
180            ShapeCase::None => "None".to_string(),
181
182            ShapeCase::Array { prefix, tail } => {
183                let mut pretty_prefix = prefix
184                    .iter()
185                    .map(|shape| print(shape, depth + 1))
186                    .collect::<Vec<_>>();
187
188                if !tail.is_none() {
189                    if pretty_prefix.is_empty() {
190                        // No prefix to suppress: render the whole array using
191                        // the canonical `List<S>` form.
192                        return print_flexible_wrapper(
193                            "List<",
194                            [print(tail, depth + 1)],
195                            ">",
196                            depth,
197                        );
198                    }
199
200                    // Non-empty prefix: condense the tail indicator so the
201                    // structural prefix stays readable. `...` alone marks the
202                    // common open-`Unknown` tail; otherwise prepend `...` to
203                    // the tail's own pretty-print, which already carries any
204                    // errors / names the tail shape has.
205                    let pretty_tail_inner = print(tail, depth + 1);
206                    let rest_marker = if pretty_tail_inner == "Unknown" {
207                        "...".to_string()
208                    } else {
209                        format!("...{pretty_tail_inner}")
210                    };
211                    pretty_prefix.push(rest_marker);
212                }
213
214                print_flexible_wrapper("[", pretty_prefix, "]", depth)
215            }
216
217            ShapeCase::Object { fields, rest } => {
218                let mut sorted_properties = fields
219                    .iter()
220                    .map(|(key, value)| (key, print(value, depth + 1)))
221                    .collect::<Vec<_>>();
222                sorted_properties.sort_by_key(|(key, _)| key.as_str());
223
224                let mut pretty_properties = sorted_properties
225                    .into_iter()
226                    .map(|(key, value)| format!("{}: {value}", quote_non_identifier(key)))
227                    .collect::<Vec<_>>();
228
229                if !rest.is_none() {
230                    if pretty_properties.is_empty() {
231                        // No fields to suppress: render the whole object using
232                        // the canonical `Dict<S>` form.
233                        return print_flexible_wrapper(
234                            "Dict<",
235                            [print(rest, depth + 1)],
236                            ">",
237                            depth,
238                        );
239                    }
240
241                    // Non-empty fields: condense the rest indicator so the
242                    // structural fields stay readable. `...` alone marks the
243                    // common open-`Unknown` rest; otherwise prepend `...` to
244                    // the rest's own pretty-print, which already carries any
245                    // errors / names the rest shape has.
246                    let pretty_rest_inner = print(rest, depth + 1);
247                    let rest_marker = if pretty_rest_inner == "Unknown" {
248                        "...".to_string()
249                    } else {
250                        format!("...{pretty_rest_inner}")
251                    };
252                    pretty_properties.push(rest_marker);
253                }
254
255                print_flexible_wrapper(
256                    // If the object is printed as a single line, the
257                    // opening and closing braces will have this extra space
258                    // of padding. If the object is broken onto multiple
259                    // lines, this extra padding will be removed.
260                    "{ ",
261                    pretty_properties,
262                    " }",
263                    depth,
264                )
265            }
266
267            ShapeCase::One(shapes) => {
268                let pretty = print_flexible_wrapper(
269                    "One<",
270                    shapes
271                        .iter()
272                        .map(|child_shape| print(child_shape, depth + 1)),
273                    ">",
274                    depth,
275                );
276
277                // The empty `One<>` union represents an unsatisfiable shape
278                // like TypeScript's `never` type, so that's how we print it.
279                if pretty == "One<>" {
280                    "Never".to_string()
281                } else {
282                    pretty
283                }
284            }
285
286            ShapeCase::All(shapes) => print_flexible_wrapper(
287                "All<",
288                shapes
289                    .iter()
290                    .map(|child_shape| print(child_shape, depth + 1)),
291                ">",
292                depth,
293            ),
294
295            ShapeCase::Name(name, _weak) => name.to_string(),
296        }
297    }
298}
299
300impl Debug for Shape {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        write!(f, "Shape({})", self.pretty_print_with_names())
303    }
304}
305
306impl Display for Shape {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        write!(f, "{}", self.pretty_print())
309    }
310}