Действительно, вызовы макросов недопустимы в середине определения структуры.Тем не менее, мы можем использовать метавариабельные там.Хитрость заключается в том, чтобы анализировать параметры постепенно , создавая токены для определений полей по пути, и, когда больше нет входных данных для обработки, генерировать определение структуры с определениями полей, полученными из мета-переменной.
В качестве первого шага давайте посмотрим, как конкретно выглядит макрос, который не обрабатывает типы полей:
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,)* } -> ());
);
}
Единственное, что делает этот макрос, это добавляет pub
в каждое поле и определяетpub struct
с атрибутом #[derive]
.Первое правило обрабатывает случай терминала, т.е. когда больше нет полей для обработки.Второе правило обрабатывает рекурсивный регистр, а третье правило обрабатывает открытый синтаксис макроса и преобразует его в синтаксис «обработки».
Обратите внимание, что я использую @
в качестве исходного токена длявнутренние правила, чтобы отличать их от «публичных» правил.Если этот макрос не предназначен для экспорта в другие ящики, вы также можете переместить внутренние правила в другой макрос.Если макрос экспортируется, то, возможно, придется экспортировать и отдельный макрос для внутренних правил.
Теперь давайте разберемся с различными типами полей:
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)*,)* } -> ());
);
}
Обратите внимание, что естьРазница в последнем правиле: вместо сопоставления на ty
мы теперь сопоставляем на последовательности tt
.Это потому, что когда макрос проанализировал ty
, он не может быть разбит, поэтому, когда мы делаем рекурсивный вызов макроса, ty
не может совпадать с чем-то вроде Option<$type:ty>
.