Skip to content
Clone
27fffe3
326 lines 12.0 KB Rust
use sea_orm::DbErr;
pub fn is_unique_violation(err: &DbErr) -> bool {
let msg = err.to_string().to_lowercase();
msg.contains("unique") || msg.contains("duplicate") || msg.contains("23505")
}
/// Generates the `From` conversions shared by all domain error enums:
/// `DbErr`, `TransactionError<DbErr>` and `TransactionError<Self>`.
/// The target enum must have `Conflict(String)` and `Internal(String)` variants.
#[macro_export]
macro_rules! db_error_conversions {
($name:ident) => {
impl From<::sea_orm::DbErr> for $name {
fn from(error: ::sea_orm::DbErr) -> Self {
if $crate::is_unique_violation(&error) {
return Self::Conflict("resource already exists".to_string());
}
Self::Internal(format!("database error: {error}"))
}
}
impl From<::sea_orm::TransactionError<::sea_orm::DbErr>> for $name {
fn from(error: ::sea_orm::TransactionError<::sea_orm::DbErr>) -> Self {
match &error {
::sea_orm::TransactionError::Transaction(db_err)
if $crate::is_unique_violation(db_err) =>
{
Self::Conflict("resource already exists".to_string())
}
_ => Self::Internal(format!("database transaction error: {error}")),
}
}
}
impl From<::sea_orm::TransactionError<$name>> for $name {
fn from(error: ::sea_orm::TransactionError<$name>) -> Self {
match error {
::sea_orm::TransactionError::Connection(db_err) => Self::from(db_err),
::sea_orm::TransactionError::Transaction(err) => err,
}
}
}
};
}
#[macro_export]
macro_rules! redis_error_conversion {
($name:ident) => {
impl From<::redis::RedisError> for $name {
fn from(error: ::redis::RedisError) -> Self {
Self::Internal(format!("redis error: {error}"))
}
}
};
}
#[macro_export]
macro_rules! storage_error_conversion {
($name:ident) => {
impl From<::storage::StorageError> for $name {
fn from(error: ::storage::StorageError) -> Self {
Self::Internal(format!("storage error: {error}"))
}
}
};
}
#[macro_export]
macro_rules! export_error_conversion {
($name:ident) => {
impl From<$crate::ExportError> for $name {
fn from(error: $crate::ExportError) -> Self {
Self::Internal(format!("infrastructure error: {error}"))
}
}
};
}
#[macro_export]
macro_rules! json_payload_conversion {
($name:ident) => {
impl From<::serde_json::Error> for $name {
fn from(error: ::serde_json::Error) -> Self {
Self::BadRequest(format!("invalid payload: {error}"))
}
}
};
}
/// Standard variant-to-variant mapping between two domain error enums sharing
/// the `BadRequest/Unauthorized/Forbidden/NotFound/Conflict/RateLimited/Internal`
/// shape; non-matching source variants fall through to `Internal`.
#[macro_export]
macro_rules! domain_error_passthrough {
($to:ident from $($src:tt)*) => {
impl From<$($src)*> for $to {
fn from(error: $($src)*) -> Self {
#[allow(unreachable_patterns)]
match error {
$($src)*::BadRequest(msg) => Self::BadRequest(msg),
$($src)*::Unauthorized => Self::Unauthorized,
$($src)*::Forbidden => Self::Forbidden,
$($src)*::NotFound => Self::NotFound,
$($src)*::Conflict(msg) => Self::Conflict(msg),
$($src)*::RateLimited(msg) => Self::RateLimited(msg),
$($src)*::Internal(msg) => Self::Internal(msg),
other => Self::Internal(other.to_string()),
}
}
}
};
}
/// Generates a crate's `ApiError` response enum plus its boilerplate:
/// `IntoResponses` derive, envelope constructors, `IntoResponse`, and the
/// `From` impls shared by every crate (boxed self, `AppResponseData<()>`,
/// request rejections, `serde_json::Error`, `(StatusCode, &str)` and
/// `jwt::SessionError`).
///
/// The enum must contain an `Internal` variant; `log` labels server-side
/// warnings as "`<log> handler failed`".
#[macro_export]
macro_rules! api_error {
(
log: $log:literal,
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$( $variant:ident ( $status:literal, $desc:literal ) ),* $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, ::utoipa::IntoResponses)]
$vis enum $name {
$(
#[response(status = $status, description = $desc)]
$variant(#[to_schema] ::std::boxed::Box<$crate::rest::AppResponseData<$crate::rest::NoData>>),
)*
}
impl $name {
fn payload(
code: i32,
message: &'static str,
) -> $crate::rest::AppResponseData<$crate::rest::NoData> {
$crate::rest::AppResponseData::err(code, message)
}
/// Wraps a plain envelope into the enum's boxed payload. The box is
/// what keeps `Result<T, ApiError>` small: an unboxed envelope is
/// 128 bytes, which every handler return type would otherwise carry.
pub fn build(
status: u16,
envelope: $crate::rest::AppResponseData<$crate::rest::NoData>,
) -> Self {
let envelope = ::std::boxed::Box::new(envelope);
#[allow(unreachable_patterns)]
match status {
$( $status => Self::$variant(envelope), )*
_ => Self::Internal(envelope),
}
}
pub fn bad_request(message: impl ::std::convert::AsRef<str>) -> Self {
let message = message.as_ref();
Self::build(
400,
$crate::rest::AppResponseData::err(400, message)
.with_error_code("bad_request"),
)
}
pub fn unauthorized() -> Self {
Self::build(
401,
Self::payload(401, "unauthorized").with_error_code("unauthorized"),
)
}
pub fn forbidden() -> Self {
Self::build(
403,
Self::payload(403, "forbidden").with_error_code("forbidden"),
)
}
pub fn not_found(message: &'static str) -> Self {
Self::build(
404,
Self::payload(404, message).with_error_code("not_found"),
)
}
pub fn conflict(message: impl ::std::convert::AsRef<str>) -> Self {
let message = message.as_ref();
Self::build(
409,
$crate::rest::AppResponseData::err(409, message).with_error_code("conflict"),
)
}
pub fn forbidden_with_code(
error_code: impl ::std::convert::Into<String>,
message: impl ::std::convert::Into<String>,
) -> Self {
Self::build(
403,
$crate::rest::AppResponseData::err(403, message)
.with_error_code(error_code),
)
}
pub fn conflict_with_code(
error_code: impl ::std::convert::Into<String>,
message: impl ::std::convert::Into<String>,
) -> Self {
Self::build(
409,
$crate::rest::AppResponseData::err(409, message)
.with_error_code(error_code),
)
}
pub fn rate_limited(message: impl ::std::convert::Into<String>) -> Self {
Self::build(
429,
$crate::rest::AppResponseData::err(429, message)
.with_error_code("rate_limited"),
)
}
pub fn rate_limited_exceeded() -> Self {
Self::build(
429,
Self::payload(429, "rate limit exceeded").with_error_code("rate_limited"),
)
}
pub fn internal(message: impl ::std::fmt::Display) -> Self {
::tracing::warn!(error = %message, concat!($log, " handler failed"));
Self::build(500, Self::payload(500, "internal server error"))
}
}
impl ::axum::response::IntoResponse for $name {
fn into_response(self) -> ::axum::response::Response {
let (status, envelope) = match self {
$( Self::$variant(envelope) => ($status, *envelope), )*
};
let status = ::axum::http::StatusCode::from_u16(status)
.unwrap_or(::axum::http::StatusCode::INTERNAL_SERVER_ERROR);
(status, ::axum::Json(envelope)).into_response()
}
}
impl From<Box<$name>> for $name {
fn from(error: Box<$name>) -> Self {
*error
}
}
impl From<$crate::rest::AppResponseData<()>> for $name {
fn from(_error: $crate::rest::AppResponseData<()>) -> Self {
Self::build(400, Self::payload(400, "bad request"))
}
}
impl From<::axum::extract::rejection::PathRejection> for $name {
fn from(error: ::axum::extract::rejection::PathRejection) -> Self {
Self::build(
400,
$crate::rest::AppResponseData::err(400, "invalid request path")
.with_error(error.body_text()),
)
}
}
impl From<::axum::extract::rejection::JsonRejection> for $name {
fn from(error: ::axum::extract::rejection::JsonRejection) -> Self {
Self::build(
400,
$crate::rest::AppResponseData::err(400, "invalid request body")
.with_error(error.body_text())
.with_error_code("invalid_request_body"),
)
}
}
impl From<::axum::extract::rejection::QueryRejection> for $name {
fn from(error: ::axum::extract::rejection::QueryRejection) -> Self {
Self::build(
400,
$crate::rest::AppResponseData::err(400, "invalid query parameters")
.with_error(error.body_text()),
)
}
}
impl From<::serde_json::Error> for $name {
fn from(_error: ::serde_json::Error) -> Self {
Self::build(400, Self::payload(400, "bad request"))
}
}
impl From<(::axum::http::StatusCode, &'static str)> for $name {
fn from((status, message): (::axum::http::StatusCode, &'static str)) -> Self {
::tracing::error!(%status, %message, "unhandled (status, message) converted to 500");
Self::build(500, Self::payload(500, "internal server error"))
}
}
impl From<::jwt::SessionError> for $name {
fn from(error: ::jwt::SessionError) -> Self {
match error {
::jwt::SessionError::Unauthorized => {
Self::build(401, Self::payload(401, "unauthenticated"))
}
::jwt::SessionError::Expired => Self::build(401, Self::payload(401, "token_expired")),
::jwt::SessionError::Verifier(message) => {
::tracing::debug!(error = %message, "bearer token rejected");
Self::build(401, Self::payload(401, "invalid_token"))
}
}
}
}
};
}