I'm trying to find some generic way to get the variant name of an enum as &str
, but in a restricted coding place where I cannot import another crate (so proc macros are off limits, since they require to be in their own crate), and Debug/Display are both used for something else, and I've to also not be using heap allocations (because I've to cover the case when I may be inside a fork
-ed process).
I've found this (seemingly)hacky way of doing it with macro_rules!
but it can only do enums that have either only tuple variants, OR only unit variants & struct variants. So it can't handle an enum that has all three. Can it be done though?(within the aforementioned constraints) that is my question. EDIT: yes, see at end of post. It's hacky though.
And I did this with ChatGPT 3.5 (the alternative being, none at all, so it's better with it than not at all, for me anyway).
Here's on playground complete code, or this is the macro:
// Define the VariantName trait
trait VariantNameAsStr {
fn variant_name_as_str(&self) -> &str;
}
macro_rules! enum_str {
//XXX: arm matches unit varians like Red and struct variants like Red { field1: i32, field2: i64, }, and a mixture of both is supported!
($(#[$attr:meta])* $vis:vis enum $name:ident $(<$($gen:ident),*>)?, $($variant:ident $({ $($field:ident: $ftype:ty),* $(,)? })?),* $(,)?) => {
$(#[$attr])*
$vis enum $name $(<$($gen),*>)? {
$(
$variant $({ $($field: $ftype),* })?
),*
}//enum
impl $(<$($gen),*>)? VariantNameAsStr for $name $(<$($gen),*>)? {
fn variant_name_as_str(&self) -> &str {
match self {
$(
// Handle variants with fields
Self::$variant $({ $($field: _),* })? => stringify!($variant),
//Self::$variant $({..})? => stringify!($variant),
)*
}
}//fn
}//impl
};
//XXX: arm matches only tuple variants eg. Red(i32,i64,i128) but not Red, nor Red { field:i32 }, so you can't mix them!
($(#[$attr:meta])* $vis:vis enum $name:ident $(<$($gen:ident),*>)?, $($variant:ident $(($($ftype:ty),* $(,)? ))?),* $(,)?) => {
$(#[$attr])*
$vis enum $name $(<$($gen),*>)? {
$(
$variant $(($($ftype),*))?,
)*
}//enum
impl $(<$($gen),*>)? VariantNameAsStr for $name $(<$($gen),*>)? {
fn variant_name_as_str(&self) -> &str {
match self {
$(
Self::$variant(..) => stringify!($variant),
)*
}
}
}//impl
};//arm
} //macro
and example use:
enum_str! {
pub enum Color,
Red, Green, Blue,
StructVariant1 {
field1: i32,
},
//TupleVariant(i32),//XXX: can't match this here!
}
enum_str! {
pub enum Color2<T,G>,
//Tee { f: i32 }, // if u use this, then the tuple variant below isn't accepted!
Red(T,G), Green(G,i32), Blue(i64,i128,),
//Magenta,//XXX: this isn't accepted here!
//Foo { field1: i32 }, //XXX: this isn't accepted here!
}
fn main() {
let c=Color::Blue;
assert_eq!(c.variant_name_as_str(),"Blue");
let c2=Color2::<i128,&str>::Green("text",2);
assert_eq!(c2.variant_name_as_str(),"Green");
}
And while writing this I've realized that if I get rid of that trait I can actually make that pub const fn variant_name_as_str(&self) -> &str
which is definitely something I'm gonna done(playground)
EDIT: holy smokes, I think I may have found some way!
Ok this seems to work at first glance(playground):
macro_rules! replace_with_2_dots {
($($input:tt)*) => {
..
};
}
macro_rules! enum_str {
($(#[$attr:meta])* $vis:vis enum $name:ident $(<$($gen:ident),*>)?,
$(
$variant:ident
$( ( $($tfield:ty),* $(,)? ) )?
$( { $($sfield:ident: $stype:ty),* $(,)? } )?
),* $(,)?
) => {
$(#[$attr])*
$vis enum $name $(<$($gen),*>)? {
$(
$variant $( ( $($tfield),* ) )?
$( { $($sfield: $stype),* } )?,
)*
}
impl $(<$($gen),*>)? $name $(<$($gen),*>)? {
fn variant_name_as_str(&self) -> &str {
match self {
$(
Self::$variant $( ( replace_with_2_dots!( $($tfield),* ) ) )? $( { $($sfield: _),* } )? => stringify!($variant),
)*
}
}
}
};
}
enum_str! {
pub enum Color,
Red, Green, Blue,
StructVariant1 { field1: i32 },
TupleVariant(i32),
}
enum_str! {
pub enum Color2<T, G>,
Tee { f: i32 },
Red(T, G), Green(G, i32), Blue(i64, i128),
Magenta,
Foo { field1: i32, field2: u8, },
}
fn main() {
let c = Color::Blue;
assert_eq!(c.variant_name_as_str(), "Blue");
let c2 = Color2::<i128, &str>::Green("text", 2);
assert_eq!(c2.variant_name_as_str(), "Green");
let c3 = Color2::<i32,i32>::Magenta;
assert_eq!(c3.variant_name_as_str(), "Magenta");
let c4 = Color2::<u8,u8>::Foo { field1: 42, field2: 18 };
assert_eq!(c4.variant_name_as_str(), "Foo");
let c5 = Color::StructVariant1 { field1: 10 };
assert_eq!(c5.variant_name_as_str(), "StructVariant1");
let c6 = Color::TupleVariant(10);
assert_eq!(c6.variant_name_as_str(), "TupleVariant");
}
expansion is this:
pub enum Color {
Red,
Green,
Blue,
StructVariant1 {
field1: i32,
},
TupleVariant(i32),
}
impl Color {
fn variant_name_as_str(&self) -> &str {
match self {
Self::Red => "Red",
Self::Green => "Green",
Self::Blue => "Blue",
Self::StructVariant1 { field1: _ } => "StructVariant1",
Self::TupleVariant(..) => "TupleVariant",
}
}
}
pub enum Color2<T, G> {
Tee {
f:
i32,
},
Red(T, G),
Green(G, i32),
Blue(i64, i128),
Magenta,
Foo {
field1: i32,
field2: u8,
},
}
impl<T, G> Color2<T, G> {
fn variant_name_as_str(&self) -> &str {
match self {
Self::Tee { f: _ } => "Tee",
Self::Red(..) => "Red",
Self::Green(..) => "Green",
Self::Blue(..) => "Blue",
Self::Magenta => "Magenta",
Self::Foo { field1: _, field2: _ } => "Foo",
}
}
}
I've stumbled upon some inconsistency between rustc
and rust-analyzer
while doing this: `rust-analyzer` shows compile error E0023 but `cargo check`/clippy/build/run do not · Issue #125464 · rust-lang/rust · GitHub
Some improvements(not seen above):
I got rid of matching lone comma, see isolated example here
Tried to match a part of the generics but it requires duplication of matcher block, here
While work-in-progress, updates should be here, after done(if ever), I'll make new post.