121 lines
3.3 KiB
Rust
121 lines
3.3 KiB
Rust
use axum::{
|
|
Json,
|
|
http::{HeaderMap, StatusCode},
|
|
response::IntoResponse,
|
|
};
|
|
use serde::Serialize;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum AppError {
|
|
#[error("bad request: {0}")]
|
|
BadRequest(String),
|
|
#[error("pokemon not found")]
|
|
NotFound,
|
|
#[error("upstream service returned invalid data: {0}")]
|
|
InvalidUpstreamData(String),
|
|
#[error("upstream service failed: {0}")]
|
|
Upstream(String),
|
|
#[error("request timed out")]
|
|
Timeout,
|
|
#[error("internal server error")]
|
|
Internal,
|
|
}
|
|
|
|
impl AppError {
|
|
#[must_use]
|
|
pub const fn status_code(&self) -> StatusCode {
|
|
match self {
|
|
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
|
|
Self::NotFound => StatusCode::NOT_FOUND,
|
|
Self::InvalidUpstreamData(_) | Self::Upstream(_) | Self::Timeout => {
|
|
StatusCode::BAD_GATEWAY
|
|
}
|
|
Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn code(&self) -> &'static str {
|
|
match self {
|
|
Self::BadRequest(_) => "bad_request",
|
|
Self::NotFound => "pokemon_not_found",
|
|
Self::InvalidUpstreamData(_) => "upstream_invalid_data",
|
|
Self::Upstream(_) | Self::Timeout => "upstream_unavailable",
|
|
Self::Internal => "internal_error",
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn public_message(&self) -> String {
|
|
match self {
|
|
Self::BadRequest(message) => message.clone(),
|
|
Self::NotFound => "pokemon not found".to_owned(),
|
|
Self::InvalidUpstreamData(_) => "upstream service returned invalid data".to_owned(),
|
|
Self::Upstream(_) | Self::Timeout => "upstream service failed".to_owned(),
|
|
Self::Internal => "internal server error".to_owned(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for AppError {
|
|
fn into_response(self) -> axum::response::Response {
|
|
ApiError::new(self, None).into_response()
|
|
}
|
|
}
|
|
|
|
pub struct ApiError {
|
|
source: AppError,
|
|
request_id: Option<String>,
|
|
}
|
|
|
|
impl ApiError {
|
|
#[must_use]
|
|
pub const fn new(source: AppError, request_id: Option<String>) -> Self {
|
|
Self { source, request_id }
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn from_headers(source: AppError, headers: &HeaderMap) -> Self {
|
|
Self::new(source, request_id_from_headers(headers))
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for ApiError {
|
|
fn into_response(self) -> axum::response::Response {
|
|
let status = self.source.status_code();
|
|
let body = ErrorBody::new(
|
|
self.source.code(),
|
|
self.source.public_message(),
|
|
self.request_id,
|
|
);
|
|
(status, Json(body)).into_response()
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct ErrorBody {
|
|
code: &'static str,
|
|
message: String,
|
|
#[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
|
|
request_id: Option<String>,
|
|
}
|
|
|
|
impl ErrorBody {
|
|
#[must_use]
|
|
pub fn new(code: &'static str, message: impl Into<String>, request_id: Option<String>) -> Self {
|
|
Self {
|
|
code,
|
|
message: message.into(),
|
|
request_id,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn request_id_from_headers(headers: &HeaderMap) -> Option<String> {
|
|
headers
|
|
.get("x-request-id")
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(str::to_owned)
|
|
}
|