Skip to main content

impl_more/
display.rs

1/// Implements [`Display`] for structs by forwarding to one of its field.
2///
3/// Emitted code is not compatible with `#[no_std]`.
4///
5/// Newtype structs can omit the field identifier.
6///
7/// # Examples
8///
9/// For newtype struct:
10///
11/// ```
12/// # use impl_more::forward_display;
13/// struct Foo(String);
14///
15/// impl_more::forward_display!(Foo);
16///
17/// assert_eq!(Foo("hello world".to_owned()).to_string(), "hello world");
18/// ```
19///
20/// For struct with named field:
21///
22/// ```
23/// # use impl_more::forward_display;
24/// struct Bar {
25///     inner: u64,
26/// }
27///
28/// impl_more::forward_display!(Bar => inner);
29///
30/// assert_eq!(Bar { inner: 42 }.to_string(), "42");
31/// ```
32///
33/// For generic newtype struct (note that `Display` bounds are applied to all type parameters):
34///
35/// ```
36/// # use impl_more::forward_display;
37/// struct Baz<T>(T);
38///
39/// impl_more::forward_display!(<T> in Baz<T>);
40///
41/// assert_eq!(Baz(42u64).to_string(), "42");
42/// ```
43///
44/// [`Display`]: core::fmt::Display
45#[macro_export]
46macro_rules! forward_display {
47    (<$($generic:ident),+> in $this:ty => $field:ident) => {
48        impl <$($generic: ::core::fmt::Display),+> ::core::fmt::Display for $this {
49            fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
50                ::core::fmt::Display::fmt(&self.$field, fmt)
51            }
52        }
53    };
54
55    (<$($generic:ident),+> in $this:ty) => {
56        impl <$($generic: ::core::fmt::Display),+> ::core::fmt::Display for $this {
57            fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
58                ::core::fmt::Display::fmt(&self.0, fmt)
59            }
60        }
61    };
62
63    ($ty:ty) => {
64        impl ::core::fmt::Display for $ty {
65            fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
66                ::core::fmt::Display::fmt(&self.0, fmt)
67            }
68        }
69    };
70
71    ($ty:ty => $field:ident) => {
72        impl ::core::fmt::Display for $ty {
73            fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
74                ::core::fmt::Display::fmt(&self.$field, fmt)
75            }
76        }
77    };
78}
79
80/// Implements [`Display`] for structs using a `format!`-like string constructor.
81///
82/// # Examples
83///
84/// Display implementation can be just a string literal.
85///
86/// ```
87/// # use impl_more::forward_display;
88/// struct Hello;
89/// impl_more::impl_display!(Hello: "hello world");
90/// assert_eq!(Hello.to_string(), "hello world");
91/// ```
92///
93/// Explicit and inline format args are supported.
94///
95/// ```
96/// # use impl_more::forward_display;
97/// struct Hello2;
98/// impl_more::impl_display!(Hello2: "hello world {}", 2);
99/// assert_eq!(Hello2.to_string(), "hello world 2");
100///
101/// const HI: &str = "hello";
102///
103/// struct Hello3;
104/// impl_more::impl_display!(Hello3: "{HI} world");
105/// assert_eq!(Hello3.to_string(), "hello world");
106/// ```
107///
108/// [`Display`]: core::fmt::Display
109#[macro_export]
110macro_rules! impl_display {
111    // no format args
112    ($ty:ty: $format:literal) => {
113        impl ::core::fmt::Display for $ty {
114            fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
115                ::core::write!(fmt, $format)
116            }
117        }
118    };
119
120    // with explicit format args
121    ($ty:ty: $format:literal, $($args:expr),+) => {
122        impl ::core::fmt::Display for $ty {
123            fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
124                ::core::write!(fmt, $format, $($args),+)
125            }
126        }
127    };
128
129    // strip trailing comma and forward to format args branch
130    ($ty:ty: $format:literal, $($args:expr),+ ,) => {
131        $crate::impl_display!($ty: $format, $($args),+);
132    };
133}
134
135/// Implements [`Display`] for enums using a static string or format args for each variant.
136///
137/// Unit, tuple, and named-field variants can be mixed. Field patterns use Rust syntax, including
138/// renamed fields, `_`, and `..`. The patterns must cover every variant.
139///
140/// Data variants accept format strings with captured fields and explicit positional or named
141/// arguments. Put the format string and explicit arguments in parentheses. Bare strings on unit
142/// variants are written verbatim; braces are not interpreted. Use parentheses to format a unit
143/// variant's output, including captured values.
144///
145/// Outer width, precision, and alignment are not applied to the complete output. Generic parameter
146/// declarations and match guards are not supported.
147///
148/// # Examples
149///
150/// ```
151/// # extern crate alloc;
152/// use impl_more::impl_display_enum;
153///
154/// enum Foo {
155///     Bar,
156///     Qux,
157/// }
158///
159/// impl_display_enum!(Foo: Bar => "bar", Qux => "qux");
160///
161/// assert_eq!(Foo::Bar.to_string(), "bar");
162/// assert_eq!(Foo::Qux.to_string(), "qux");
163///
164/// enum CoordOrMsg {
165///     Coord(i64, i64),
166///     Msg(&'static str),
167/// }
168///
169/// impl_display_enum!(CoordOrMsg: Coord(x, y) => "{x}, {y}", Msg(msg) => "message: {msg}");
170///
171/// assert_eq!(CoordOrMsg::Coord(4, 2).to_string(), "4, 2");
172/// assert_eq!(CoordOrMsg::Msg("hi").to_string(), "message: hi");
173/// ```
174///
175/// Mix variant shapes and compute format arguments:
176///
177/// ```
178/// use impl_more::impl_display_enum;
179///
180/// enum Event {
181///     Idle,
182///     Items(Vec<u8>),
183///     Progress { completed: usize, total: usize },
184/// }
185///
186/// impl_display_enum! {
187///     Event:
188///     Idle => "idle",
189///     Items(items) => ("{} items", items.len()),
190///     Progress { completed: count, .. } => "{count} complete",
191/// }
192///
193/// assert_eq!(Event::Items(vec![1, 2]).to_string(), "2 items");
194/// assert_eq!(Event::Progress { completed: 2, total: 3 }.to_string(), "2 complete");
195/// ```
196///
197/// Explicit format arguments must be inside the parentheses:
198///
199/// ```compile_fail
200/// enum Event { Items(Vec<u8>) }
201/// impl_more::impl_display_enum!(Event: Items(items) => "{} items", items.len());
202/// ```
203///
204/// [`Display`]: core::fmt::Display
205#[macro_export]
206macro_rules! impl_display_enum {
207    (
208        $ty:ty:
209        $(
210            $variant:ident
211            $( ( $($tuple:tt)* ) )?
212            $( { $($named:tt)* } )?
213            => $output:tt
214        ),+
215        $(,)?
216    ) => {
217        impl ::core::fmt::Display for $ty {
218            fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
219                match self {
220                    $(
221                        Self::$variant
222                        $( ( $($tuple)* ) )?
223                        $( { $($named)* } )?
224                        => $crate::impl_display_enum!(
225                            @write fmt
226                            [
227                                $( ( $($tuple)* ) )?
228                                $( { $($named)* } )?
229                            ]
230                            $output
231                        ),
232                    )+
233                }
234            }
235        }
236    };
237
238    // Bare unit-variant strings retain their verbatim output.
239    (
240        @write $fmt:ident
241        []
242        $text:literal
243    ) => {
244        $fmt.write_str($text)
245    };
246
247    (
248        @write $fmt:ident
249        [ $($fields:tt)* ]
250        $format:literal
251    ) => {
252        ::core::write!($fmt, $format)
253    };
254
255    (
256        @write $fmt:ident
257        [ $($fields:tt)* ]
258        ( $format:literal $($args:tt)* )
259    ) => {
260        ::core::write!($fmt, $format $($args)*)
261    };
262}
263
264#[cfg(test)]
265mod tests {
266    use alloc::{
267        borrow::ToOwned as _,
268        string::{String, ToString as _},
269    };
270
271    #[test]
272    fn impl_forward_for_newtype_struct() {
273        struct Foo(String);
274
275        forward_display!(Foo);
276
277        assert_eq!(Foo("hello world".to_owned()).to_string(), "hello world");
278    }
279
280    #[test]
281    fn impl_forward_newtype_named_struct() {
282        struct Foo {
283            inner: u64,
284        }
285
286        forward_display!(Foo => inner);
287
288        assert_eq!(Foo { inner: 42 }.to_string(), "42");
289    }
290
291    #[test]
292    fn impl_forward_generic_newtype_struct() {
293        struct Foo<T>(T);
294
295        forward_display!(<T> in Foo<T>);
296
297        assert_eq!(Foo(42).to_string(), "42");
298    }
299
300    #[test]
301    fn impl_forward_generic_named_struct() {
302        struct Foo<T> {
303            inner: T,
304        }
305
306        forward_display!(<T> in Foo<T> => inner);
307
308        assert_eq!(Foo { inner: 42 }.to_string(), "42");
309    }
310
311    #[test]
312    fn impl_basic_for_unit_struct() {
313        struct Foo;
314        impl_display!(Foo: "foo");
315        assert_eq!(Foo.to_string(), "foo");
316    }
317
318    #[test]
319    fn impl_basic_with_args() {
320        struct Foo;
321        impl_display!(Foo: "foo {} {}", 2, 3);
322        assert_eq!(Foo.to_string(), "foo 2 3");
323    }
324
325    #[test]
326    fn impl_basic_with_inline_args() {
327        const HI: &str = "hello";
328
329        struct Hello3;
330        impl_display!(Hello3: "{HI} world");
331        assert_eq!(Hello3.to_string(), "hello world");
332    }
333
334    #[test]
335    fn impl_enum_named_variant_with_trailing_comma() {
336        enum Foo {
337            Bar { value: u64 },
338        }
339
340        impl_display_enum!(Foo: Bar { value } => "{value}",);
341
342        assert_eq!(Foo::Bar { value: 42 }.to_string(), "42");
343    }
344
345    #[test]
346    fn enum_mixed_patterns_and_format_arguments() {
347        #[allow(dead_code)] // These fields test patterns that ignore data.
348        enum Event {
349            Idle,
350            Message(&'static str, bool),
351            Progress { completed: usize, total: usize },
352            Items(&'static [u8]),
353            Done,
354        }
355
356        impl_display_enum! {
357            Event:
358            Idle => "{{idle}}",
359            Message(msg, ..) => "{{{msg}}}",
360            Progress { completed: count, .. } => "{count} complete",
361            Items(items) => ("{} items ({label})", items.len(), label = "ready",),
362            Done => ("done: {}", 42),
363        }
364
365        assert_eq!(Event::Idle.to_string(), "{{idle}}");
366        assert_eq!(Event::Message("hi", true).to_string(), "{hi}");
367        assert_eq!(
368            Event::Progress {
369                completed: 2,
370                total: 3
371            }
372            .to_string(),
373            "2 complete"
374        );
375        assert_eq!(Event::Items(&[1, 2]).to_string(), "2 items (ready)");
376        assert_eq!(Event::Done.to_string(), "done: 42");
377    }
378
379    #[test]
380    fn enum_format_arguments_without_trailing_comma() {
381        #[allow(dead_code)] // The first tuple field tests the wildcard pattern.
382        enum Value {
383            Tuple(u8, u8),
384            Named { value: u8 },
385        }
386
387        impl_display_enum!(Value:
388            Tuple(_, value) => "{value:02x}",
389            Named { value } => ("{number:02x}", number = value)
390        );
391
392        assert_eq!(Value::Tuple(0, 10).to_string(), "0a");
393        assert_eq!(Value::Named { value: 11 }.to_string(), "0b");
394
395        enum Count {
396            Items(&'static [u8]),
397        }
398        impl_display_enum!(Count: Items(items) => ("{}", items.len()));
399        assert_eq!(Count::Items(&[1, 2]).to_string(), "2");
400    }
401
402    #[test]
403    fn enum_grouped_unit_string_supports_captures() {
404        const LABEL: &str = "ready";
405        enum State {
406            Bare,
407            Grouped,
408        }
409        impl_display_enum!(State:
410            Bare => "{LABEL}",
411            Grouped => ("{LABEL}"),
412        );
413        assert_eq!(State::Bare.to_string(), "{LABEL}");
414        assert_eq!(State::Grouped.to_string(), "ready");
415    }
416
417    #[test]
418    fn enum_propagates_format_errors() {
419        struct Fails;
420        impl core::fmt::Display for Fails {
421            fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
422                Err(core::fmt::Error)
423            }
424        }
425
426        enum Value {
427            Tuple(Fails),
428            Named { value: Fails },
429        }
430        impl_display_enum!(Value: Tuple(value) => "{value}", Named { value } => "{value}");
431
432        let mut output = String::new();
433        core::fmt::write(&mut output, format_args!("{}", Value::Tuple(Fails))).unwrap_err();
434        core::fmt::write(
435            &mut output,
436            format_args!("{}", Value::Named { value: Fails }),
437        )
438        .unwrap_err();
439    }
440}