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                    let pretty_tail =
190                        print_flexible_wrapper("List<", [print(tail, depth + 1)], ">", depth);
191
192                    if pretty_prefix.is_empty() {
193                        // If there is no prefix, then the tail is the whole
194                        // list, so we can return pretty_tail as-is.
195                        return pretty_tail;
196                    }
197
198                    // If the List<S> notation appears at the end of an array
199                    // with a non-empty prefix, use ...List<S> to indicate it is
200                    // not just a single element, but governs the whole rest of
201                    // the array.
202                    pretty_prefix.push(format!("...{pretty_tail}"));
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                if !rest.is_none() {
221                    let pretty_rest =
222                        print_flexible_wrapper("Dict<", [print(rest, depth + 1)], ">", depth);
223
224                    if pretty_properties.is_empty() {
225                        // If there are no fields, then the rest is the whole
226                        // object, so we can return pretty_rest as-is.
227                        return pretty_rest;
228                    }
229
230                    // If the Dict<S> notation appears at the end of an object
231                    // with a non-empty fields, use ...Dict<S> to indicate it is
232                    // not just a single property, but governs all dynamic
233                    // fields of the object.
234                    pretty_properties.push(format!("...{pretty_rest}"));
235                }
236
237                print_flexible_wrapper(
238                    // If the object is printed as a single line, the
239                    // opening and closing braces will have this extra space
240                    // of padding. If the object is broken onto multiple
241                    // lines, this extra padding will be removed.
242                    "{ ",
243                    pretty_properties,
244                    " }",
245                    depth,
246                )
247            }
248
249            ShapeCase::One(shapes) => {
250                let pretty = print_flexible_wrapper(
251                    "One<",
252                    shapes
253                        .iter()
254                        .map(|child_shape| print(child_shape, depth + 1)),
255                    ">",
256                    depth,
257                );
258
259                // The empty `One<>` union represents an unsatisfiable shape
260                // like TypeScript's `never` type, so that's how we print it.
261                if pretty == "One<>" {
262                    "Never".to_string()
263                } else {
264                    pretty
265                }
266            }
267
268            ShapeCase::All(shapes) => print_flexible_wrapper(
269                "All<",
270                shapes
271                    .iter()
272                    .map(|child_shape| print(child_shape, depth + 1)),
273                ">",
274                depth,
275            ),
276
277            ShapeCase::Name(name, _weak) => name.to_string(),
278        }
279    }
280}
281
282impl Debug for Shape {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        write!(f, "Shape({})", self.pretty_print_with_names())
285    }
286}
287
288impl Display for Shape {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        write!(f, "{}", self.pretty_print())
291    }
292}