163952e
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
/// `session::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 ::std::fmt::Display for $name {
fn fmt(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match self {
$( Self::$variant(envelope) => formatter.write_str(&envelope.message), )*
}
}
}
impl ::actix_web::ResponseError for $name {
fn status_code(&self) -> ::actix_web::http::StatusCode {
let status = match self {
$( Self::$variant(_) => $status, )*
};
::actix_web::http::StatusCode::from_u16(status)
.unwrap_or(::actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)
}
fn error_response(&self) -> ::actix_web::HttpResponse {
let (status, mut envelope) = match self {
$( Self::$variant(envelope) => ($status, (**envelope).clone()), )*
};
envelope.request_id = $crate::middleware::request_id().unwrap_or_default();
envelope.log_id = $crate::middleware::log_id().unwrap_or_default();
let status = ::actix_web::http::StatusCode::from_u16(status)
.unwrap_or(::actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
::actix_web::HttpResponse::build(status).json(envelope)
}
}
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<::serde_json::Error> for $name {
fn from(_error: ::serde_json::Error) -> Self {
Self::build(400, Self::payload(400, "bad request"))
}
}
impl From<(::actix_web::http::StatusCode, &'static str)> for $name {
fn from((status, message): (::actix_web::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<::session::SessionError> for $name {
fn from(error: ::session::SessionError) -> Self {
match error {
::session::SessionError::Unauthorized => {
Self::build(401, Self::payload(401, "unauthenticated"))
}
::session::SessionError::Purged => {
Self::build(401, Self::payload(401, "session_revoked"))
}
other => {
::tracing::error!(error = %other, "session backend error");
Self::build(500, Self::payload(500, "internal server error"))
}
}
}
}
};
}