Reputation: 8962
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> });
Upvotes: 8
Views: 3215
Reputation: 11
@francis-gagné provided a great idea, just with a little flaws.
$($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>),
});
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,
});
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
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
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