flying sheep
flying sheep

Reputation: 8962

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

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

Upvotes: 8

Views: 3215

Answers (2)

kukuro
kukuro

Reputation: 11

@francis-gagné provided a great idea, just with a little flaws.

Multiple Fields

$($type:tt)* will match all codes except brackets{}/()/[], so it works with only one field like

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

As a result, multiple fields like

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

will throw an error:

error: local ambiguity when calling macro `impl_extra`: multiple parsing options: built-in NTs tt ('r#type') or 2 other options.
  --> src\main.rs:73:47
   |
73 | impl_extra!(MyStruct1 { member: Option<String>, member2: Option<String> });
   |                                               ^

As a solution, the last part can be replaced with the following code:

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

Then multiple fields can be accepted like this:

impl_extra!(MyStruct {
    name(Option<bool>),
    name2(Vec<bool>),
});

Default handle

There is no necessary to write all types by hand. Add this code block to add default handle:

     ( @ $name:ident { $param:ident : $default:tt, $($rest:tt)* } -> ($($result:tt)*) ) => (
        impl_extra!(@ $name { $($rest)* } -> (
            $($result)*
            pub $param : $default,
        ));
    );
impl_extra!(MyStruct {
    name(Option<bool>),
    //specialization handle -> #[serde(skip_serializing_if = "Option::is_none")]
    name2(String), 
    //default handle ->  pub name2: String,
    name3(AnyType), 
    //default handle ->  pub name3: AnyType,
});

bool::not

Not very important, bool::not cannot be used on bool directly, an error will be thrown:

32 |           #[derive(Default, Debug, Serialize)]
   |                                    ^^^^^^^^^ expected `bool`, found `&bool`
...
57 |               #[serde(skip_serializing_if = "bool::not")]
   |                                             ----------- arguments to this function are incorrect

Final Code

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 : $default:tt, $($rest:tt)* } -> ($($result:tt)*) ) => (
        impl_extra!(@ $name { $($rest)* } -> (
            $($result)*
            pub $param : $default,
        ));
    );

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

impl_extra!(MyStruct {
    name(Option<bool>),
    name2(String),
});

Upvotes: 1

Francis Gagn&#233;
Francis Gagn&#233;

Reputation: 65887

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>.

Upvotes: 10

Related Questions