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}