Skip to main content

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
14/// Printed by [`Shape::pretty_print_bounded`] in place of whatever it leaves
15/// out. Deliberately not `...`, which already marks an array's tail or an
16/// object's rest.
17const ELIDED: &str = "…";
18
19/// Limits for [`Shape::pretty_print_bounded`].
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct PrintLimits {
22    /// How many levels of nested arrays and objects to print. Anything deeper
23    /// prints as `…`. A union or intersection counts as a level only when two
24    /// or more of the members it prints are themselves arrays, objects, unions
25    /// or intersections: a nullable object (`One<{ ... }, null>`) costs the
26    /// same as the object, while a choice between several objects costs a
27    /// level, like the objects it multiplies.
28    pub max_depth: usize,
29    /// How many elements, fields or members of any one shape to print, after
30    /// which a single `…` stands for the rest. Object fields and union or
31    /// intersection members carrying their own errors are kept ahead of the
32    /// cutoff. Array elements are positional, so they are never reordered.
33    pub max_width: usize,
34}
35
36impl Default for PrintLimits {
37    fn default() -> Self {
38        Self {
39            max_depth: 3,
40            max_width: 8,
41        }
42    }
43}
44
45impl Shape {
46    /// Returns a string representation of the [`Shape`], including any attached
47    /// errors as `(err "message")` suffix.
48    ///
49    /// Please note: this display format does not imply an input syntax or
50    /// parser for the shape language. To create new [`Shape`] elements, use the
51    /// various `Shape::*` helper functions.
52    #[must_use]
53    pub fn pretty_print(&self) -> String {
54        self.pretty_print_at_depth(0)
55    }
56
57    fn pretty_print_at_depth(&self, depth: usize) -> String {
58        let pretty_shape =
59            self.pretty_print_recursive(depth, usize::MAX, Shape::pretty_print_at_depth);
60        self.append_error_suffix(pretty_shape)
61    }
62
63    /// Like [`Shape::pretty_print`], but prints at most `limits.max_depth`
64    /// levels of nesting and `limits.max_width` children of any one shape,
65    /// printing `…` for whatever it leaves out.
66    ///
67    /// Use this wherever a shape of unknown size is printed for a person to
68    /// read, such as in an error message. A shape can share its subtrees, so
69    /// its full printed form can be exponentially larger than the shape is in
70    /// memory. This stops at the limits rather than printing everything and
71    /// truncating, so its cost is bounded too.
72    #[must_use]
73    pub fn pretty_print_bounded(&self, limits: PrintLimits) -> String {
74        self.pretty_print_bounded_at_depth(0, limits.max_depth, limits.max_width)
75    }
76
77    fn pretty_print_bounded_at_depth(
78        &self,
79        depth: usize,
80        remaining_depth: usize,
81        max_width: usize,
82    ) -> String {
83        let child_remaining_depth = match self.case() {
84            ShapeCase::Array { .. } | ShapeCase::Object { .. } => {
85                if remaining_depth == 0 {
86                    // The shape's own errors survive its elision. They are
87                    // usually the point of the message, and reading them
88                    // costs nothing more than the shape itself did.
89                    return self.append_error_suffix(ELIDED.to_string());
90                }
91                remaining_depth - 1
92            }
93            // A union or intersection that merely wraps one composite shape
94            // (`One<{ ... }, null>`) is free. One that chooses between several
95            // composite shapes multiplies the output the same way a level of
96            // fields does, so it is charged like one; otherwise the bound is
97            // (max_width * members)^max_depth, which is 2 MB at the defaults
98            // for eight-way unions, not the 8^3 leaves the limits suggest.
99            // The union itself still prints, so `One<…, null>` keeps saying
100            // what was there. Only the members the width lets through count,
101            // since the output grows only through printed members.
102            ShapeCase::One(members) if has_several_composite_members(members.iter(), max_width) => {
103                remaining_depth.saturating_sub(1)
104            }
105            ShapeCase::All(members) if has_several_composite_members(members.iter(), max_width) => {
106                remaining_depth.saturating_sub(1)
107            }
108            _ => remaining_depth,
109        };
110        let pretty_shape = self.pretty_print_recursive(depth, max_width, |child, child_depth| {
111            child.pretty_print_bounded_at_depth(child_depth, child_remaining_depth, max_width)
112        });
113        self.append_error_suffix(pretty_shape)
114    }
115
116    /// Returns a string representation of the [`Shape`] without any error
117    /// annotations. This is the most concise representation.
118    #[must_use]
119    pub fn pretty_print_without_errors(&self) -> String {
120        self.pretty_print_without_errors_at_depth(0)
121    }
122
123    fn pretty_print_without_errors_at_depth(&self, depth: usize) -> String {
124        self.pretty_print_recursive(
125            depth,
126            usize::MAX,
127            Shape::pretty_print_without_errors_at_depth,
128        )
129    }
130
131    /// Returns a string representation of the [`Shape`] with [`Name`] metadata
132    /// included as `<shape> (aka <name1>, <name2>)` and error metadata as
133    /// `(err "message")`. Errors are shown first, then names.
134    #[must_use]
135    pub fn pretty_print_with_names(&self) -> String {
136        self.pretty_print_with_names_at_depth(0)
137    }
138
139    fn pretty_print_with_names_at_depth(&self, depth: usize) -> String {
140        let pretty_shape =
141            self.pretty_print_recursive(depth, usize::MAX, Shape::pretty_print_with_names_at_depth);
142
143        let mut result = pretty_shape;
144
145        // Append errors first
146        result = self.append_error_suffix(result);
147
148        // Then append names
149        if self.names().count() > 0 {
150            let joined = self
151                .names()
152                .map(Name::to_string)
153                .collect::<Vec<_>>()
154                .join(", ");
155            // Since the (aka ...) annotation trails multiline array and object
156            // shapes, it often fits neatly into the otherwise mostly empty line
157            // just after the closing ] or }.
158            result = format!("{result} (aka {joined})");
159        }
160
161        result
162    }
163
164    /// Helper to append `(err "msg1", "msg2")` suffix if shape has its own errors.
165    fn append_error_suffix(&self, pretty_shape: String) -> String {
166        let error_messages: Vec<_> = self
167            .own_errors()
168            .map(|e| quote_string(&e.message))
169            .collect();
170        if error_messages.is_empty() {
171            return pretty_shape;
172        }
173        format!("{pretty_shape} (err {})", error_messages.join(", "))
174    }
175
176    #[allow(clippy::too_many_lines)]
177    fn pretty_print_recursive(
178        &self,
179        depth: usize,
180        max_width: usize,
181        print: impl Fn(&Shape, usize) -> String,
182    ) -> String {
183        fn print_flexible_wrapper(
184            prefix: &str,
185            pretty_children: impl IntoIterator<Item = String>,
186            suffix: &str,
187            depth: usize,
188        ) -> String {
189            let pretty_children = pretty_children.into_iter().collect::<Vec<_>>();
190
191            // We may want to print the one-line version with some padding
192            // inside the prefix/suffix, but we don't want that for the
193            // multiline version, so we strip any padding before proceeding.
194            let trimmed_prefix = prefix.trim();
195            let trimmed_suffix = suffix.trim();
196
197            if pretty_children.is_empty() {
198                // If prefix was "{ " and suffix was " }", this prints "{}"
199                // without the spaces when pretty_children is empty.
200                return format!("{trimmed_prefix}{trimmed_suffix}");
201            }
202
203            if let (1, Some(only)) = (pretty_children.len(), pretty_children.first()) {
204                // If there is only one child, always print it without
205                // leading/trailing line breaks (though `prefix` and `suffix`
206                // may include padding spaces, and `only` may print with its own
207                // internal line breaks).
208                return format!("{prefix}{only}{suffix}");
209            }
210
211            if pretty_children
212                .iter()
213                .all(|pretty| pretty.lines().count() <= 1)
214            {
215                // If all the children are single-line, and they fit within
216                // WRAP_COLUMN columns, we may be able to print them all on one
217                // line. Note we are using the untrimmed prefix and suffix,
218                // since this is a place where padding spaces may be desired.
219                //
220                // By corollary, if the shape has any multiline children, the
221                // whole shape needs to be printed with line breaks. We never
222                // want to print mixed inline/multiline expressions, as in
223                //
224                //   [a, {
225                //     blah: 1
226                //   }, b, c, {
227                //     whatever: true
228                //   }]
229                //
230                // A human might be able to make this style readable, but
231                // machine formatters that allow this hybrid line style tend to
232                // produce unreadable monstrosities.
233                let one_line = format!("{prefix}{}{suffix}", pretty_children.join(", "));
234                if one_line.len() <= WRAP_COLUMN {
235                    return one_line;
236                }
237                // If trying to print all the children on one line would exceed
238                // WRAP_COLUMN columns, proceed to multi-line printing...
239            }
240
241            let mut result = format!("{trimmed_prefix}\n");
242            let child_indent = UNIT_OF_INDENTATION.repeat(depth + 1);
243
244            for pretty in &pretty_children {
245                let _ = writeln!(result, "{child_indent}{pretty},");
246            }
247
248            let suffix_indent = UNIT_OF_INDENTATION.repeat(depth);
249            let _ = write!(result, "{suffix_indent}{trimmed_suffix}");
250
251            result
252        }
253
254        match self.case() {
255            ShapeCase::Bool(Some(b)) => b.to_string(),
256            ShapeCase::Bool(None) => "Bool".to_string(),
257            ShapeCase::String(Some(s)) => quote_string(s.as_str()),
258            ShapeCase::String(None) => "String".to_string(),
259            ShapeCase::Int(Some(i)) => i.to_string(),
260            ShapeCase::Int(None) => "Int".to_string(),
261            ShapeCase::Float => "Float".to_string(),
262            ShapeCase::Null => "null".to_string(), // No typo: JSON null is lowercase.
263
264            ShapeCase::Unknown => "Unknown".to_string(),
265
266            // There may be some argument for using lower-case "none" here,
267            // since None is not a reserved/built-in GraphQL type name like
268            // Bool, String, Int, and Float are, so someone could define a
269            // custom GraphQL None type that would collide with this Shape name.
270            ShapeCase::None => "None".to_string(),
271
272            ShapeCase::Array { prefix, tail } => {
273                let mut pretty_prefix = prefix
274                    .iter()
275                    .take(max_width)
276                    .map(|shape| print(shape, depth + 1))
277                    .collect::<Vec<_>>();
278                if prefix.len() > max_width {
279                    pretty_prefix.push(ELIDED.to_string());
280                }
281
282                if !tail.is_none() {
283                    // Condense the tail indicator so the structural prefix
284                    // stays readable. `...` alone marks the common
285                    // open-`Unknown` tail; otherwise prepend `...` to the
286                    // tail's own pretty-print, which already carries any
287                    // errors / names the tail shape has. A prefix-free open
288                    // array (formerly the canonical `List<S>` form) thus
289                    // renders as `[...S]`, or just `[...]` when S is Unknown.
290                    let pretty_tail_inner = print(tail, depth + 1);
291                    let rest_marker = if pretty_tail_inner == "Unknown" {
292                        "...".to_string()
293                    } else {
294                        format!("...{pretty_tail_inner}")
295                    };
296                    pretty_prefix.push(rest_marker);
297                }
298
299                print_flexible_wrapper("[", pretty_prefix, "]", depth)
300            }
301
302            ShapeCase::Object { fields, rest } => {
303                // Sort before printing, so that a `max_width` limit keeps the
304                // same fields whatever their insertion order, and fields past
305                // the limit are never printed at all.
306                let mut sorted_properties = fields.iter().collect::<Vec<_>>();
307                sorted_properties.sort_by_key(|(key, _)| key.as_str());
308
309                if fields.len() > max_width {
310                    // Fields carrying their own errors are kept ahead of the
311                    // cutoff, since those errors are usually what the reader
312                    // needs, and the kept fields then go back into key order.
313                    // `has_own_errors` is O(1); the recursive `has_errors`
314                    // would visit the subtrees the limits exist to skip.
315                    let (with_errors, without_errors): (Vec<_>, Vec<_>) = sorted_properties
316                        .into_iter()
317                        .partition(|(_, value)| value.has_own_errors());
318                    sorted_properties = with_errors
319                        .into_iter()
320                        .chain(without_errors)
321                        .take(max_width)
322                        .collect();
323                    sorted_properties.sort_by_key(|(key, _)| key.as_str());
324                }
325
326                let mut pretty_properties = sorted_properties
327                    .into_iter()
328                    .map(|(key, value)| {
329                        format!("{}: {}", quote_non_identifier(key), print(value, depth + 1))
330                    })
331                    .collect::<Vec<_>>();
332                if fields.len() > max_width {
333                    pretty_properties.push(ELIDED.to_string());
334                }
335
336                // Named fields get padded braces (`{ a: Int }`); a bare
337                // rest-only object prints tight (`{...S}`), so remember
338                // whether any real fields were present before we push the
339                // rest marker below.
340                let has_fields = !pretty_properties.is_empty();
341
342                if !rest.is_none() {
343                    // Condense the rest indicator so the structural fields
344                    // stay readable. `...` alone marks the common
345                    // open-`Unknown` rest; otherwise prepend `...` to the
346                    // rest's own pretty-print, which already carries any
347                    // errors / names the rest shape has. A field-free open
348                    // object (formerly the canonical `Dict<S>` form) thus
349                    // renders as `{...S}`, or just `{...}` when S is Unknown.
350                    let pretty_rest_inner = print(rest, depth + 1);
351                    let rest_marker = if pretty_rest_inner == "Unknown" {
352                        "...".to_string()
353                    } else {
354                        format!("...{pretty_rest_inner}")
355                    };
356                    pretty_properties.push(rest_marker);
357                }
358
359                // When there are named fields and the object prints on a
360                // single line, the opening and closing braces carry this
361                // extra space of padding. If the object is broken onto
362                // multiple lines, `print_flexible_wrapper` trims it. A bare
363                // rest-only object (`{...S}`) uses tight braces with no
364                // padding.
365                let (prefix, suffix) = if has_fields { ("{ ", " }") } else { ("{", "}") };
366
367                print_flexible_wrapper(prefix, pretty_properties, suffix, depth)
368            }
369
370            ShapeCase::One(shapes) => {
371                let pretty = print_flexible_wrapper(
372                    "One<",
373                    print_members(shapes.iter(), max_width, |child_shape| {
374                        print(child_shape, depth + 1)
375                    }),
376                    ">",
377                    depth,
378                );
379
380                // The empty `One<>` union represents an unsatisfiable shape
381                // like TypeScript's `never` type, so that's how we print it.
382                if pretty == "One<>" {
383                    "Never".to_string()
384                } else {
385                    pretty
386                }
387            }
388
389            ShapeCase::All(shapes) => print_flexible_wrapper(
390                "All<",
391                print_members(shapes.iter(), max_width, |child_shape| {
392                    print(child_shape, depth + 1)
393                }),
394                ">",
395                depth,
396            ),
397
398            ShapeCase::Name(name, _weak) => name.to_string(),
399        }
400    }
401}
402
403/// Print the members that `max_width` lets through (see [`kept_members`]),
404/// then one [`ELIDED`] marker if any were left out.
405fn print_members<'a>(
406    members: impl Iterator<Item = &'a Shape>,
407    max_width: usize,
408    print: impl Fn(&Shape) -> String,
409) -> Vec<String> {
410    let (kept, elided) = kept_members(members, max_width);
411    let mut pretty = kept.into_iter().map(print).collect::<Vec<_>>();
412    if elided {
413        pretty.push(ELIDED.to_string());
414    }
415    // Members are a set, so every member elided by depth collapses into one
416    // bare marker: `One<…, null>` rather than `One<…, null, …>`. A marker
417    // carrying errors (`… (err "…")`) is not bare, and stays.
418    let mut seen_bare_marker = false;
419    pretty.retain(|member| {
420        if member != ELIDED {
421            return true;
422        }
423        let first = !seen_bare_marker;
424        seen_bare_marker = true;
425        first
426    });
427    pretty
428}
429
430/// The members of a union or intersection that `max_width` lets through, in
431/// their original order, and whether any were left out. Members carrying
432/// their own errors are kept ahead of the cutoff, for the same reason object
433/// fields are: those errors are usually what the reader needs. Only
434/// `has_own_errors` (O(1)) is consulted, so the cost bound holds.
435fn kept_members<'a>(
436    members: impl Iterator<Item = &'a Shape>,
437    max_width: usize,
438) -> (Vec<&'a Shape>, bool) {
439    let members = members.collect::<Vec<_>>();
440    if members.len() <= max_width {
441        return (members, false);
442    }
443    let (with_errors, without_errors): (Vec<usize>, Vec<usize>) =
444        (0..members.len()).partition(|&index| members[index].has_own_errors());
445    let mut kept = with_errors
446        .into_iter()
447        .chain(without_errors)
448        .take(max_width)
449        .collect::<Vec<_>>();
450    kept.sort_unstable();
451    (kept.into_iter().map(|index| members[index]).collect(), true)
452}
453
454/// Whether two or more of the members that `max_width` lets through are
455/// arrays, objects, unions or intersections. See [`PrintLimits::max_depth`].
456fn has_several_composite_members<'a>(
457    members: impl Iterator<Item = &'a Shape>,
458    max_width: usize,
459) -> bool {
460    kept_members(members, max_width)
461        .0
462        .into_iter()
463        .filter(|member| {
464            matches!(
465                member.case(),
466                ShapeCase::Array { .. }
467                    | ShapeCase::Object { .. }
468                    | ShapeCase::One(_)
469                    | ShapeCase::All(_)
470            )
471        })
472        .nth(1)
473        .is_some()
474}
475
476impl Debug for Shape {
477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478        write!(f, "Shape({})", self.pretty_print_with_names())
479    }
480}
481
482impl Display for Shape {
483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484        write!(f, "{}", self.pretty_print())
485    }
486}
487
488#[cfg(test)]
489mod bounded_tests {
490    use indexmap::IndexMap;
491
492    use super::PrintLimits;
493    use crate::Shape;
494
495    fn record(fields: impl IntoIterator<Item = (impl Into<String>, Shape)>) -> Shape {
496        Shape::closed_record(
497            fields
498                .into_iter()
499                .map(|(key, value)| (key.into(), value))
500                .collect::<IndexMap<_, _>>(),
501            [],
502        )
503    }
504
505    fn limits(max_depth: usize, max_width: usize) -> PrintLimits {
506        PrintLimits {
507            max_depth,
508            max_width,
509        }
510    }
511
512    #[test]
513    fn unlimited_matches_pretty_print() {
514        let shape = Shape::one(
515            [
516                record([
517                    ("id", Shape::int([])),
518                    ("tags", Shape::list(Shape::string([]), [])),
519                    (
520                        "nested",
521                        record([(
522                            "deep",
523                            Shape::closed_tuple([Shape::bool([]), Shape::null([])], []),
524                        )]),
525                    ),
526                ]),
527                Shape::null([]),
528            ],
529            [],
530        );
531        assert_eq!(
532            shape.pretty_print_bounded(limits(usize::MAX, usize::MAX)),
533            shape.pretty_print(),
534        );
535    }
536
537    #[test]
538    fn elides_below_max_depth() {
539        let shape = record([("a", record([("b", record([("c", Shape::int([]))]))]))]);
540        assert_eq!(shape.pretty_print_bounded(limits(0, 8)), "…");
541        assert_eq!(shape.pretty_print_bounded(limits(1, 8)), "{ a: … }");
542        assert_eq!(shape.pretty_print_bounded(limits(2, 8)), "{ a: { b: … } }");
543        assert_eq!(
544            shape.pretty_print_bounded(limits(3, 8)),
545            "{ a: { b: { c: Int } } }",
546        );
547    }
548
549    #[test]
550    fn nullable_unions_do_not_count_as_a_level() {
551        let nullable = |shape| Shape::one([shape, Shape::null([])], []);
552        let shape = nullable(record([("a", nullable(record([("b", Shape::int([]))])))]));
553        assert_eq!(
554            shape.pretty_print_bounded(limits(1, 8)),
555            "One<{ a: One<…, null> }, null>",
556        );
557    }
558
559    #[test]
560    fn elides_past_max_width() {
561        let object = record([
562            ("d", Shape::int([])),
563            ("a", Shape::int([])),
564            ("c", Shape::int([])),
565            ("b", Shape::int([])),
566        ]);
567        // Fields are chosen in sorted order, not insertion order.
568        assert_eq!(
569            object.pretty_print_bounded(limits(3, 2)),
570            "{ a: Int, b: Int, … }"
571        );
572
573        let tuple = Shape::closed_tuple([Shape::int([]), Shape::bool([]), Shape::null([])], []);
574        assert_eq!(tuple.pretty_print_bounded(limits(3, 2)), "[Int, Bool, …]");
575
576        let one = Shape::one([Shape::int([]), Shape::bool([]), Shape::string([])], []);
577        assert_eq!(one.pretty_print_bounded(limits(3, 2)), "One<Int, Bool, …>");
578
579        // Nothing is elided when the width is exactly enough.
580        assert_eq!(one.pretty_print_bounded(limits(3, 3)), one.pretty_print());
581    }
582
583    #[test]
584    fn bounded_on_shared_subtrees() {
585        // Each level refers to the next twice, so the full printed form doubles
586        // with every level: 2^40 leaves here. Only the bounded print is
587        // feasible, and it stays small.
588        let mut shape = Shape::int([]);
589        for _ in 0..40 {
590            shape = record([("a", shape.clone()), ("b", shape)]);
591        }
592        let printed = shape.pretty_print_bounded(PrintLimits::default());
593        assert!(printed.len() < 1_000, "{} bytes:\n{printed}", printed.len());
594        assert!(printed.contains('…'));
595    }
596
597    #[test]
598    fn unions_of_several_composites_count_as_a_level() {
599        let a = record([("a", Shape::int([]))]);
600        let b = record([("b", Shape::int([]))]);
601        let shape = record([("x", Shape::one([a.clone(), b], []))]);
602        // `One<{ a }, { b }>` costs a level, so its objects need depth 3.
603        assert_eq!(shape.pretty_print_bounded(limits(1, 8)), "{ x: One<…> }");
604        assert_eq!(shape.pretty_print_bounded(limits(2, 8)), "{ x: One<…> }");
605        assert_eq!(
606            shape.pretty_print_bounded(limits(3, 8)),
607            "{ x: One<{ a: Int }, { b: Int }> }",
608        );
609        // A union of one composite shape and scalars stays free.
610        let nullable = record([("x", Shape::one([a, Shape::null([])], []))]);
611        assert_eq!(
612            nullable.pretty_print_bounded(limits(2, 8)),
613            "{ x: One<{ a: Int }, null> }",
614        );
615    }
616
617    #[test]
618    fn bounded_on_union_heavy_shape() {
619        // Every field is an eight-way union of objects with eight fields each,
620        // eight levels deep through shared subtrees: 64^8 leaves. With unions
621        // free of charge this printed 2 MB under the default limits.
622        let mut variants = (0..8)
623            .map(|v| record((0..8).map(|i| (format!("v{v}_f{i}"), Shape::int([])))))
624            .collect::<Vec<_>>();
625        for _ in 1..8 {
626            let union = Shape::one(variants, []);
627            variants = (0..8)
628                .map(|v| record((0..8).map(|i| (format!("v{v}_f{i}"), union.clone()))))
629                .collect();
630        }
631        let printed = variants[0].pretty_print_bounded(PrintLimits::default());
632        assert!(printed.len() < 32_000, "{} bytes", printed.len());
633        assert!(printed.contains("One<…>"));
634    }
635
636    #[test]
637    fn elided_shapes_keep_their_errors() {
638        let inner = Shape::error_with_partial(
639            "field c is not a String",
640            record([("c", Shape::int([]))]),
641            [],
642        );
643        let shape = record([("a", record([("b", inner)]))]);
644        assert_eq!(
645            shape.pretty_print_bounded(limits(2, 8)),
646            r#"{ a: { b: … (err "field c is not a String") } }"#,
647        );
648        // A shape without errors of its own elides to the bare marker.
649        assert_eq!(shape.pretty_print_bounded(limits(0, 8)), "…");
650        let top = Shape::error_with_partial("top", record([("a", Shape::int([]))]), []);
651        assert_eq!(top.pretty_print_bounded(limits(0, 8)), r#"… (err "top")"#);
652    }
653
654    #[test]
655    fn width_cutoff_keeps_fields_with_errors() {
656        let shape = record([
657            ("a", Shape::int([])),
658            ("b", Shape::int([])),
659            ("z", Shape::error("z is bad", [])),
660        ]);
661        assert_eq!(
662            shape.pretty_print_bounded(limits(3, 2)),
663            r#"{ a: Int, z: Unknown (err "z is bad"), … }"#,
664        );
665        // Nothing is reordered when nothing is cut.
666        assert_eq!(
667            shape.pretty_print_bounded(limits(3, 3)),
668            shape.pretty_print()
669        );
670    }
671
672    #[test]
673    fn elided_union_members_fold_into_one_marker() {
674        let shape = Shape::one(
675            [
676                record([("a", Shape::int([]))]),
677                record([("b", Shape::int([]))]),
678                Shape::null([]),
679            ],
680            [],
681        );
682        assert_eq!(shape.pretty_print_bounded(limits(0, 8)), "One<…, null>");
683    }
684
685    #[test]
686    fn tails_and_rests_at_the_cutoff() {
687        let object = record([("x", Shape::int([]))]);
688        let lists = Shape::list(Shape::list(object.clone(), []), []);
689        assert_eq!(lists.pretty_print_bounded(limits(1, 8)), "[...…]");
690        assert_eq!(
691            Shape::dict(object, []).pretty_print_bounded(limits(1, 8)),
692            "{...…}"
693        );
694    }
695
696    #[test]
697    fn zero_width() {
698        let shape = record([("a", Shape::int([])), ("b", Shape::int([]))]);
699        assert_eq!(shape.pretty_print_bounded(limits(3, 0)), "{ … }");
700        assert_eq!(
701            Shape::closed_tuple([Shape::int([])], []).pretty_print_bounded(limits(3, 0)),
702            "[…]",
703        );
704        assert_eq!(
705            Shape::one([Shape::int([]), Shape::bool([])], []).pretty_print_bounded(limits(3, 0)),
706            "One<…>",
707        );
708    }
709
710    #[test]
711    fn every_bare_marker_folds_even_when_not_adjacent() {
712        let shape = Shape::one(
713            [
714                record([("a", Shape::int([]))]),
715                Shape::null([]),
716                record([("b", Shape::int([]))]),
717            ],
718            [],
719        );
720        assert_eq!(shape.pretty_print_bounded(limits(0, 8)), "One<…, null>");
721
722        // A marker carrying errors is not bare, and stays.
723        let shape = Shape::one(
724            [
725                Shape::error_with_partial("a bad", record([("a", Shape::int([]))]), []),
726                record([("b", Shape::int([]))]),
727                record([("c", Shape::int([]))]),
728            ],
729            [],
730        );
731        assert_eq!(
732            shape.pretty_print_bounded(limits(0, 8)),
733            r#"One<… (err "a bad"), …>"#,
734        );
735    }
736
737    #[test]
738    fn union_charge_counts_only_printed_members() {
739        let a = record([("a", Shape::int([]))]);
740        let b = record([("b", Shape::int([]))]);
741        let shape = record([("x", Shape::one([a, b], []))]);
742        // At width 1 only one object prints, so the union is free.
743        assert_eq!(
744            shape.pretty_print_bounded(limits(2, 1)),
745            "{ x: One<{ a: Int }, …> }",
746        );
747    }
748
749    #[test]
750    fn union_of_an_object_and_an_array_counts_as_a_level() {
751        let shape = record([(
752            "x",
753            Shape::one(
754                [
755                    record([("a", Shape::int([]))]),
756                    Shape::list(Shape::int([]), []),
757                ],
758                [],
759            ),
760        )]);
761        assert_eq!(shape.pretty_print_bounded(limits(2, 8)), "{ x: One<…> }");
762        assert_eq!(
763            shape.pretty_print_bounded(limits(3, 8)),
764            "{ x: One<{ a: Int }, [...Int]> }",
765        );
766    }
767
768    #[test]
769    fn width_cutoff_keeps_union_members_with_errors() {
770        let shape = Shape::one(
771            [
772                Shape::int([]),
773                Shape::bool([]),
774                Shape::error_with_partial("member bad", Shape::string([]), []),
775            ],
776            [],
777        );
778        // Kept members stay in their original order.
779        assert_eq!(
780            shape.pretty_print_bounded(limits(3, 2)),
781            r#"One<Int, String (err "member bad"), …>"#,
782        );
783        // Nothing is reordered when nothing is cut.
784        assert_eq!(
785            shape.pretty_print_bounded(limits(3, 3)),
786            shape.pretty_print()
787        );
788    }
789
790    #[test]
791    fn tuple_elements_stay_positional_at_the_cutoff() {
792        let shape = Shape::closed_tuple(
793            [
794                Shape::int([]),
795                Shape::int([]),
796                Shape::error("third bad", []),
797            ],
798            [],
799        );
800        assert_eq!(shape.pretty_print_bounded(limits(3, 2)), "[Int, Int, …]");
801    }
802
803    #[test]
804    fn more_error_fields_than_the_width_allows() {
805        let shape = record([
806            ("c", Shape::error("ec", [])),
807            ("a", Shape::error("ea", [])),
808            ("b", Shape::error("eb", [])),
809            ("d", Shape::int([])),
810        ]);
811        // Error fields are cut among themselves in key order.
812        assert_eq!(
813            shape.pretty_print_bounded(limits(3, 2)),
814            r#"{ a: Unknown (err "ea"), b: Unknown (err "eb"), … }"#,
815        );
816    }
817
818    #[test]
819    fn intersection_of_several_composites_counts_as_a_level() {
820        // Objects merge under `Shape::all`, so pair an object with an array.
821        let shape = record([(
822            "x",
823            Shape::all(
824                [
825                    record([("a", Shape::int([]))]),
826                    Shape::list(Shape::int([]), []),
827                ],
828                [],
829            ),
830        )]);
831        assert_eq!(shape.pretty_print_bounded(limits(2, 8)), "{ x: All<…> }");
832        assert_eq!(
833            shape.pretty_print_bounded(limits(3, 8)),
834            "{ x: All<[...Int], { a: Int }> }",
835        );
836    }
837}