Open18

Rust マクロわからん

koko_ukoko_u

背景

テンプレートエンジンの 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,
}
koko_ukoko_u

マクロ用のプロジェクトを作成する

手続きマクロを定義するには、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 の結果を観察して、何も出力されていないことを確認する
koko_ukoko_u

ジェネリックなし版を作る

構造体にジェネリックパラメータがあると複雑なので、まずはプレーンな構造体に対応する

#[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
  }
}

が生成できる姿を目指す

Hidden comment
Hidden comment
Hidden comment
Hidden comment
Hidden comment
Hidden comment
Hidden comment
koko_ukoko_u

できた

#[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
  }
}
koko_ukoko_u

ジェネリック版を作る

最初のバージョンでは

struct Example<T> {
  pub id: i64,
  value: T,
}

のような構造体に IntoContext を付けると、impl の実装が

impl From<Example> for tera::Context { ... } 

とジェネリックパラメータを失ってしまい、正常なコードが生成されない

syn::DeriveInputgenerics フィールドがあるので、これがジェネリックパラメータ

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_tokengt_tokenInputData<T> の "<" ">" の部分
  • params が実際の型のリスト
  • where_clause が型制約

今回は本体の構造体のジェネリックパラメータに型制約があっても、From トレイトの実装には無関係なので、params だけあれば十分だろう

Hidden comment
Hidden comment
Hidden comment
Hidden comment
koko_ukoko_u

単純にジェネリックパラメータに全部 Serialize を付けるとライフタイムパラメータや、定数パラメータに対して構文エラーになる

syn::GenericParam
pub enum GenericParam {
    Lifetime(LifetimeParam),
    Type(TypeParam),
    Const(ConstParam),
}