rustscyllarust-macros

Creating a Macro in Rust to take a Struct and turning into a Tuple of its field's types in order


Basically, the goal is to be able to take a struct like the following:

struct Test
{
    id : i64,
    name : String,
    data : HashMap<String, String>,
}

And have the macro return a tuple of its field types in the order that we declared the fields of test like follows: So it could be used in the following context for example in scylla::IntoTypedRows;

rows.into_typed::<(get_field_types!(Test))>()

instead of the current way it has to be done which is:

rows.into_typed::<(i64, String, HashMap<String, String>)>()

That way as structs get larger, it does not become tedious or bulky.

The issue I have had has been that I am able to return a Vec<&str> but can't seem to reason how to get it to do the above. Here was my attempt:

macro_rules! get_field_types {
    ($struct_name:ty) => {
        {
            let mut field_types = Vec::new();
            let _ = <$struct_name>::default(); // Ensure the struct is instantiated
            let field_values = stringify!($struct_name {
                $(field: _type,)*
                ..
            });
            let field_pairs = field_values.split(',').filter_map(|field_value| {
                let field_type = field_value.split_whitespace().nth(1)?;
                Some(field_type.trim_end_matches(',').trim())
            });
            field_types.extend(field_pairs);
            field_types
        }
    };
}

Solution

  • PitaJ is correct here that you should be doing this with #[derive(FromRow)] and using rows.into_typed::<Test>(), as it saves you the hassle of maintaining a macro, and makes your struct more flexible.

    In the spirit of learning, here is how you could do this with a declarative macro. While macros can't do lookups currently (and I would be surprised if they ever would be able to), you can apply them at struct declaration like so:

    use paste::paste;
    
    macro_rules! field_tuple {
            (
                $vis_:vis struct $name:ident {
                    $($fvis_:vis $it:ident : $typ:ty, )*
                }
            ) => {
                $vis_ struct $name {
                    $($fvis_ $it: $typ),*
                }
                
                paste! {
                    $vis_ type [<$name FieldTypes>] = ($($typ),*);
                }
            };
    }
    
    enum Color {
        Pink,
        Blue,
        Black,
        White,
    }
    
    field_tuple!(struct Penguin {
        pub height: u32,
        pub(crate) weight: u8,
        color: Color,
    });
    
    #[cfg(test)]
    mod tests {
        use super::*;
        
        #[test]
        fn compiles() {
            let _: PenguinFieldTypes;
        }
    }
    

    This uses the paste crate in order to concatenate the struct name and FieldTypes in order to generate a type definition, so that you can use it to define multiple field-tuple types without conflicting with one another.