From b2aff5b5d92cd91f4c846ac576a0ea98175138ce Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Tue, 2 Jul 2024 09:59:20 +0200 Subject: [PATCH] Support _variant in outer level enum formatting for Display --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 4 +- Cargo.toml | 2 +- README.md | 4 +- clippy.toml | 2 +- impl/Cargo.toml | 2 +- impl/doc/display.md | 22 ++- impl/src/fmt/display.rs | 148 +++++++++++++++--- impl/src/fmt/parsing.rs | 86 +++++++++- .../shared_format_positional_placeholders.rs | 19 +++ ...ared_format_positional_placeholders.stderr | 17 ++ .../display/shared_format_unclosed_brace.rs | 7 + .../shared_format_unclosed_brace.stderr | 9 ++ .../display/shared_format_variant_spec.rs | 7 + .../display/shared_format_variant_spec.stderr | 5 + tests/display.rs | 125 +++++++++++++++ 16 files changed, 427 insertions(+), 34 deletions(-) create mode 100644 tests/compile_fail/display/shared_format_positional_placeholders.rs create mode 100644 tests/compile_fail/display/shared_format_positional_placeholders.stderr create mode 100644 tests/compile_fail/display/shared_format_unclosed_brace.rs create mode 100644 tests/compile_fail/display/shared_format_unclosed_brace.stderr create mode 100644 tests/compile_fail/display/shared_format_variant_spec.rs create mode 100644 tests/compile_fail/display/shared_format_variant_spec.stderr diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 657f25c0..13439b8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - msrv: ["1.65.0"] + msrv: ["1.70.0"] os: - ubuntu - macOS diff --git a/CHANGELOG.md b/CHANGELOG.md index a9805322..95d036f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Breaking changes -- The minimum supported Rust version (MSRV) is now Rust 1.65. +- The minimum supported Rust version (MSRV) is now Rust 1.70. - Add the `std` feature which should be disabled in `no_std` environments. - All Cargo features, except `std`, are now disabled by default. The `full` feature can be used to get the old behavior of supporting all possible @@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). [#294](https://github.com/JelteF/derive_more/pull/294)) - The `as_mut` feature is removed, and the `AsMut` derive is now gated by the `as_ref` feature. ([#295](https://github.com/JelteF/derive_more/pull/295)) +- A top level `#[display("...")]` attribute on an enum now requires the usage + of `{_variant}` to include the variant instead of including it at `{}`. ([#377](https://github.com/JelteF/derive_more/pull/377)) ### Added diff --git a/Cargo.toml b/Cargo.toml index 420f60b3..c734182e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "derive_more" version = "1.0.0-beta.6" edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" description = "Adds #[derive(x)] macros for more traits" authors = ["Jelte Fennema "] license = "MIT" diff --git a/README.md b/README.md index 383f773d..4ce210fc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Latest Version](https://img.shields.io/crates/v/derive_more.svg)](https://crates.io/crates/derive_more) [![Rust Documentation](https://docs.rs/derive_more/badge.svg)](https://docs.rs/derive_more) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/JelteF/derive_more/master/LICENSE) -[![Rust 1.65+](https://img.shields.io/badge/rustc-1.65+-lightgray.svg)](https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html) +[![Rust 1.70+](https://img.shields.io/badge/rustc-1.70+-lightgray.svg)](https://blog.rust-lang.org/2022/11/03/Rust-1.70.0.html) [![Unsafe Forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance) Rust has lots of builtin traits that are implemented for its basic types, such @@ -215,7 +215,7 @@ extern crate derive_more; ## [MSRV] policy -This library requires Rust 1.65 or higher. +This library requires Rust 1.70 or higher. Changing [MSRV] (minimum supported Rust version) of this crate is treated as a **minor version change** in terms of [Semantic Versioning]. - So, if [MSRV] changes are **NOT concerning** for your project, just use the default [caret requirement]: diff --git a/clippy.toml b/clippy.toml index 0c697c31..ca697fd7 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,7 +2,7 @@ # See full lints list at: # https://rust-lang.github.io/rust-clippy/master/index.html -msrv = "1.65.0" +msrv = "1.70.0" # Ensures consistent bracing for macro calls in the codebase. # Extends default settings: diff --git a/impl/Cargo.toml b/impl/Cargo.toml index 8252fdfb..dc6bcf53 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -2,7 +2,7 @@ name = "derive_more-impl" version = "1.0.0-beta.6" edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" description = "Internal implementation of `derive_more` crate" authors = ["Jelte Fennema "] license = "MIT" diff --git a/impl/doc/display.md b/impl/doc/display.md index 5c242bdf..6a7139e6 100644 --- a/impl/doc/display.md +++ b/impl/doc/display.md @@ -26,6 +26,12 @@ The variables available in the arguments is `self` and each member of the varian with members of tuple structs being named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc. +For enums you can also specify a shared format on the enum itself instead of +the variant. This format is used for each of the variants, and can be +customized per variant by including the special `{_variant}` placeholder in +this shared format, which is then replaced by the format string that's provided +on the variant. + ### Other formatting traits @@ -175,6 +181,7 @@ struct Point2D { } #[derive(Display)] +#[display("Enum E: {_variant}")] enum E { Uint(u32), #[display("I am B {:b}", i)] @@ -185,6 +192,13 @@ enum E { Path(PathBuf), } +#[derive(Display)] +#[display("Enum E2: {_0:?}")] +enum E2 { + Uint(u32), + String(&'static str, &'static str), +} + #[derive(Display)] #[display("Hello there!")] union U { @@ -223,9 +237,11 @@ impl PositiveOrNegative { assert_eq!(MyInt(-2).to_string(), "-2"); assert_eq!(Point2D { x: 3, y: 4 }.to_string(), "(3, 4)"); -assert_eq!(E::Uint(2).to_string(), "2"); -assert_eq!(E::Binary { i: -2 }.to_string(), "I am B 11111110"); -assert_eq!(E::Path("abc".into()).to_string(), "I am C abc"); +assert_eq!(E::Uint(2).to_string(), "Enum E: 2"); +assert_eq!(E::Binary { i: -2 }.to_string(), "Enum E: I am B 11111110"); +assert_eq!(E::Path("abc".into()).to_string(), "Enum E: I am C abc"); +assert_eq!(E2::Uint(2).to_string(), "Enum E2: 2"); +assert_eq!(E2::String("shown", "ignored").to_string(), "Enum E2: \"shown\""); assert_eq!(U { i: 2 }.to_string(), "Hello there!"); assert_eq!(format!("{:o}", S), "7"); assert_eq!(format!("{:X}", UH), "UpperHex"); diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index e41dcbd1..eaf66c4a 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -9,7 +9,7 @@ use syn::{parse_quote, spanned::Spanned as _}; use crate::utils::{attr::ParseMultiple as _, Spanning}; -use super::{trait_name_to_attribute_name, ContainerAttributes}; +use super::{parsing, trait_name_to_attribute_name, ContainerAttributes, FmtAttribute}; /// Expands a [`fmt::Display`]-like derive macro. /// @@ -80,6 +80,7 @@ fn expand_struct( (attrs, ident, trait_ident, _): ExpansionCtx<'_>, ) -> syn::Result<(Vec, TokenStream)> { let s = Expansion { + shared_format: None, attrs, fields: &s.fields, trait_ident, @@ -110,12 +111,8 @@ fn expand_struct( /// Expands a [`fmt`]-like derive macro for the provided enum. fn expand_enum( e: &syn::DataEnum, - (attrs, _, trait_ident, attr_name): ExpansionCtx<'_>, + (shared_attrs, _, trait_ident, attr_name): ExpansionCtx<'_>, ) -> syn::Result<(Vec, TokenStream)> { - if attrs.fmt.is_some() { - todo!("https://github.com/JelteF/derive_more/issues/142"); - } - let (bounds, match_arms) = e.variants.iter().try_fold( (Vec::new(), TokenStream::new()), |(mut bounds, mut arms), variant| { @@ -138,6 +135,7 @@ fn expand_enum( } let v = Expansion { + shared_format: shared_attrs.fmt.as_ref(), attrs: &attrs, fields: &variant.fields, trait_ident, @@ -198,6 +196,9 @@ fn expand_union( /// [`Display::fmt()`]: fmt::Display::fmt() #[derive(Debug)] struct Expansion<'a> { + /// Format shared between all variants of an enum. + shared_format: Option<&'a FmtAttribute>, + /// Derive macro [`ContainerAttributes`]. attrs: &'a ContainerAttributes, @@ -226,6 +227,79 @@ impl<'a> Expansion<'a> { /// [`Display::fmt()`]: fmt::Display::fmt() /// [`FmtAttribute`]: super::FmtAttribute fn generate_body(&self) -> syn::Result { + if self.shared_format.is_none() { + return self.generate_body_impl(); + } + let shared_format = self.shared_format.as_ref().unwrap(); + if !shared_format.args.is_empty() { + return Err(syn::Error::new( + shared_format.args.span(), + "shared format string does not support positional placeholders, use named placeholders instead", + )); + } + let mut tokens = TokenStream::new(); + let mut maybe_body = None; + let mut current_format = String::new(); + let fmt_string = shared_format.lit.value(); + let maybe_parts = parsing::format_parts(&fmt_string); + let Some(parts) = maybe_parts else { + // If we could not parse the format string, we just use the original string so + // we get a nice error message. We also assert false so we get a compile + // error. + return Ok(quote! { + derive_more::core::write!(__derive_more_f, #shared_format); + panic!("derive_more could not parse shared format string, but rust could: {:?}", #fmt_string); + }); + }; + for part in parts { + match part { + parsing::FormatPart::Text(s) => { + current_format.push_str(s); + } + parsing::FormatPart::Format { raw, format } => { + if format.arg == Some(parsing::Argument::Identifier("_variant")) { + if format.spec.is_some() { + return Err(syn::Error::new( + shared_format.span(), + "shared format _variant placehold cannot contain format specifiers", + )); + } + if !current_format.is_empty() { + tokens.extend(quote! { derive_more::core::write!(__derive_more_f, #current_format)?; }); + current_format.clear(); + } + if maybe_body.is_none() { + maybe_body = Some(self.generate_body_impl()?); + } + let body = maybe_body.as_ref().unwrap(); + tokens.extend(quote! { #body?; }); + } else { + if format.arg.is_none() || matches!(format.arg, Some(parsing::Argument::Integer(_))) { + return Err(syn::Error::new( + shared_format.span(), + "shared format string cannot contain positional placeholders, use named placeholders instead", + )); + } + current_format.push_str(raw); + } + } + }; + } + if !current_format.is_empty() { + tokens.extend( + quote! { derive_more::core::write!(__derive_more_f, #current_format) }, + ) + } else { + tokens.extend(quote! { Ok(()) }); + } + Ok(tokens) + } + + /// Generates [`Display::fmt()`] implementation for a struct or an enum variant + /// without considering `shared_format`. + /// + /// [`Display::fmt()`]: fmt::Display::fmt() + fn generate_body_impl(&self) -> syn::Result { match &self.attrs.fmt { Some(fmt) => { Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() { @@ -267,27 +341,55 @@ impl<'a> Expansion<'a> { /// Generates trait bounds for a struct or an enum variant. fn generate_bounds(&self) -> Vec { + let mut bounds: Vec = + if let Some(shared_format) = self.shared_format { + let shared_bounds = shared_format + .bounded_types(self.fields) + .map(|(ty, trait_name)| { + let trait_ident = format_ident!("{trait_name}"); + + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + }) + .chain(self.attrs.bounds.0.clone()) + .collect(); + // If it doesn't contain _variant we don't need to add any other bounds + if !parsing::format_string(&shared_format.lit.value()) + .into_iter() + .flat_map(|f| f.formats) + .any(|f| f.arg == Some(parsing::Argument::Identifier("_variant"))) + { + return shared_bounds; + } + shared_bounds + } else { + Vec::new() + }; + let Some(fmt) = &self.attrs.fmt else { - return self - .fields - .iter() - .next() - .map(|f| { - let ty = &f.ty; - let trait_ident = &self.trait_ident; - vec![parse_quote! { #ty: derive_more::core::fmt::#trait_ident }] - }) - .unwrap_or_default(); + bounds.extend( + self.fields + .iter() + .next() + .map(|f| { + let ty = &f.ty; + let trait_ident = &self.trait_ident; + vec![parse_quote! { #ty: derive_more::core::fmt::#trait_ident }] + }) + .unwrap_or_default(), + ); + return bounds; }; - fmt.bounded_types(self.fields) - .map(|(ty, trait_name)| { - let trait_ident = format_ident!("{trait_name}"); + bounds.extend( + fmt.bounded_types(self.fields) + .map(|(ty, trait_name)| { + let trait_ident = format_ident!("{trait_name}"); - parse_quote! { #ty: derive_more::core::fmt::#trait_ident } - }) - .chain(self.attrs.bounds.0.clone()) - .collect() + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + }) + .chain(self.attrs.bounds.0.clone()), + ); + bounds } } diff --git a/impl/src/fmt/parsing.rs b/impl/src/fmt/parsing.rs index 8a5d7527..73acc849 100644 --- a/impl/src/fmt/parsing.rs +++ b/impl/src/fmt/parsing.rs @@ -207,6 +207,26 @@ pub(crate) fn format_string(input: &str) -> Option> { input.is_empty().then_some(FormatString { formats }) } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) enum FormatPart<'a> { + Format { raw: &'a str, format: Format<'a> }, + Text(&'a str), +} + +pub(crate) fn format_parts(mut input: &str) -> Option> { + let parts = iter::repeat(()) + .scan(&mut input, |input, _| { + let (curr, format) = alt(&mut [ + &mut format_part, + &mut map(text, |(i, x)| (i, FormatPart::Text(x))), + ])(input)?; + **input = curr; + Some(format) + }) + .collect(); + input.is_empty().then_some(parts) +} + /// Parses a `maybe_format` as defined in the [grammar spec][0]. /// /// # Grammar @@ -233,6 +253,17 @@ fn maybe_format(input: &str) -> Option<(LeftToParse<'_>, MaybeFormat<'_>)> { ])(input) } +fn format_part(input: &str) -> Option<(LeftToParse<'_>, FormatPart<'_>)> { + alt(&mut [ + &mut map(str("{{"), |i| (i, FormatPart::Text("{{"))), + &mut map(str("}}"), |i| (i, FormatPart::Text("}}"))), + &mut map(format, |(i, format)| { + let raw = &input[..input.len() - i.len()]; + (i, FormatPart::Format { raw, format }) + }), + ])(input) +} + /// Parses a `format` as defined in the [grammar spec][0]. /// /// # Grammar @@ -1261,7 +1292,7 @@ mod tests { } #[test] - fn full() { + fn full_format() { assert_eq!( format_string("prefix{{{0:#?}postfix{par:-^par$.a$}}}"), Some(FormatString { @@ -1297,6 +1328,51 @@ mod tests { ); } + #[test] + fn full_parts() { + assert_eq!( + format_parts("prefix{{{0:#?}postfix{par:-^par$.a$}}}"), + Some(vec![ + FormatPart::Text("prefix"), + FormatPart::Text("{{"), + FormatPart::Format { + raw: "{0:#?}", + format: Format { + arg: Some(Argument::Integer(0)), + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: Some(Alternate), + zero_padding: None, + width: None, + precision: None, + ty: Type::Debug, + }), + } + }, + FormatPart::Text("postfix"), + FormatPart::Format { + raw: "{par:-^par$.a$}", + format: Format { + arg: Some(Argument::Identifier("par")), + spec: Some(FormatSpec { + align: Some((Some('-'), Align::Center)), + sign: None, + alternate: None, + zero_padding: None, + width: Some(Count::Parameter(Argument::Identifier("par"))), + precision: Some(Precision::Count(Count::Parameter( + Argument::Identifier("a"), + ))), + ty: Type::Display, + }), + } + }, + FormatPart::Text("}}"), + ]), + ); + } + #[test] fn error() { assert_eq!(format_string("{"), None); @@ -1307,5 +1383,13 @@ mod tests { assert_eq!(format_string("{:q}"), None); assert_eq!(format_string("{:par}"), None); assert_eq!(format_string("{⚙️}"), None); + assert_eq!(format_parts("{"), None); + assert_eq!(format_parts("}"), None); + assert_eq!(format_parts("{{}"), None); + assert_eq!(format_parts("{:x?"), None); + assert_eq!(format_parts("{:.}"), None); + assert_eq!(format_parts("{:q}"), None); + assert_eq!(format_parts("{:par}"), None); + assert_eq!(format_parts("{⚙️}"), None); } } diff --git a/tests/compile_fail/display/shared_format_positional_placeholders.rs b/tests/compile_fail/display/shared_format_positional_placeholders.rs new file mode 100644 index 00000000..8d2a0ff2 --- /dev/null +++ b/tests/compile_fail/display/shared_format_positional_placeholders.rs @@ -0,0 +1,19 @@ +#[derive(derive_more::Display)] +#[display("Stuff({})")] +enum Foo { + A, +} + +#[derive(derive_more::Display)] +#[display("Stuff({0})")] +enum Foo2 { + A, +} + +#[derive(derive_more::Display)] +#[display("Stuff()", _0, _2)] +enum Foo3 { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_positional_placeholders.stderr b/tests/compile_fail/display/shared_format_positional_placeholders.stderr new file mode 100644 index 00000000..d9f2d523 --- /dev/null +++ b/tests/compile_fail/display/shared_format_positional_placeholders.stderr @@ -0,0 +1,17 @@ +error: shared format string cannot contain positional placeholders, use named placeholders instead + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:2:11 + | +2 | #[display("Stuff({})")] + | ^^^^^^^^^^^ + +error: shared format string cannot contain positional placeholders, use named placeholders instead + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:8:11 + | +8 | #[display("Stuff({0})")] + | ^^^^^^^^^^^^ + +error: shared format string does not support positional placeholders, use named placeholders instead + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:14:22 + | +14 | #[display("Stuff()", _0, _2)] + | ^^ diff --git a/tests/compile_fail/display/shared_format_unclosed_brace.rs b/tests/compile_fail/display/shared_format_unclosed_brace.rs new file mode 100644 index 00000000..47134e25 --- /dev/null +++ b/tests/compile_fail/display/shared_format_unclosed_brace.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display("Stuff({)")] +enum Foo { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_unclosed_brace.stderr b/tests/compile_fail/display/shared_format_unclosed_brace.stderr new file mode 100644 index 00000000..4d4861f3 --- /dev/null +++ b/tests/compile_fail/display/shared_format_unclosed_brace.stderr @@ -0,0 +1,9 @@ +error: invalid format string: expected `'}'`, found `')'` + --> tests/compile_fail/display/shared_format_unclosed_brace.rs:2:19 + | +2 | #[display("Stuff({)")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` diff --git a/tests/compile_fail/display/shared_format_variant_spec.rs b/tests/compile_fail/display/shared_format_variant_spec.rs new file mode 100644 index 00000000..9ad647ad --- /dev/null +++ b/tests/compile_fail/display/shared_format_variant_spec.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display("Stuff({_variant:?})")] +enum Foo { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_variant_spec.stderr b/tests/compile_fail/display/shared_format_variant_spec.stderr new file mode 100644 index 00000000..c4d0049a --- /dev/null +++ b/tests/compile_fail/display/shared_format_variant_spec.stderr @@ -0,0 +1,5 @@ +error: shared format _variant placehold cannot contain format specifiers + --> tests/compile_fail/display/shared_format_variant_spec.rs:2:11 + | +2 | #[display("Stuff({_variant:?})")] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/display.rs b/tests/display.rs index 1e7ddfc1..ebc0012e 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -1283,6 +1283,131 @@ mod enums { } } } + + mod shared_format { + use super::*; + mod single { + use super::*; + + #[derive(Display)] + #[display("Variant: {_variant}")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + } + + #[test] + fn assert() { + assert_eq!(Enum::A(1).to_string(), "Variant: A 1"); + assert_eq!(Enum::B { field: 2 }.to_string(), "Variant: B 2",); + assert_eq!(Enum::C.to_string(), "Variant: C",); + } + } + + mod multiple { + use super::*; + + #[derive(Display)] + #[display("{_variant} Variant: {_variant} {_variant}")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + } + + #[test] + fn assert() { + assert_eq!(Enum::A(1).to_string(), "A 1 Variant: A 1 A 1"); + assert_eq!( + Enum::B { field: 2 }.to_string(), + "B 2 Variant: B 2 B 2", + ); + assert_eq!(Enum::C.to_string(), "C Variant: C C",); + } + } + + mod none { + use super::*; + + /// Make sure that variant specific bounds are not added if _variant is + /// not used. + struct NoDisplay; + + #[derive(Display)] + #[display("Variant")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + D(T), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "Variant"); + assert_eq!( + Enum::::B { field: 2 }.to_string(), + "Variant", + ); + assert_eq!(Enum::::C.to_string(), "Variant",); + assert_eq!(Enum::::D(NoDisplay).to_string(), "Variant",); + } + } + + mod use_field { + use super::*; + + #[derive(Display)] + #[display("Variant {_0}")] + enum Enum { + A(i32), + B(&'static str), + C(T), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "Variant 1"); + assert_eq!(Enum::::B("abc").to_string(), "Variant abc",); + assert_eq!(Enum::::C(9).to_string(), "Variant 9",); + } + } + + mod use_field_and_variant { + use super::*; + + #[derive(Display)] + #[display("Variant {_variant} {_0}")] + enum Enum { + #[display("A")] + A(i32), + #[display("B")] + B(&'static str), + #[display("C")] + C(T), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "Variant A 1"); + assert_eq!(Enum::::B("abc").to_string(), "Variant B abc",); + assert_eq!(Enum::::C(9).to_string(), "Variant C 9",); + } + } + } } }