はじめに
こんにちは、テックリードのかどたみです。 弊社ではマイクロサービスの開発にRustを使っているというのは前回・前々回の記事でもご紹介したのですが、フレームワークを紹介できていませんでした。弊社ではactix-webを利用しています。 Railsなどの大規模フレームワークに比べるとかゆいところに手が届かない部分ももちろんありますが、シンプルでカスタマイズ性が高いため自分の手で機能を追加していくことが出来ます。 本記事ではカスタマイズした機能とその実現方法を紹介していきたいと思います。
※この記事はEmotion Tech Advent Calendar 2021 11日目の記事です
独自に追加した機能
紹介するのは以下の2つです。
- HTTP Headerに格納された値を取得してバリデーションをする機能
- リクエストパラメータを外部crateを利用して取得する機能
前者はAPIでよくある認証トークンの確認のようなものです。HTTP Headerから特定のkeyに紐づくvalueを取得してそのvalueの正当性を判別するものです。 後者はGET時のURLパラメータで配列の取得を行いたかったのですが、actix-web標準の機能ではサポートされていなかったため、外部のcrateを用いることでそれを実現したものです。
カスタマイズするとどう便利?
actix-webのドキュメントにはリクエストのBodyJsonやFormDataをパースする例が記載されていて、このような機能はextractorと呼ばれています。
#[derive(Deserialize)] struct Info { username: String, } /// extract `Info` using serde async fn index(info: web::Json<Info>) -> Result<String> { // info: web::Json<Info>がextractor Ok(format!("Welcome {}!", info.username)) }
extractorを使うことで簡単にリクエスト情報を引数で定義でき、handler内で利用できます。 しかし、今回やりたいようなヘッダーの情報を取得してバリデーションするような複雑なことはhandler内で呼び出すのは煩雑になりがちですし、getのパラメータを配列で取得したいといったactix-webで提供されていないことはもちろん出来ません。 このextractorを自前で作成することができれば、以下のようにhandler内はユースケースやロジックの呼び出しに専念できスッキリ書けるようになりますし、リクエストの形式にとらわれず自由にパラメータを取得できるようになります。
#[get("")] pub async fn index( req: CustomQuery<TodoListRequest>, // TodoListRequestにオリジナルパーサーを使ってURLパラメータをマッピング header_token_info: HeaderTokenInfo, // トークンのチェックと付随する情報を取得 ) -> Result<HttpResponse, ErrorResponse> { let usecase = TodoUsecase::new(); let res = usecase.get_list(req).await.map_err(map_error)?; Ok(HttpResponse::Ok().json(res)) }
どうやってカスタマイズするの?
実は先程のextractorのページに答えが書いてあります。
Actix-web provides a facility for type-safe request information access called extractors (i.e., impl FromRequest). By default, actix-web provides several extractor implementations.
そうですimpl FromRequestです! FromRequest Traitを実装してあげるだけでextractorとして利用できます。 例えばHeaderTokenInfoの場合は以下のようになります。
// 構造体を定義 pub struct HeaderTokenInfo { pub user_id: Id<User>, pub token: Uuid, } impl FromRequest for HeaderTokenInfo { type Error = TokenError; type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; type Config = (); fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { // headerの値を取得 let header_value = match req.headers().get('header_key') { Some(value) => value.to_str(), None => return Err(TokenError::NotFound), }; ... // tokenからuser_idなどを取得する処理 } }
FromRequest内のfrom_requestを実装するだけです!簡単!便利! これでリクエストベースで情報を取得する際は何でもhandlerの引数に設定するだけで使い回せるようになります。
おまけ ~GETのパラメータを配列で受け取れるようにしよう~
マイクロサービスを開発するにあたってGETのパラメータで配列を受け取る必要が出てきました。
吉田のOpenRestyの記事にもあったとおりマイクロサービスを作る上でフロントエンドに影響は極力与えたくありません。HTTPの標準ではないのですが、現状のRailsのAPIがGETにて配列をparam[]=
の形で受け取っているためRustでもそれに合わせることにしたのです。
しかし、actix-webが提供しているextractor(Query)では上記のような配列は受け付けてくれません。処理を司っているcrateはserde_urlencodedなのですが、そこでは以下のようにHTTPの標準以外は実装しないことが明記されています。
This is not a crate for practical sense, this is a crate for deserialising from query strings as specified by the URL Standard, nothing more, nothing less.
actix-webのissueでも議論されているのですが、こちらでは配列を利用したい場合はserde_qsというcrateを利用することがおすすめされています。
これも今回紹介したFromRequestを独自に実装してQueryのserde_urlencodedをserde_qsにして定義してしまえばよいということです。
pub struct CustomQuery<T>(pub T); impl<T> FromRequest for CustomQuery<T> where T: de::DeserializeOwned, { ... #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ... // もともとの処理はserde_urlencodedを利用している // serde_urlencoded::from_str::<T>(req.query_string()) // .map(|val| ok(Query(val))) // .unwrap_or_else(move |e| { // ... // }) // 配列が処理できる別crateに変更 serde_qs::from_str::<T>(req.query_string()) .map(|val| ok(CustomQuery(val))) .unwrap_or_else(move |e| { ... }) } }
serde_qsを愚直にhandlerの中で利用してもよいのですが、エンドポイントが増えるとどうしても冗長になってしまいます。このようにFromRequestを利用するととても簡潔に書くことができるようになるので皆さんも是非試してみてください。
おわりに
いかがでしたでしょうか? 今回はactix-webでのカスタマイズ方法を紹介しました。actix-webで用意されているものももちろん便利なのですが、他のcrateを埋め込みたかったり、仕様に紐付いたロジックなどに対応させる際には是非カスタマイズしてみてください。 実は他のRustフレームワークでも似たような設計みたいなので参考になるかもしれません。
Emotion Techでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、Rustを使ってバリバリ開発したい方、イキイキと働けるチームでプロダクトをより良くしていきたい方がいらっしゃいましたら、ぜひ採用ページからご応募をお願いいたします。