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}