Open18
Rust マクロわからん
参考
背景
テンプレートエンジンの Tera でテンプレートにデータを渡す時には Context
型で渡す必要がある
コード片
let data = IndexData { title: "Blog Title" };
let mut context = tera::Context::new();
context.insert("title", data.title);
let body = engine.render("index.html", &context).unwrap();
Ok(HttpResponse::Ok().body(body))
業務データ(上記の例では IndexData
) を Context
に変換するロジックを From
トレイトに抽出する
コード片(改)
impl<'a> From<IndexData<'a>> for tera::Context {
fn from(data: IndexData<'a>) -> Self {
let mut context = tera::Context::new();
context.insert("title", data.title);
context
}
}
// inside handler
let data = IndexData { title: "Blog Title" };
let body = engine.render("index.html", &data.into()).unwrap();
Ok(HttpResponse::Ok().body(body))
テンプレートに渡したいデータ型はたくさんあるので、From
トレイトをマクロで導出したい。
実装イメージ
#[derive(IntoContext)]
pub struct IndexData<'a> {
put title: &'a str,
}
マクロ用のプロジェクトを作成する
手続きマクロを定義するには、Cargo.toml
ファイルに
[lib]
proc-macro = true
の指定をして新しいプロジェクトを用意して、lib.rs
に実装を始める
空の実装
#[proc_macro_derive(IntoContext)]
pub fn into_context(item: TokenStream) -> TokenStream {
let impl_context = quote! {};
impl_context.into()
}
- プロジェクトが正常にビルドできて、利用側で
derive(IntoContext)
の指定をしても正常にビルドできることを確認する - cargo expand の結果を観察して、何も出力されていないことを確認する
ジェネリックなし版を作る
構造体にジェネリックパラメータがあると複雑なので、まずはプレーンな構造体に対応する
#[derive(IntoContext)
pub struct IndexData {
title: String
}
から
impl From<IndexData> for tera::Context {
fn from(value: IndexData) -> Self {
let mut context = tera::Context::new();
context.insert("title", &value.title);
context
}
}
が生成できる姿を目指す
できた
#[derive(IntoContext)]
pub struct Person {
pub name: String,
pub age: u32,
pub married: bool,
}
を cargo expand
すると
impl From<Person> for tera::Context {
fn from(value: Person) -> Self {
let mut context = tera::Context::new();
context.insert("name", &value.name);
context.insert("age", &value.age);
context.insert("married", &value.married);
context
}
}
ジェネリック版を作る
最初のバージョンでは
struct Example<T> {
pub id: i64,
value: T,
}
のような構造体に IntoContext
を付けると、impl の実装が
impl From<Example> for tera::Context { ... }
とジェネリックパラメータを失ってしまい、正常なコードが生成されない
syn::DeriveInput
に generics
フィールドがあるので、これがジェネリックパラメータ
syn::Generics
pub struct Generics {
pub lt_token: Option<Lt>,
pub params: Punctuated<GenericParam, Comma>,
pub gt_token: Option<Gt>,
pub where_clause: Option<WhereClause>,
}
-
lt_token
とgt_token
がInputData<T>
の "<" ">" の部分 -
params
が実際の型のリスト -
where_clause
が型制約
今回は本体の構造体のジェネリックパラメータに型制約があっても、From
トレイトの実装には無関係なので、params
だけあれば十分だろう
単純にジェネリックパラメータに全部 Serialize
を付けるとライフタイムパラメータや、定数パラメータに対して構文エラーになる
syn::GenericParam
pub enum GenericParam {
Lifetime(LifetimeParam),
Type(TypeParam),
Const(ConstParam),
}