use crate::rok_exception;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RokError {
#[error("not found")]
NotFound,
#[error("forbidden")]
Forbidden,
#[cfg(feature = "orm")]
#[error("database error: {0}")]
Orm(sqlx::Error),
#[error("internal server error: {0}")]
Internal(String),
}
#[cfg(feature = "orm")]
impl From<sqlx::Error> for RokError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => Self::NotFound,
other => Self::Orm(other),
}
}
}
impl From<String> for RokError {
fn from(s: String) -> Self {
Self::Internal(s)
}
}
impl From<&str> for RokError {
fn from(s: &str) -> Self {
Self::Internal(s.to_string())
}
}
#[cfg(feature = "axum")]
mod axum_impl {
use super::RokError;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
impl IntoResponse for RokError {
fn into_response(self) -> Response {
match self {
RokError::NotFound => {
(StatusCode::NOT_FOUND, Json(serde_json::json!({"message": "not found"}))).into_response()
}
RokError::Forbidden => {
(StatusCode::FORBIDDEN, Json(serde_json::json!({"message": "forbidden"}))).into_response()
}
#[cfg(feature = "orm")]
RokError::Orm(e) => {
#[cfg(feature = "app")]
tracing::error!(error = %e, "Internal server error (ORM)");
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"message": "internal server error"}))).into_response()
}
#[cfg(feature = "orm")]
RokError::Internal(ref msg) => {
#[cfg(feature = "app")]
tracing::error!(error = %msg, "Internal server error");
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"message": "internal server error"}))).into_response()
}
#[cfg(not(feature = "orm"))]
RokError::Internal(ref msg) => {
#[cfg(feature = "app")]
tracing::error!(error = %msg, "Internal server error");
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"message": "internal server error"}))).into_response()
}
}
}
}
}
pub trait RokException: std::error::Error + Send + Sync + 'static {
fn name(&self) -> &'static str;
fn status_code(&self) -> u16;
fn self_handled(&self) -> bool {
true
}
fn translation_id(&self) -> Option<&'static str> {
None
}
fn help(&self) -> Option<&'static str> {
None
}
}
rok_exception! {
pub struct E_HTTP_EXCEPTION {
status = 500,
self_handled = true,
fields: {
pub message: String,
}
}
}
rok_exception! {
pub struct E_HTTP_REQUEST_ABORTED {
status = 500,
self_handled = true,
fields: {
pub message: String,
}
}
}
rok_exception! {
pub struct E_ROUTE_NOT_FOUND {
status = 404,
self_handled = true,
fields: {
pub method: String,
pub path: String,
}
}
}
rok_exception! {
pub struct E_METHOD_NOT_ALLOWED {
status = 405,
self_handled = true,
fields: {
pub method: String,
pub path: String,
}
}
}
rok_exception! {
pub struct E_CANNOT_LOOKUP_ROUTE {
status = 500,
self_handled = false,
fields: {
pub route_name: String,
}
}
}
rok_exception! {
pub struct E_MISSING_ROUTE_PARAM {
status = 500,
self_handled = false,
fields: {
pub param_name: String,
}
}
}
rok_exception! {
pub struct E_INSECURE_APP_KEY {
status = 500,
self_handled = false,
fields: {
pub actual_length: usize,
}
}
}
rok_exception! {
pub struct E_MISSING_APP_KEY {
status = 500,
self_handled = false,
fields: {
}
}
}
rok_exception! {
pub struct E_INVALID_ENV_VARIABLES {
status = 500,
self_handled = false,
fields: {
pub help: String,
}
}
}
rok_exception! {
pub struct E_MISSING_CONFIG_KEY {
status = 500,
self_handled = false,
fields: {
pub key: String,
}
}
}
rok_exception! {
pub struct E_CONFIG_PARSE_ERROR {
status = 500,
self_handled = false,
fields: {
pub key: String,
pub expected: String,
}
}
}
rok_exception! {
pub struct E_SESSION_NOT_MUTABLE {
status = 500,
self_handled = false,
fields: {
}
}
}
rok_exception! {
pub struct E_SESSION_NOT_READY {
status = 500,
self_handled = false,
fields: {
}
}
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for Box<dyn RokException> {
fn into_response(self) -> axum::response::Response {
let status = axum::http::StatusCode::from_u16(self.status_code())
.unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let body = serde_json::json!({
"error": self.name(),
"message": self.to_string(),
"statusCode": self.status_code(),
});
(status, axum::Json(body)).into_response()
}
}
#[macro_export]
macro_rules! rok_exception {
(
$(#[$meta:meta])*
$vis:vis struct $name:ident {
status = $status:expr,
self_handled = true,
translation = $translation:expr,
fields: { $(pub $field:ident: $ty:ty),* $(,)? }
}
) => {
$(#[$meta])*
#[allow(non_camel_case_types)]
#[derive(Debug)]
$vis struct $name {
$(pub $field: $ty,)*
}
impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use $crate::RokException;
write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
}
}
impl std::error::Error for $name {}
impl $crate::error::RokException for $name {
fn name(&self) -> &'static str { stringify!($name) }
fn status_code(&self) -> u16 { $status }
fn self_handled(&self) -> bool { true }
fn translation_id(&self) -> Option<&'static str> { Some($translation) }
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for $name {
fn into_response(self) -> axum::response::Response {
let status = axum::http::StatusCode::from_u16($status)
.unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let body = serde_json::json!({
"error": stringify!($name),
"message": self.to_string(),
"statusCode": $status,
});
(status, axum::Json(body)).into_response()
}
}
};
(
$(#[$meta:meta])*
$vis:vis struct $name:ident {
status = $status:expr,
self_handled = true,
fields: { $(pub $field:ident: $ty:ty),* $(,)? }
}
) => {
$(#[$meta])*
#[allow(non_camel_case_types)]
#[derive(Debug)]
$vis struct $name {
$(pub $field: $ty,)*
}
impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use $crate::RokException;
write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
}
}
impl std::error::Error for $name {}
impl $crate::error::RokException for $name {
fn name(&self) -> &'static str { stringify!($name) }
fn status_code(&self) -> u16 { $status }
fn self_handled(&self) -> bool { true }
fn translation_id(&self) -> Option<&'static str> { None }
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for $name {
fn into_response(self) -> axum::response::Response {
let status = axum::http::StatusCode::from_u16($status)
.unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let body = serde_json::json!({
"error": stringify!($name),
"message": self.to_string(),
"statusCode": $status,
});
(status, axum::Json(body)).into_response()
}
}
};
(
$(#[$meta:meta])*
$vis:vis struct $name:ident {
status = $status:expr,
self_handled = false,
translation = $translation:expr,
fields: { $(pub $field:ident: $ty:ty),* $(,)? }
}
) => {
$(#[$meta])*
#[allow(non_camel_case_types)]
#[derive(Debug)]
$vis struct $name {
$(pub $field: $ty,)*
}
impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use $crate::RokException;
write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
}
}
impl std::error::Error for $name {}
impl $crate::error::RokException for $name {
fn name(&self) -> &'static str { stringify!($name) }
fn status_code(&self) -> u16 { $status }
fn self_handled(&self) -> bool { false }
fn translation_id(&self) -> Option<&'static str> { Some($translation) }
}
};
(
$(#[$meta:meta])*
$vis:vis struct $name:ident {
status = $status:expr,
self_handled = false,
fields: { $(pub $field:ident: $ty:ty),* $(,)? }
}
) => {
$(#[$meta])*
#[allow(non_camel_case_types)]
#[derive(Debug)]
$vis struct $name {
$(pub $field: $ty,)*
}
impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use $crate::RokException;
write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
}
}
impl std::error::Error for $name {}
impl $crate::error::RokException for $name {
fn name(&self) -> &'static str { stringify!($name) }
fn status_code(&self) -> u16 { $status }
fn self_handled(&self) -> bool { false }
fn translation_id(&self) -> Option<&'static str> { None }
}
};
(
$(#[$meta:meta])*
$vis:vis struct $name:ident {
status = $status:expr,
fields: { $(pub $field:ident: $ty:ty),* $(,)? }
}
) => {
$(#[$meta])*
#[allow(non_camel_case_types)]
#[derive(Debug)]
$vis struct $name {
$(pub $field: $ty,)*
}
impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use $crate::RokException;
write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
}
}
impl std::error::Error for $name {}
impl $crate::error::RokException for $name {
fn name(&self) -> &'static str { stringify!($name) }
fn status_code(&self) -> u16 { $status }
fn self_handled(&self) -> bool { true }
fn translation_id(&self) -> Option<&'static str> { None }
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for $name {
fn into_response(self) -> axum::response::Response {
let status = axum::http::StatusCode::from_u16($status)
.unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let body = serde_json::json!({
"error": stringify!($name),
"message": self.to_string(),
"statusCode": $status,
});
(status, axum::Json(body)).into_response()
}
}
};
}