rari 0.13.6

rari: Runtime Accelerated Rendering Infrastructure - A high-performance React Server Components framework powered by Rust
Documentation
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ApiRouteError {
    NotFound { path: String, message: String },
    MethodNotAllowed { path: String, method: String, allowed_methods: Vec<String>, message: String },
    HandlerError { path: String, method: String, message: String, stack: Option<String> },
    InvalidResponse { path: String, method: String, message: String, details: Option<String> },
    HandlerFileNotFound { path: String, file_path: String, message: String },
    HandlerLoadError { path: String, file_path: String, message: String },
    BodyParseError { path: String, method: String, message: String },
}

impl ApiRouteError {
    pub fn status_code(&self) -> StatusCode {
        match self {
            Self::NotFound { .. } => StatusCode::NOT_FOUND,
            Self::MethodNotAllowed { .. } => StatusCode::METHOD_NOT_ALLOWED,
            Self::HandlerError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
            Self::InvalidResponse { .. } => StatusCode::INTERNAL_SERVER_ERROR,
            Self::HandlerFileNotFound { .. } => StatusCode::INTERNAL_SERVER_ERROR,
            Self::HandlerLoadError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
            Self::BodyParseError { .. } => StatusCode::BAD_REQUEST,
        }
    }

    pub fn error_code(&self) -> &'static str {
        match self {
            Self::NotFound { .. } => "API_ROUTE_NOT_FOUND",
            Self::MethodNotAllowed { .. } => "METHOD_NOT_ALLOWED",
            Self::HandlerError { .. } => "HANDLER_EXECUTION_ERROR",
            Self::InvalidResponse { .. } => "INVALID_RESPONSE",
            Self::HandlerFileNotFound { .. } => "HANDLER_FILE_NOT_FOUND",
            Self::HandlerLoadError { .. } => "HANDLER_LOAD_ERROR",
            Self::BodyParseError { .. } => "BODY_PARSE_ERROR",
        }
    }

    pub fn path(&self) -> &str {
        match self {
            Self::NotFound { path, .. } => path,
            Self::MethodNotAllowed { path, .. } => path,
            Self::HandlerError { path, .. } => path,
            Self::InvalidResponse { path, .. } => path,
            Self::HandlerFileNotFound { path, .. } => path,
            Self::HandlerLoadError { path, .. } => path,
            Self::BodyParseError { path, .. } => path,
        }
    }

    pub fn method(&self) -> Option<&str> {
        match self {
            Self::MethodNotAllowed { method, .. } => Some(method),
            Self::HandlerError { method, .. } => Some(method),
            Self::InvalidResponse { method, .. } => Some(method),
            Self::BodyParseError { method, .. } => Some(method),
            _ => None,
        }
    }

    pub fn message(&self) -> &str {
        match self {
            Self::NotFound { message, .. } => message,
            Self::MethodNotAllowed { message, .. } => message,
            Self::HandlerError { message, .. } => message,
            Self::InvalidResponse { message, .. } => message,
            Self::HandlerFileNotFound { message, .. } => message,
            Self::HandlerLoadError { message, .. } => message,
            Self::BodyParseError { message, .. } => message,
        }
    }

    pub fn allowed_methods(&self) -> Option<&[String]> {
        match self {
            Self::MethodNotAllowed { allowed_methods, .. } => Some(allowed_methods),
            _ => None,
        }
    }

    pub fn stack(&self) -> Option<&str> {
        match self {
            Self::HandlerError { stack, .. } => stack.as_deref(),
            _ => None,
        }
    }
}

impl fmt::Display for ApiRouteError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotFound { path, message } => {
                write!(f, "API route not found at {}: {}", path, message)
            }
            Self::MethodNotAllowed { path, method, allowed_methods, message } => {
                write!(
                    f,
                    "Method {} not allowed for route {}. Allowed methods: {}. {}",
                    method,
                    path,
                    allowed_methods.join(", "),
                    message
                )
            }
            Self::HandlerError { path, method, message, .. } => {
                write!(f, "Handler error for {} {}: {}", method, path, message)
            }
            Self::InvalidResponse { path, method, message, .. } => {
                write!(f, "Invalid response from {} {}: {}", method, path, message)
            }
            Self::HandlerFileNotFound { path, file_path, message } => {
                write!(f, "Handler file not found for route {} at {}: {}", path, file_path, message)
            }
            Self::HandlerLoadError { path, file_path, message } => {
                write!(
                    f,
                    "Failed to load handler for route {} from {}: {}",
                    path, file_path, message
                )
            }
            Self::BodyParseError { path, method, message } => {
                write!(f, "Failed to parse request body for {} {}: {}", method, path, message)
            }
        }
    }
}

impl std::error::Error for ApiRouteError {}

impl From<ApiRouteError> for crate::error::RariError {
    fn from(error: ApiRouteError) -> Self {
        match error {
            ApiRouteError::NotFound { message, .. } => crate::error::RariError::not_found(message)
                .with_property("error_type", "api_route_not_found"),
            ApiRouteError::MethodNotAllowed { message, allowed_methods, .. } => {
                crate::error::RariError::bad_request(message)
                    .with_property("error_type", "method_not_allowed")
                    .with_property("allowed_methods", &allowed_methods.join(","))
            }
            ApiRouteError::HandlerError { message, .. } => {
                crate::error::RariError::js_execution(message)
                    .with_property("error_type", "handler_error")
            }
            ApiRouteError::InvalidResponse { message, .. } => {
                crate::error::RariError::internal(message)
                    .with_property("error_type", "invalid_response")
            }
            ApiRouteError::HandlerFileNotFound { message, .. } => {
                crate::error::RariError::not_found(message)
                    .with_property("error_type", "handler_file_not_found")
            }
            ApiRouteError::HandlerLoadError { message, .. } => {
                crate::error::RariError::internal(message)
                    .with_property("error_type", "handler_load_error")
            }
            ApiRouteError::BodyParseError { message, .. } => {
                crate::error::RariError::bad_request(message)
                    .with_property("error_type", "body_parse_error")
            }
        }
    }
}

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub error: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub route: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub method: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stack: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<serde_json::Value>,
}

impl ApiRouteError {
    pub fn to_json_response(&self, is_development: bool) -> ErrorResponse {
        if is_development {
            ErrorResponse {
                error: self.status_code().canonical_reason().unwrap_or("Error").to_string(),
                message: self.message().to_string(),
                code: Some(self.error_code().to_string()),
                route: Some(self.path().to_string()),
                method: self.method().map(|m| m.to_string()),
                stack: self.stack().map(|s| s.to_string()),
                details: self.get_error_details(),
            }
        } else {
            let generic_message = match self {
                Self::NotFound { .. } => "The requested resource was not found",
                Self::MethodNotAllowed { .. } => "Method not allowed for this resource",
                Self::BodyParseError { .. } => "Invalid request body",
                _ => "An error occurred processing your request",
            };

            ErrorResponse {
                error: self.status_code().canonical_reason().unwrap_or("Error").to_string(),
                message: generic_message.to_string(),
                code: None,
                route: None,
                method: None,
                stack: None,
                details: None,
            }
        }
    }

    #[allow(clippy::disallowed_methods)]
    fn get_error_details(&self) -> Option<serde_json::Value> {
        match self {
            Self::MethodNotAllowed { allowed_methods, .. } => Some(serde_json::json!({
                "allowed_methods": allowed_methods
            })),
            Self::InvalidResponse { details, .. } => {
                details.as_ref().and_then(|d| serde_json::from_str(d).ok())
            }
            Self::HandlerFileNotFound { file_path, .. } => Some(serde_json::json!({
                "file_path": file_path
            })),
            Self::HandlerLoadError { file_path, .. } => Some(serde_json::json!({
                "file_path": file_path
            })),
            _ => None,
        }
    }

    pub fn to_json_string(&self, is_development: bool) -> String {
        let response = self.to_json_response(is_development);
        serde_json::to_string(&response).unwrap_or_else(|_| {
            r#"{"error":"Internal Server Error","message":"Failed to serialize error response"}"#
                .to_string()
        })
    }

    pub fn to_http_response(&self, is_development: bool) -> axum::http::Response<axum::body::Body> {
        let status = self.status_code();
        let body = self.to_json_string(is_development);

        let mut builder = axum::http::Response::builder()
            .status(status)
            .header("content-type", "application/json");

        if let Self::MethodNotAllowed { allowed_methods, .. } = self {
            let allow_header = allowed_methods.join(", ");
            builder = builder.header("allow", allow_header);
        }

        builder.body(axum::body::Body::from(body)).expect("Valid error response")
    }
}

pub fn create_generic_error_response(
    status: StatusCode,
    message: &str,
    is_development: bool,
) -> axum::http::Response<axum::body::Body> {
    let response = if is_development {
        ErrorResponse {
            error: status.canonical_reason().unwrap_or("Error").to_string(),
            message: message.to_string(),
            code: None,
            route: None,
            method: None,
            stack: None,
            details: None,
        }
    } else {
        ErrorResponse {
            error: status.canonical_reason().unwrap_or("Error").to_string(),
            message: "An error occurred processing your request".to_string(),
            code: None,
            route: None,
            method: None,
            stack: None,
            details: None,
        }
    };

    let body = serde_json::to_string(&response).unwrap_or_else(|_| {
        r#"{"error":"Internal Server Error","message":"Failed to serialize error response"}"#
            .to_string()
    });

    axum::http::Response::builder()
        .status(status)
        .header("content-type", "application/json")
        .body(axum::body::Body::from(body))
        .expect("Valid error response")
}