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}