Search code examples
rust

How to implement a trait with a macro only if a struct has a field


I want to always call a macro on any struct, but only implement TryInto<u8> in the case where such struct has a field named ˋstatusˋ:

#[macro_export]
macro_rules! impl_status {
    (
        struct $name:ident {
            status: $status_type:ty,
            $($rest:tt)*
        }
    ) => {
        #[allow(dead_code)]
        impl TryInto<u8> for $name {
            type Error = ();

            fn try_into(
                self,
            ) -> Result<u8, Self::Error> {
               todo!()
            }
        }
    };
    (
        struct $name:ident {
            $($field:ident : $type:ty),*
        }
    ) => {
        impl TryInto<()> for $name {
            type Error = ();

            fn try_into(self) -> Result<(), Self::Error> {
                Ok(())
            }
        }
    };
}

pub struct HasStatusStruct {
    status: u8
}

impl_status!{HasStatusStruct}

I know why this does not work: ˋHasStatusStructˋ is purely a name and not a struct definition.

How do I solve this?


Solution

  • The pattern you give to the macro must actually match one of the macro's arms. The patterns are struct definitions, not just struct names. If you therefore pass your full struct definition to the macro, it will match.

    Note that your macro will also need to emit the struct definition.

    For example:

    #[macro_export]
    macro_rules! impl_status {
        (
            struct $name:ident {
                status: $status_type:ty,
                $($rest:tt)*
            }
        ) => {
            struct $name {
                status: $status_type,
                $($rest)*
            }
            
            #[allow(dead_code)]
            impl TryInto<u8> for $name {
                type Error = ();
    
                fn try_into(
                    self,
                ) -> Result<u8, Self::Error> {
                   todo!()
                }
            }
        };
        (
            struct $name:ident {
                $($field:ident : $type:ty),*
            }
        ) => {
            struct $name {
                $($field : $type),*
            }
            
            impl TryInto<()> for $name {
                type Error = ();
    
                fn try_into(self) -> Result<(), Self::Error> {
                    Ok(())
                }
            }
        };
    }
    
    impl_status! {
        struct HasStatusStruct {
            status: u8,
        }
    }
    

    Note that I had to remove pub from the struct because the patterns didn't call for it. You could fix this by taking an optional vis-type token, for example.

    Further, note that the patterns you match preclude the possibility of specifying macros on either the struct or the fields. For example, this makes applying derive macros to the generated struct impossible, unless you account for that syntax in your macro.

    You should consider writing a derive macro to handle this case instead, as it doesn't require you to specify the entire struct definition syntax in your macro.


    As a side note, you should always prefer implementing From and TryFrom over Into and TryInto. In this case you should impl TryFrom<$name> for u8 and impl TryFrom<$name> for () instead.