use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde_json::json; /// Erro centralizado da aplicação. /// Toda camada retorna `Result`. #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("not found")] NotFound, #[error("unauthorized")] Unauthorized, #[error("forbidden")] Forbidden, #[error("email not verified")] EmailNotVerified, #[error("validation error: {0}")] Validation(String), #[error("bad request: {0}")] BadRequest(String), #[error("conflict: {0}")] Conflict(String), #[error("unprocessable entity: {0}")] UnprocessableEntity(String), #[error("internal error")] Internal(#[from] anyhow::Error), } impl AppError { fn status_and_code(&self) -> (StatusCode, &'static str) { match self { Self::NotFound => (StatusCode::NOT_FOUND, "NOT_FOUND"), Self::Unauthorized => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED"), Self::Forbidden => (StatusCode::FORBIDDEN, "FORBIDDEN"), Self::EmailNotVerified => (StatusCode::FORBIDDEN, "EMAIL_NOT_VERIFIED"), Self::Validation(_) => (StatusCode::BAD_REQUEST, "VALIDATION_ERROR"), Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "BAD_REQUEST"), Self::Conflict(_) => (StatusCode::CONFLICT, "CONFLICT"), Self::UnprocessableEntity(_) => (StatusCode::UNPROCESSABLE_ENTITY, "UNPROCESSABLE_ENTITY"), Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"), } } } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, code) = self.status_and_code(); if status == StatusCode::INTERNAL_SERVER_ERROR { tracing::error!(error = %self, "Internal server error"); } let body = Json(json!({ "error": self.to_string(), "code": code, })); (status, body).into_response() } } impl From for AppError { fn from(err: sqlx::Error) -> Self { match err { sqlx::Error::RowNotFound => Self::NotFound, _ => Self::Internal(err.into()), } } } impl From for AppError { fn from(err: validator::ValidationErrors) -> Self { Self::Validation(err.to_string()) } }