Rocket で controller のハンドラ関数でリクエストを受けて、use_case や repository で詳細の処理を実行するようになっています。use_case, repository で発生したエラー毎にレスポンス時のステータスコードを決めないといけないです。 ですので、use_case, repository は基本全てResult
型を返し、Err
はオリジナルのstatus_code
を持つAppError
型を使おうと思っています。use_case, repository 等でエラーが発生しうるハンドラ関数も、基本的にResult
型を返します。 Rocket はここ に書いていますが、Responder トレイトが実装されている型であれば、何でもハンドラ関数の戻り値に設定できます。ですので、AppError
型にResponder
トレイトを実装します。
こういうやつを想定しています。
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"))
みたいな感じでエラーを作れます。
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 のレスポンスに使うには、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())
}