vantus 0.3.0

Macro-first async Rust backend framework with explicit composition, typed extraction, and hardened HTTP defaults.
Documentation
use thiserror::Error;

use crate::config::ConfigError;
use crate::core::http::Response;
use crate::di::ExtractorError;

#[derive(Debug, Error)]
pub enum FrameworkError {
    #[error(transparent)]
    Http(#[from] HttpError),
    #[error("{context}: {source}")]
    Config {
        context: &'static str,
        #[source]
        source: ConfigError,
    },
    #[error(transparent)]
    Extractor(#[from] ExtractorError),
    #[error(
        "duplicate route registration for {method} {new_path} (conflicts with {existing_path})"
    )]
    RouteConflict {
        method: String,
        existing_path: String,
        new_path: String,
    },
    #[error("{context}")]
    Startup { context: String },
    #[error("{message}")]
    Internal { message: String },
}

impl FrameworkError {
    pub fn to_response(&self) -> Response {
        match self {
            FrameworkError::Http(error) => error.to_response(),
            FrameworkError::Extractor(source) => {
                HttpError::bad_request(source.to_string()).to_response()
            }
            FrameworkError::Config { .. }
            | FrameworkError::RouteConflict { .. }
            | FrameworkError::Startup { .. }
            | FrameworkError::Internal { .. } => Response::internal_server_error(),
        }
    }

    pub fn internal(message: impl Into<String>) -> Self {
        Self::Internal {
            message: message.into(),
        }
    }

    pub fn startup(context: impl Into<String>) -> Self {
        Self::Startup {
            context: context.into(),
        }
    }

    pub fn config_context(context: &'static str, source: ConfigError) -> Self {
        Self::Config { context, source }
    }

    pub fn route_conflict(
        method: impl Into<String>,
        existing_path: impl Into<String>,
        new_path: impl Into<String>,
    ) -> Self {
        Self::RouteConflict {
            method: method.into(),
            existing_path: existing_path.into(),
            new_path: new_path.into(),
        }
    }
}

#[derive(Debug, Clone, Error)]
#[error("{status_code} {status_text}")]
pub struct HttpError {
    pub status_code: u16,
    pub status_text: String,
    pub message: String,
}

impl HttpError {
    pub fn new(
        status_code: u16,
        status_text: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Self {
            status_code,
            status_text: status_text.into(),
            message: message.into(),
        }
    }

    pub fn bad_request(message: impl Into<String>) -> Self {
        Self::new(400, "Bad Request", message)
    }

    pub fn not_found(message: impl Into<String>) -> Self {
        Self::new(404, "Not Found", message)
    }

    pub fn method_not_allowed(message: impl Into<String>) -> Self {
        Self::new(405, "Method Not Allowed", message)
    }

    pub fn payload_too_large(message: impl Into<String>) -> Self {
        Self::new(413, "Payload Too Large", message)
    }

    pub fn unsupported_media_type(message: impl Into<String>) -> Self {
        Self::new(415, "Unsupported Media Type", message)
    }

    pub fn too_many_requests(message: impl Into<String>) -> Self {
        Self::new(429, "Too Many Requests", message)
    }

    pub fn internal_server_error(message: impl Into<String>) -> Self {
        Self::new(500, "Internal Server Error", message)
    }

    pub fn to_response(&self) -> Response {
        Response::from_error(self.status_code, &self.status_text, self.message.clone())
    }
}