テンプレートエンジン Handlebars を Rust で扱う

# はじめに

Tauri のインストーラースクリプト作成に　[Handlebars が使われているの](https://tauri.app/v1/guides/building/windows/#customizing-the-nsis-installer-template)を見かけた。
今後 Rust でのテンプレートエンジンはどれにしようかなとも思っていたのと、[Handlebars](https://handlebarsjs.com) 自体は初めてなので、挙動や書き方をメモろうと思ったのでこのポストを書く。

https://docs.rs/handlebars/latest/handlebars/index.html

### 確認環境

* handlebars 5.1.2
* Windows 11 Pro 23H2
* Rust 1.77.2

# 導入

```powershell
cargo add handlebars
```

# 使い方

## 基本的な使い方

基本的には

1. テンプレートの作成・取得
2. テンプレートの登録
3. レンダリングの実行

という手順をとる。

```rust
use handlebars::Handlebars;
use serde::Serialize;

#[derive(Serialize)]
struct Data {
    world: String,
}

fn main() {
    // オブジェクトを作成
    let mut handlebars = Handlebars::new();

    // 代入するための構造体を作成
    let data = Data {
        world: "世界!".to_string(),
    };

    // テンプレートを定義
    let template = "hello, {{world}}";
    // テンプレートを `t1` という名前で登録
    handlebars.register_template_string("t1", template).unwrap();

    // レンダリング (値の代入と結果の出力) を実行
    println!("{}", handlebars.render("t1", &data).unwrap());
}
```

実行すると以下の結果を得られる。

```
hello, 世界!
```

:::note info
公式ドキュメントでは、 [`BTreeMap`](https://doc.servo.org/serde/lib/struct.BTreeMap.html) のサンプルも記載がある。

```rust
let mut data = BTreeMap::new();
data.insert("world".to_string(), "世界!".to_string());
assert_eq!(handlebars.render("t1", &data).unwrap(), "hello 世界!");
```

これは [`serde`](https://serde.rs) 自体が `BTreeMap` のシリアライズを実装しているためそのまま使えるという事。
ドキュメントには `serde::Serialize` 可能なデータタイプでないとだめとしか書かれていないので混乱するが、つまりそういう事。
:::

### 配列を処理する

基本は `{{#each <key>}} {{/each}}` で囲む。

```rust
use handlebars::Handlebars;
use serde::Serialize;

#[derive(Serialize)]
struct Goods {
    name: String,
    price: u32,
}

#[derive(Serialize)]
struct Basket {
    contents: Vec<Goods>,
}

fn main() {
    let mut handlebars = Handlebars::new();

    let data = Basket {
        contents: vec![
            Goods {
                name: "ほうれん草".to_string(),
                price: 99,
            },
            Goods {
                name: "新玉ねぎ".to_string(),
                price: 280,
            },
            Goods {
                name: "レタス".to_string(),
                price: 158,
            },
        ],
    };

    let template = "{{#each contents}}\n{{name}}  {{price}}円\n{{/each}}\n";
    handlebars.register_template_string("t1", template).unwrap();

    println!("{}", handlebars.render("t1", &data).unwrap());
}
```

実行すると次の結果を得られる。

```
ほうれん草  99円
新玉ねぎ  280円
レタス  158円
```

## テンプレートファイルをレンダリングする

```template.txt
今日の買い物リスト {{date}}

バスケット:
{{#each contents}}
    - {{name}} {{price}}円
{{/each}}
```

```rust
use handlebars::Handlebars;
use serde::Serialize;
use std::path::PathBuf;

#[derive(Serialize)]
struct Goods {
    name: String,
    price: u32,
}

#[derive(Serialize)]
struct Basket {
    date: String,
    contents: Vec<Goods>,
}

fn main() {
    let mut handlebars = Handlebars::new();

    let data = Basket {
        date: chrono::Local::now().to_string(),
        contents: vec![
            Goods {
                name: "ほうれん草".to_string(),
                price: 99,
            },
            Goods {
                name: "新玉ねぎ".to_string(),
                price: 280,
            },
            Goods {
                name: "レタス".to_string(),
                price: 158,
            },
        ],
    };

    // let template = "{{#each contents}}\n{{name}}  {{price}}円\n{{/each}}\n";
    handlebars
        .register_template_file(
            "t1",
            PathBuf::from(r"F:\Develop\Rust\_learn\handlebars_\src\template.txt"),
        )
        .unwrap();

    println!("{}", handlebars.render("t1", &data).unwrap());
}
```

実行すると次の結果が得られる。

```
今日の買い物リスト 2024-05-18 20:33:49.547171800 +09:00

バスケット:
    - ほうれん草 99円
    - 新玉ねぎ 280円
    - レタス 158円
```

# さいごに

Handlebars 自体は色んな言語環境で使われている様子だし、そんなに書き方も違和感を感じない。
Rust でのどの serde::Serialize タイプを前提にというのもあるので、そこだけ意識しておけばよさそう。