blueprint_display_container/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is dual-licensed under either the MIT license found in the
5 * LICENSE-MIT file in the root directory of this source tree or the Apache
6 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
7 * of this source tree. You may select, at your option, one of the
8 * above-listed licenses.
9 */
10
11//! Provides utilities to implement `Display`, which also provides an "alternate" display.
12//!
13//! As an example of using these combinators:
14//!
15//! ```
16//! use std::fmt;
17//!
18//! use blueprint_display_container::*;
19//!
20//! struct MyItems(Vec<(String, i32)>);
21//!
22//! impl fmt::Display for MyItems {
23//!     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24//!         fmt_container(
25//!             f,
26//!             "{",
27//!             "}",
28//!             iter_display_chain(
29//!                 &["magic"],
30//!                 self.0.iter().map(|(k, v)| display_pair(k, "=", v)),
31//!             ),
32//!         )
33//!     }
34//! }
35//! ```
36//!
37//! Would produce results such as:
38//!
39//! ```ignore
40//! {magic, hello=1, world=2}
41//! ```
42//!
43//! For "normal" display, produces output like (with `prefix="prefix[", suffix="]"`):
44//!
45//! `prefix[]`
46//! `prefix[1]`
47//! `prefix[1, 2]`
48//!
49//! For "alternate" display, produces output like:
50//!
51//! `prefix[]`
52//! `prefix[ 1 ]`
53//! ```ignore
54//! prefix[
55//!   1,
56//!   2
57//! ]
58//! ```
59//!
60//! This doesn't propagate the flags on the Formatter other than alternate.
61// TODO(cjhopman): Starlark values don't really do anything with the rest of the flags so
62// propagating them hasn't been necessary, but it would be easy enough to implement if we wanted to.
63
64use std::fmt;
65use std::fmt::Display;
66use std::fmt::Write;
67
68use either::Either;
69
70const INDENT: &str = "  ";
71
72/// Used to indent a displayed item for alternate display. This helps us pretty-print deep data structures.
73fn subwriter<T: Display>(indent: &'static str, f: &mut fmt::Formatter, v: T) -> fmt::Result {
74    if f.alternate() {
75        write!(indenter::indented(f).with_str(indent), "{:#}", &v)
76    } else {
77        Display::fmt(&v, f)
78    }
79}
80
81/// Iterator length for display.
82enum Len {
83    Zero,
84    One,
85    Many, // > 1
86}
87
88/// Display a pair of elements with a separator in the middle.
89///
90/// Equivalent to `write!(f, "{}{}{}", key, separator, value)`.
91pub fn display_pair<'a, K: Display + 'a, V: Display + 'a>(
92    key: K,
93    separator: &'a str,
94    value: V,
95) -> impl Display + 'a {
96    DisplayPair(key, separator, value)
97}
98
99struct DisplayPair<'a, K: Display, V: Display>(pub K, pub &'a str, pub V);
100
101impl<K: Display, V: Display> Display for DisplayPair<'_, K, V> {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        Display::fmt(&self.0, f)?;
104        f.write_str(self.1)?;
105        Display::fmt(&self.2, f)
106    }
107}
108
109/// The low-level helper for displaying containers. For simple containers, it may be more convenient to use `display_container` or `display_keyed_container`.
110struct ContainerDisplayHelper<'a, 'b> {
111    f: &'a mut fmt::Formatter<'b>,
112    /// The additional separator to be added after each item (except the last).
113    separator: &'static str,
114    /// Extra output to be added after the prefix and before the suffix. Only non-empty for the single item case.
115    outer: &'static str,
116    /// The indent used for each item in the multiple items case (where each item is placed on its own line).
117    indent: &'static str,
118    /// A count of items, used for correctly adding the separator.
119    seen_items: usize,
120}
121
122impl<'a, 'b> ContainerDisplayHelper<'a, 'b> {
123    /// Begins displaying a container. The provided num_items will be used to select which formatting to use for alternate display.
124    fn begin_inner(
125        f: &'a mut fmt::Formatter<'b>,
126        prefix: &str,
127        num_items: Len,
128    ) -> Result<Self, fmt::Error> {
129        let (separator, outer, indent) = match (f.alternate(), num_items) {
130            // We want to be formatted as `{prefix}item1, item2{suffix}`, like `[1, 2]` for lists.
131            (false, _) => (", ", "", ""),
132            // We want to be formatted as `{prefix}{suffix}`, like `[]` for lists.
133            (true, Len::Zero) => ("", "", ""),
134            // We want to be formatted as `{prefix} item {suffix}`, like `[ item ]` for lists
135            (true, Len::One) => ("", " ", ""),
136            // We want to be formatted as `{prefix}\n  item1,\n  item2\n{suffix}`, for lists like:
137            // ```
138            // [
139            //    item1,
140            //    item2
141            // ]
142            // ```
143            _ => (",\n", "\n", INDENT),
144        };
145        f.write_str(prefix)?;
146        f.write_str(outer)?;
147
148        Ok(Self {
149            f,
150            separator,
151            outer,
152            indent,
153            seen_items: 0,
154        })
155    }
156
157    /// Displays an item.
158    pub fn item<T: Display>(&mut self, v: T) -> fmt::Result {
159        if self.seen_items != 0 {
160            self.f.write_str(self.separator)?;
161        }
162        self.seen_items += 1;
163        subwriter(self.indent, self.f, &v)
164    }
165
166    /// Ends displaying a container.
167    pub fn end(self, suffix: &str) -> fmt::Result {
168        self.f.write_str(self.outer)?;
169        self.f.write_str(suffix)
170    }
171}
172
173/// Helper for display implementation of container-y types (like list, tuple).
174pub fn fmt_container<T: Display, Iter: IntoIterator<Item = T>>(
175    f: &mut fmt::Formatter,
176    prefix: &str,
177    suffix: &str,
178    items: Iter,
179) -> fmt::Result {
180    let mut items = items.into_iter();
181    let helper = match items.next() {
182        None => ContainerDisplayHelper::begin_inner(f, prefix, Len::Zero)?,
183        Some(first) => match items.next() {
184            None => {
185                let mut helper = ContainerDisplayHelper::begin_inner(f, prefix, Len::One)?;
186                helper.item(first)?;
187                helper
188            }
189            Some(second) => {
190                let mut helper = ContainerDisplayHelper::begin_inner(f, prefix, Len::Many)?;
191                helper.item(first)?;
192                helper.item(second)?;
193                for v in items {
194                    helper.item(v)?;
195                }
196                helper
197            }
198        },
199    };
200    helper.end(suffix)
201}
202
203/// Helper for display implementation of container-y types (like list, tuple).
204pub fn display_container<'a, C>(prefix: &'a str, suffix: &'a str, items: C) -> impl Display + 'a
205where
206    C: Copy + IntoIterator + 'a,
207    <C as IntoIterator>::Item: Display,
208{
209    struct Impl<'a, C> {
210        prefix: &'a str,
211        suffix: &'a str,
212        items: C,
213    }
214    impl<C> Display for Impl<'_, C>
215    where
216        C: Copy + IntoIterator,
217        <C as IntoIterator>::Item: Display,
218    {
219        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
220            fmt_container(f, self.prefix, self.suffix, self.items)
221        }
222    }
223    Impl {
224        prefix,
225        suffix,
226        items,
227    }
228}
229
230/// Helper for display implementation of container-y types (like dict, struct).
231///
232/// Equivalent to [`fmt_container`] where the items have [`display_pair`] applied to them.
233pub fn fmt_keyed_container<K: Display, V: Display, Iter: IntoIterator<Item = (K, V)>>(
234    f: &mut fmt::Formatter,
235    prefix: &str,
236    suffix: &str,
237    separator: &str,
238    items: Iter,
239) -> fmt::Result {
240    fmt_container(
241        f,
242        prefix,
243        suffix,
244        items
245            .into_iter()
246            .map(|(k, v)| display_pair(k, separator, v)),
247    )
248}
249
250/// Chain two iterators together that produce `Display` items.
251pub fn iter_display_chain<A, B>(first: A, second: B) -> impl Iterator<Item = impl Display>
252where
253    A: IntoIterator,
254    A::Item: Display,
255    B: IntoIterator,
256    B::Item: Display,
257{
258    first
259        .into_iter()
260        .map(Either::Left)
261        .chain(second.into_iter().map(Either::Right))
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_container() {
270        struct Wrapped(Vec<u32>);
271        impl fmt::Display for Wrapped {
272            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273                fmt_container(f, "prefix[", "]", self.0.iter())
274            }
275        }
276
277        assert_eq!("prefix[]", format!("{:}", Wrapped(vec![])));
278        assert_eq!("prefix[1]", format!("{:}", Wrapped(vec![1])));
279        assert_eq!("prefix[1, 2]", format!("{:}", Wrapped(vec![1, 2])));
280        assert_eq!("prefix[1, 2, 3]", format!("{:}", Wrapped(vec![1, 2, 3])));
281
282        assert_eq!("prefix[]", format!("{:#}", Wrapped(vec![])));
283        assert_eq!("prefix[ 1 ]", format!("{:#}", Wrapped(vec![1])));
284        assert_eq!(
285            "prefix[\n  1,\n  2\n]",
286            format!("{:#}", Wrapped(vec![1, 2])),
287        );
288        assert_eq!(
289            "prefix[\n  1,\n  2,\n  3\n]",
290            format!("{:#}", Wrapped(vec![1, 2, 3])),
291        );
292    }
293
294    #[test]
295    fn test_keyed_container() {
296        struct Wrapped(Vec<(u32, &'static str)>);
297        impl fmt::Display for Wrapped {
298            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299                fmt_keyed_container(
300                    f,
301                    "prefix[",
302                    "]",
303                    ": ",
304                    // just wrap with `"` to make it clearer in the output
305                    self.0.iter().map(|(k, v)| (k, format!("\"{v}\""))),
306                )
307            }
308        }
309
310        assert_eq!("prefix[]", format!("{:}", Wrapped(vec![])));
311        assert_eq!("prefix[1: \"1\"]", format!("{:}", Wrapped(vec![(1, "1")])));
312        assert_eq!(
313            "prefix[1: \"1\", 2: \"2\"]",
314            format!("{:}", Wrapped(vec![(1, "1"), (2, "2")])),
315        );
316        assert_eq!(
317            "prefix[1: \"1\", 2: \"2\", 3: \"3\"]",
318            format!("{:}", Wrapped(vec![(1, "1"), (2, "2"), (3, "3")])),
319        );
320
321        assert_eq!("prefix[]", format!("{:#}", Wrapped(vec![])));
322        assert_eq!(
323            "prefix[ 1: \"1\" ]",
324            format!("{:#}", Wrapped(vec![(1, "1")]))
325        );
326        assert_eq!(
327            "prefix[\n  1: \"1\",\n  2: \"2\"\n]",
328            format!("{:#}", Wrapped(vec![(1, "1"), (2, "2")])),
329        );
330        assert_eq!(
331            "prefix[\n  1: \"1\",\n  2: \"2\",\n  3: \"3\"\n]",
332            format!("{:#}", Wrapped(vec![(1, "1"), (2, "2"), (3, "3")])),
333        );
334    }
335
336    #[test]
337    fn test_combinators() {
338        struct MyItems(Vec<(String, i32)>);
339        impl fmt::Display for MyItems {
340            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
341                fmt_container(
342                    f,
343                    "{",
344                    "}",
345                    iter_display_chain(
346                        &["magic"],
347                        self.0.iter().map(|(k, v)| display_pair(k, "=", v)),
348                    ),
349                )
350            }
351        }
352        assert_eq!(
353            MyItems(vec![("hello".to_owned(), 1), ("world".to_owned(), 2)]).to_string(),
354            "{magic, hello=1, world=2}"
355        );
356    }
357
358    #[test]
359    fn test_display_container() {
360        assert_eq!("[1]", display_container("[", "]", &vec![1]).to_string());
361    }
362}