Rocketで controller のハンドラ関数でリクエストを受けて、use_case や repository で詳細の処理を実行するようになっています。use_case, repository で発生したエラー毎にレスポンス時のステータスコードを決めないといけないです。
ですので、use_case, repository は基本全てResult
型を返し、Err
はオリジナルのstatus_code
を持つAppError
型を使おうと思っています。use_case, repository 等でエラーが発生しうるハンドラ関数も、基本的にResult
型を返します。
Rocket はここに書いていますが、Responderトレイトが実装されている型であれば、何でもハンドラ関数の戻り値に設定できます。ですので、AppError
型にResponder
トレイトを実装します。
オリジナルの AppError 型
こういうやつを想定しています。
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] pub struct AppError { pub status_code: u16, pub message: String, } impl AppError { pub fn new(status_code: u16, message: &str) -> Self { Self { status_code, message: message.to_string(), } } }
new
関数を作ってますので、Err(AppError::new(404, "Not Found"))
みたいな感じでエラーを作れます。
どんなエラーでも簡単に AppError に変換できるようにする
Result
型のErr
の中身は何でもよいのですが、一般的には、std::error::Errorトレイトが実装されているようです。これは、Display
トレイトも必要になりますので、to_string()
が使えます。
Result<T, E>
で、E
がstd::error::Error
トレイトを実装している場合、app_error()
関数を使えるようにして、簡単にAppError
に変換できるようにしてみます。
pub trait AppErrorResultExt<T> { fn app_error(self, status_code: u16, message: &str) -> Result<T, AppError>; } impl<T, E> AppErrorResultExt<T> for Result<T, E> where E: std::error::Error, { fn app_error(self, status_code: u16, message: &str) -> Result<T, AppError> { self.map_err(|_| AppError::new(status_code, message)) } }
これで、下記のように使えます。parse()
はi32
型に変換しようとしますが、130a
は変換に失敗しますので、Err<ParseIntError>
を返します。app_error
関数はErr
の場合、AppError::new
を実行して、Err<AppError>
を返します。?
によりErr
の場合は、自動的に return されますので、結果的にErr<AppError>
が返されます。もし"130".parse()
など、変換が成功する場合は、app_error
は何もしませんし、?
でOk()
の囲いが取れて、変換後のi32
の値がnum
に返されます。
let num:i32 = "130a".parse().app_err(400, "Bad Request")?;
Rocket のレスポンスで使えるようにする
先述のとおり、Rocket のレスポンスに使うには、Responderトレイトを実装する必要があります。ドキュメントには下記のように Deriving を使うのがおすすめと書いてありました。
use rocket::http::ContentType; use rocket::serde::{Serialize, json::Json}; #[derive(Responder)] enum Error<T> { #[response(status = 400)] Unauthorized(Json<T>), #[response(status = 404)] NotFound(Template, ContentType), }
今回は、ステータスコードが動的に変わるので、ちょっと上記は使えないかなあと思い、下記のようにしてみました。
use rocket::http::Status; use rocket::serde::json::Json; use rocket::response::{Responder, Response}; use rocket::Request; use serde::{Serialize, Deserialize}; impl<'r> Responder<'r, 'static> for AppError { fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> { let status = Status::from_code(self.status_code).unwrap_or(Status::InternalServerError); Response::build_from(Json(self).respond_to(req)?) .status(status) .ok() } }
これでハンドラ関数で下記のように使えます。
#[get("/hoge")] async fn hoge() -> Result<String, AppError> { hoge_use_case::hoge().await?; Ok("ok!".to_string()) }