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::case_enum::Error;
9use crate::helpers::quote_non_identifier;
10use crate::name::Name;
11
12const UNIT_OF_INDENTATION: &str = "  ";
13const WRAP_COLUMN: usize = 60;
14
15impl Shape {
16    /// Returns a string representation of the [`Shape`].
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        self.pretty_print_recursive(depth, Shape::pretty_print_at_depth)
28    }
29
30    /// Returns a string representation of the [`Shape`] with [`Name`] metadata
31    /// included as `<shape> (aka <name1>, <name2>)` instead of just `<shape>`
32    /// (which is what `pretty_print` would print). Note this extra verbosity
33    /// may lead to more aggressive line breaking.
34    #[must_use]
35    pub fn pretty_print_with_names(&self) -> String {
36        self.pretty_print_with_names_at_depth(0)
37    }
38
39    pub fn pretty_print_with_names_at_depth(&self, depth: usize) -> String {
40        let pretty_shape =
41            self.pretty_print_recursive(depth, Shape::pretty_print_with_names_at_depth);
42        if self.names().count() == 0 {
43            pretty_shape
44        } else {
45            let joined = self
46                .names()
47                .map(Name::to_string)
48                .collect::<Vec<_>>()
49                .join(", ");
50            // Since the (aka ...) annotation trails multiline array and object
51            // shapes, it often fits neatly into the otherwise mostly empty line
52            // just after the closing ] or }.
53            format!("{pretty_shape} (aka {joined})")
54        }
55    }
56
57    #[allow(clippy::too_many_lines)]
58    fn pretty_print_recursive(
59        &self,
60        depth: usize,
61        print: impl Fn(&Shape, usize) -> String,
62    ) -> String {
63        fn print_flexible_wrapper(
64            prefix: &str,
65            pretty_children: impl IntoIterator<Item = String>,
66            suffix: &str,
67            depth: usize,
68        ) -> String {
69            let pretty_children = pretty_children.into_iter().collect::<Vec<_>>();
70
71            // We may want to print the one-line version with some padding
72            // inside the prefix/suffix, but we don't want that for the
73            // multiline version, so we strip any padding before proceeding.
74            let trimmed_prefix = prefix.trim();
75            let trimmed_suffix = suffix.trim();
76
77            if pretty_children.is_empty() {
78                // If prefix was "{ " and suffix was " }", this prints "{}"
79                // without the spaces when pretty_children is empty.
80                return format!("{trimmed_prefix}{trimmed_suffix}");
81            }
82
83            if let (1, Some(only)) = (pretty_children.len(), pretty_children.first()) {
84                // If there is only one child, always print it without
85                // leading/trailing line breaks (though `prefix` and `suffix`
86                // may include padding spaces, and `only` may print with its own
87                // internal line breaks).
88                return format!("{prefix}{only}{suffix}");
89            }
90
91            if pretty_children
92                .iter()
93                .all(|pretty| pretty.lines().count() <= 1)
94            {
95                // If all the children are single-line, and they fit within
96                // WRAP_COLUMN columns, we may be able to print them all on one
97                // line. Note we are using the untrimmed prefix and suffix,
98                // since this is a place where padding spaces may be desired.
99                //
100                // By corollary, if the shape has any multiline children, the
101                // whole shape needs to be printed with line breaks. We never
102                // want to print mixed inline/multiline expressions, as in
103                //
104                //   [a, {
105                //     blah: 1
106                //   }, b, c, {
107                //     whatever: true
108                //   }]
109                //
110                // A human might be able to make this style readable, but
111                // machine formatters that allow this hybrid line style tend to
112                // produce unreadable monstrosities.
113                let one_line = format!("{prefix}{}{suffix}", pretty_children.join(", "));
114                if one_line.len() <= WRAP_COLUMN {
115                    return one_line;
116                }
117                // If trying to print all the children on one line would exceed
118                // WRAP_COLUMN columns, proceed to multi-line printing...
119            }
120
121            let mut result = format!("{trimmed_prefix}\n");
122            let child_indent = UNIT_OF_INDENTATION.repeat(depth + 1);
123
124            for pretty in &pretty_children {
125                let _ = writeln!(result, "{child_indent}{pretty},");
126            }
127
128            let suffix_indent = UNIT_OF_INDENTATION.repeat(depth);
129            let _ = write!(result, "{suffix_indent}{trimmed_suffix}");
130
131            result
132        }
133
134        match self.case() {
135            ShapeCase::Bool(Some(b)) => b.to_string(),
136            ShapeCase::Bool(None) => "Bool".to_string(),
137            ShapeCase::String(Some(s)) => quote_string(s.as_str()),
138            ShapeCase::String(None) => "String".to_string(),
139            ShapeCase::Int(Some(i)) => i.to_string(),
140            ShapeCase::Int(None) => "Int".to_string(),
141            ShapeCase::Float => "Float".to_string(),
142            ShapeCase::Null => "null".to_string(), // No typo: JSON null is lowercase.
143
144            ShapeCase::Unknown => "Unknown".to_string(),
145
146            // There may be some argument for using lower-case "none" here,
147            // since None is not a reserved/built-in GraphQL type name like
148            // Bool, String, Int, and Float are, so someone could define a
149            // custom GraphQL None type that would collide with this Shape name.
150            ShapeCase::None => "None".to_string(),
151
152            ShapeCase::Array { prefix, tail } => {
153                let mut pretty_prefix = prefix
154                    .iter()
155                    .map(|shape| print(shape, depth + 1))
156                    .collect::<Vec<_>>();
157
158                if !tail.is_none() {
159                    let pretty_tail =
160                        print_flexible_wrapper("List<", [print(tail, depth + 1)], ">", depth);
161
162                    if pretty_prefix.is_empty() {
163                        // If there is no prefix, then the tail is the whole
164                        // list, so we can return pretty_tail as-is.
165                        return pretty_tail;
166                    }
167
168                    // If the List<S> notation appears at the end of an array
169                    // with a non-empty prefix, use ...List<S> to indicate it is
170                    // not just a single element, but governs the whole rest of
171                    // the array.
172                    pretty_prefix.push(format!("...{pretty_tail}"));
173                }
174
175                print_flexible_wrapper("[", pretty_prefix, "]", depth)
176            }
177
178            ShapeCase::Object { fields, rest } => {
179                let mut sorted_properties = fields
180                    .iter()
181                    .map(|(key, value)| (key, print(value, depth + 1)))
182                    .collect::<Vec<_>>();
183                sorted_properties.sort_by_key(|(key, _)| key.as_str());
184
185                let mut pretty_properties = sorted_properties
186                    .into_iter()
187                    .map(|(key, value)| format!("{}: {value}", quote_non_identifier(key)))
188                    .collect::<Vec<_>>();
189
190                if !rest.is_none() {
191                    let pretty_rest =
192                        print_flexible_wrapper("Dict<", [print(rest, depth + 1)], ">", depth);
193
194                    if pretty_properties.is_empty() {
195                        // If there are no fields, then the rest is the whole
196                        // object, so we can return pretty_rest as-is.
197                        return pretty_rest;
198                    }
199
200                    // If the Dict<S> notation appears at the end of an object
201                    // with a non-empty fields, use ...Dict<S> to indicate it is
202                    // not just a single property, but governs all dynamic
203                    // fields of the object.
204                    pretty_properties.push(format!("...{pretty_rest}"));
205                }
206
207                print_flexible_wrapper(
208                    // If the object is printed as a single line, the
209                    // opening and closing braces will have this extra space
210                    // of padding. If the object is broken onto multiple
211                    // lines, this extra padding will be removed.
212                    "{ ",
213                    pretty_properties,
214                    " }",
215                    depth,
216                )
217            }
218
219            ShapeCase::One(shapes) => {
220                let pretty = print_flexible_wrapper(
221                    "One<",
222                    shapes
223                        .iter()
224                        .map(|child_shape| print(child_shape, depth + 1)),
225                    ">",
226                    depth,
227                );
228
229                // The empty `One<>` union represents an unsatisfiable shape
230                // like TypeScript's `never` type, so that's how we print it.
231                if pretty == "One<>" {
232                    "Never".to_string()
233                } else {
234                    pretty
235                }
236            }
237
238            ShapeCase::All(shapes) => print_flexible_wrapper(
239                "All<",
240                shapes
241                    .iter()
242                    .map(|child_shape| print(child_shape, depth + 1)),
243                ">",
244                depth,
245            ),
246
247            ShapeCase::Name(name, _weak) => name.to_string(),
248
249            ShapeCase::Error(Error {
250                message, partial, ..
251            }) => print_flexible_wrapper(
252                "Error<",
253                std::iter::once(quote_string(message.as_str()))
254                    .chain(partial.iter().map(|p| print(p, depth + 1))),
255                ">",
256                depth,
257            ),
258        }
259    }
260}
261
262impl Debug for Shape {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        write!(f, "Shape({})", self.pretty_print_with_names())
265    }
266}
267
268impl Display for Shape {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        write!(f, "{}", self.pretty_print())
271    }
272}