feat: implement pokedex api
This commit is contained in:
120
src/error.rs
Normal file
120
src/error.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user