Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to let a macro expand to a struct field?

Tags:

macros

rust

I would like to do the following, but macros in that position don’t seem to work (I get error: expected `:`, found `!`. How can I pattern-match individual struct members and attach attributes to them based on the match?

use serde_derive::Serialize;

macro_rules! optional_param {
    ($name:ident : Option<$type:ty>) => { #[serde(skip_serializing_if = "Option::is_none")] pub $name: Option<$ty> };
    ($name:ident : Vec   <$type:ty>) => { #[serde(skip_serializing_if = "Vec::is_empty"  )] pub $name: Vec   <$ty> };
    ($name:ident : bool            ) => { #[serde(skip_serializing_if = "bool::not"      )] pub $name: bool        };
}

macro_rules! impl_extra {
    ( $name:ident { $( $param:ident : $type:ty ),* $(,)* } ) => (
        #[derive(Default,Debug,Serialize)]
        pub struct $name {
            $( optional_param!($param : $type), )*
        }
    );
}

impl_extra!(MyStruct { member: Option<String> });

Link to the playground

like image 412
flying sheep Avatar asked Dec 02 '18 12:12

flying sheep


1 Answers

Indeed, macro invocations are not valid in the middle of a struct definition. However, we can use metavariables there. The trick is to parse the parameters incrementally, building the tokens for the field definitions along the way, and when there's no more input to process, emit a struct definition with the field definitions coming from a metavariable.

As a first step, let's see what a macro that doesn't handle field types specifically looks like:

macro_rules! impl_extra {
    ( @ $name:ident { } -> ($($result:tt)*) ) => (
        #[derive(Default, Debug, Serialize)]
        pub struct $name {
            $($result)*
        }
    );

    ( @ $name:ident { $param:ident : $type:ty, $($rest:tt)* } -> ($($result:tt)*) ) => (
        impl_extra!(@ $name { $($rest)* } -> (
            $($result)*
            pub $param : $type,
        ));
    );

    ( $name:ident { $( $param:ident : $type:ty ),* $(,)* } ) => (
        impl_extra!(@ $name { $($param : $type,)* } -> ());
    );
}

The only thing this macro does is add pub on each field and define a pub struct with a #[derive] attribute. The first rule handles the terminal case, i.e. when there are no more fields to process. The second rule handles the recursive case, and the third rule handles the macro's "public" syntax and transforms it into the "processing" syntax.

Note that I'm using an @ as the initial token for internal rules to distinguish them from "public" rules. If this macro is not meant to be exported to other crates, then you could also move the internal rules to a different macro. If the macro is exported though, then the separate macro for the internal rules might have to be exported too.

Now, let's handle the various field types:

macro_rules! impl_extra {
    ( @ $name:ident { } -> ($($result:tt)*) ) => (
        #[derive(Default, Debug, Serialize)]
        pub struct $name {
            $($result)*
        }
    );

    ( @ $name:ident { $param:ident : Option<$type:ty>, $($rest:tt)* } -> ($($result:tt)*) ) => (
        impl_extra!(@ $name { $($rest)* } -> (
            $($result)*
            #[serde(skip_serializing_if = "Option::is_none")]
            pub $param : Option<$type>,
        ));
    );

    ( @ $name:ident { $param:ident : Vec<$type:ty>, $($rest:tt)* } -> ($($result:tt)*) ) => (
        impl_extra!(@ $name { $($rest)* } -> (
            $($result)*
            #[serde(skip_serializing_if = "Vec::is_empty")]
            pub $param : Vec<$type>,
        ));
    );

    ( @ $name:ident { $param:ident : bool, $($rest:tt)* } -> ($($result:tt)*) ) => (
        impl_extra!(@ $name { $($rest)* } -> (
            $($result)*
            #[serde(skip_serializing_if = "bool::not")]
            pub $param : bool,
        ));
    );

    ( $name:ident { $( $param:ident : $($type:tt)* ),* $(,)* } ) => (
        impl_extra!(@ $name { $($param : $($type)*,)* } -> ());
    );
}

Note that there's a difference in the last rule: instead of matching on a ty, we now match on a sequence of tt. That's because once the macro has parsed a ty, it can't be broken down, so when we make a recursive macro call, a ty cannot possibly match something like Option<$type:ty>.

like image 188
Francis Gagné Avatar answered Nov 15 '22 09:11

Francis Gagné