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                    // Condense the tail indicator so the structural prefix
190                    // stays readable. `...` alone marks the common
191                    // open-`Unknown` tail; otherwise prepend `...` to the
192                    // tail's own pretty-print, which already carries any
193                    // errors / names the tail shape has. A prefix-free open
194                    // array (formerly the canonical `List<S>` form) thus
195                    // renders as `[...S]`, or just `[...]` when S is Unknown.
196                    let pretty_tail_inner = print(tail, depth + 1);
197                    let rest_marker = if pretty_tail_inner == "Unknown" {
198                        "...".to_string()
199                    } else {
200                        format!("...{pretty_tail_inner}")
201                    };
202                    pretty_prefix.push(rest_marker);
203                }
204
205                print_flexible_wrapper("[", pretty_prefix, "]", depth)
206            }
207
208            ShapeCase::Object { fields, rest } => {
209                let mut sorted_properties = fields
210                    .iter()
211                    .map(|(key, value)| (key, print(value, depth + 1)))
212                    .collect::<Vec<_>>();
213                sorted_properties.sort_by_key(|(key, _)| key.as_str());
214
215                let mut pretty_properties = sorted_properties
216                    .into_iter()
217                    .map(|(key, value)| format!("{}: {value}", quote_non_identifier(key)))
218                    .collect::<Vec<_>>();
219
220                // Named fields get padded braces (`{ a: Int }`); a bare
221                // rest-only object prints tight (`{...S}`), so remember
222                // whether any real fields were present before we push the
223                // rest marker below.
224                let has_fields = !pretty_properties.is_empty();
225
226                if !rest.is_none() {
227                    // Condense the rest indicator so the structural fields
228                    // stay readable. `...` alone marks the common
229                    // open-`Unknown` rest; otherwise prepend `...` to the
230                    // rest's own pretty-print, which already carries any
231                    // errors / names the rest shape has. A field-free open
232                    // object (formerly the canonical `Dict<S>` form) thus
233                    // renders as `{...S}`, or just `{...}` when S is Unknown.
234                    let pretty_rest_inner = print(rest, depth + 1);
235                    let rest_marker = if pretty_rest_inner == "Unknown" {
236                        "...".to_string()
237                    } else {
238                        format!("...{pretty_rest_inner}")
239                    };
240                    pretty_properties.push(rest_marker);
241                }
242
243                // When there are named fields and the object prints on a
244                // single line, the opening and closing braces carry this
245                // extra space of padding. If the object is broken onto
246                // multiple lines, `print_flexible_wrapper` trims it. A bare
247                // rest-only object (`{...S}`) uses tight braces with no
248                // padding.
249                let (prefix, suffix) = if has_fields { ("{ ", " }") } else { ("{", "}") };
250
251                print_flexible_wrapper(prefix, pretty_properties, suffix, depth)
252            }
253
254            ShapeCase::One(shapes) => {
255                let pretty = print_flexible_wrapper(
256                    "One<",
257                    shapes
258                        .iter()
259                        .map(|child_shape| print(child_shape, depth + 1)),
260                    ">",
261                    depth,
262                );
263
264                // The empty `One<>` union represents an unsatisfiable shape
265                // like TypeScript's `never` type, so that's how we print it.
266                if pretty == "One<>" {
267                    "Never".to_string()
268                } else {
269                    pretty
270                }
271            }
272
273            ShapeCase::All(shapes) => print_flexible_wrapper(
274                "All<",
275                shapes
276                    .iter()
277                    .map(|child_shape| print(child_shape, depth + 1)),
278                ">",
279                depth,
280            ),
281
282            ShapeCase::Name(name, _weak) => name.to_string(),
283        }
284    }
285}
286
287impl Debug for Shape {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        write!(f, "Shape({})", self.pretty_print_with_names())
290    }
291}
292
293impl Display for Shape {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        write!(f, "{}", self.pretty_print())
296    }
297}