📝

actix-webとmicroCMSを使ってRust製のブログを作った話

2023/03/03に公開

actix-webとmicroCMSを使ってブログを作りました。
調べたことや学びをまとめておきたくて記事を書きます。

https://actix.rs/

https://microcms.io/

今回作成したブログのソースコードはこちらにおいてあります。

https://github.com/sakamuuy/blog_rs

actix-webの導入

公式のgetting startedを参考に導入しました。
ここで特に何かつまる、ということはなかったです。

環境変数を.envに記述したい

今回の作成するブログはmicroCMSからWebAPI経由でコンテンツを取得することを想定していました。
そのため、API keyやエンドポイントを管理する必要があるのですが、ソースコードに直接書くのは良くないので.envファイルで管理し、それをbuild時に取得するような作りにしました。
こちらを実現するためにdotenvを利用しています。

https://crates.io/crates/dotenv

このcrateを利用すると、.envファイルに記述されている環境変数をstd::envを利用して取得することができます。

use std::env;
use dotenv::dotenv;

...
dotenv.ok();
let v = env::var("ENV_VAR").expect("Error msg");
...

.envに記述されているENV_VARというキーの環境変数を取得できます。

actix-webのStateについて

こういう使い方で良かったかいまいち自信がなかったのですが、環境変数から取得したAPI keyなどを異なるrouteのハンドラで共有したかったので、actix-webのStateを利用しました。

https://actix.rs/docs/application#state

使い方はドキュメントの通りで、共有したいStateの構造体を定義し、それをAppインスタンスの生成時に渡します。
利用する側はハンドラの引数にStateを受け取ることができます。

struct AppState {
  api_key: String
};

...
  HttpServer::new(|| {
    App::new()
      .app_data(web::Data::new(AppState {
        api_key,  // 環境変数から取得したキーを渡す
      }))
  })
...

// 利用するハンドラ
async fn index(data: web::Data<AppState>) -> String {
  let api_key = &data.key;
}

単体で使う分には困らなかったのですが、URLからPathを取得したいときや、後述するテンプレートエンジンをハンドラ内で同時に利用しようとしたときに少しつまりました。

actix-webのPathについて

actix-webでは関数に対してget post patch deleteなどのHTTPRequestのメソッドに対応するアトリビュートを付与することで、そのルートに対するハンドラを設定することができます。
その際に、Pathを中括弧で囲むとその情報をハンドラの引数に受け取ることができます。これをactix-webではdynamic segmentsと呼びます。

#[get("/article/{article_id}")]
async fn index(path: web::Path<String>) -> Result<HttpResponse, Error> {
  // /article/hoge とアクセスした場合、hogeが取得できる
  let article_id = path.into_inner(); 
}

このようにリクエストされたパスから値を取得できるのですが、上で書いたようにStateと併用する場合、Stateもハンドラの引数で受け取るため、どちらも受け取りたい場合の書き方がわかりませんでした。
結論、HttpRequestを受け取ることでPathとStateの両方を取得することができます。
追記: これを書いている途中に気づいたのですが、ハンドラは引数を複数受け取れるみたいで、HttpRequest自体を受け取らなくても実現できたかもです...

HttpRequestを使う場合、match_infoを使うことでPathの値が、app_dataでStateを受け取ることができます。

Templateエンジン

TeraというTepmlateエンジンを利用しました。
actix-webと一緒に利用するにあたって、各ハンドラ内で対応するHTMLをrenderingして、responseにして返す、という実装をしています。

まず、サーバー起動時にTemplateのHTMLがおいてあるパスを指定して、Teraのインスタンスを生成します。
このインスタンスをリクエストハンドラが受け取れるようにactix-webのStateにセットします。

// 初期化
let templates = Tera::new("/templates/**/*");

App::new()
  .app_data(web::Data::new(AppState {
    templates,
  }))

ハンドラではTeraのインスタンスを受け取り、contextに必要な値を注入してから、レンダリングして、responseに詰めて返します。

// 受け取るハンドラ
#[get("/")]
async fn index(state: web::Data<AppState>) -> Result<HttpResponse, Error> {
  let mut ctx = tera::Context::new();
  ctx.insert("articles", articles);
  
  let view = state
    .templates
    .render("index.html.tera", &ctx)
    .map_err(|e| error::ErrorInternalServerError(e))?;
  Ok(HttpResponse::Ok().content_type("text/html").body(view))
}

静的ファイル

CSSやJSなどの静的なアセットはactix-filesを利用して配信できました。

App::new()
  .service(fs::Files::new("/static", "./static").show_files_listing())

microCMSからコンテンツを取得する

reqwest を利用しました。
headerを設定したい場合、clientを生成する必要がありました。

let client = reqwest::Client::new();
let res: Content = client.header("X-MICROCMS-API-KEY", api_key)

取得したレスポンスに型を付けたい場合にどう実装するのか悩んだのですが、型を当てたい構造体にserdeのDeserializeをderiveしておくだけで、jsonメソッドが型を解決してくれました。

#[derive(Debug, Serialize, Deserialize)]
pub struct Content {
  pub id: String,
}

...
// 型が効いている
let res: Content = client
  .get(end_point + "/api/v1/article/" + article_id)
  .header("X-MICROCMS-API-KEY", api_key)
  .send()
  .await?
  .json()
  .await?;

また、Rustは習慣的にsnakeケースを利用しますが、APIのレスポンスではケバブケースなどが使われていることもあるかと思います。その場合、serdeのAttributeを利用することでsnakeケースにrenameすることができます。

pub struct Content {
  // created_atとして扱われる
  #[serde(rename = "createdAt")]
  pub created_at: String
}

おわりに

今回ブログを作るにあたって調べたことをまとめてみました。
次はもう少し機能を増やしたものを作ってみたいとおもいます。

Discussion