openserve 2.0.3

A modern, high-performance, AI-enhanced file server built in Rust
Documentation
//! Error Handling Module
//!
//! This module provides comprehensive error handling for the OpenServe application.
//! It defines custom error types that provide better context and error reporting.

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;
use thiserror::Error;

/// Main application error type
#[derive(Error, Debug)]
pub enum AppError {
    /// An error related to authentication or authorization.
    #[error("Authentication error: {0}")]
    Auth(#[from] AuthError),
    
    /// An error related to file system operations.
    #[error("File system error: {0}")]
    FileSystem(#[from] FileSystemError),
    
    /// An error related to search operations.
    #[error("Search error: {0}")]
    Search(#[from] SearchError),
    
    /// An error related to the AI service.
    #[error("AI service error: {0}")]
    Ai(#[from] AiError),
    
    /// An error related to application configuration.
    #[error("Configuration error: {0}")]
    Config(#[from] ConfigError),
    
    /// An error related to data validation.
    #[error("Validation error: {0}")]
    Validation(#[from] ValidationError),
    
    /// A generic internal server error.
    #[error("Internal server error: {0}")]
    Internal(#[from] anyhow::Error),
}

/// Authentication-related errors.
#[derive(Error, Debug)]
pub enum AuthError {
    /// Provided credentials are invalid.
    #[error("Invalid credentials")]
    InvalidCredentials,
    
    /// The authentication token has expired.
    #[error("Token expired")]
    TokenExpired,
    
    /// The authentication token is invalid or malformed.
    #[error("Invalid token")]
    InvalidToken,
    
    /// The user does not have sufficient permissions for the operation.
    #[error("Insufficient permissions")]
    InsufficientPermissions,
    
    /// The requested user was not found.
    #[error("User not found")]
    UserNotFound,
    
    /// User registration is currently disabled.
    #[error("Registration disabled")]
    RegistrationDisabled,
}

/// File system operation errors.
#[derive(Error, Debug)]
pub enum FileSystemError {
    /// The requested file was not found.
    #[error("File not found: {path}")]
    FileNotFound {
        /// The path of the file that was not found.
        path: String,
    },
    
    /// The requested directory was not found.
    #[error("Directory not found: {path}")]
    DirectoryNotFound {
        /// The path of the directory that was not found.
        path: String,
    },
    
    /// Permission was denied for a file system operation.
    #[error("Permission denied: {path}")]
    PermissionDenied {
        /// The path for which permission was denied.
        path: String,
    },
    
    /// An attempt was made to access a path outside the allowed directory.
    #[error("Path outside allowed directory: {path}")]
    PathTraversal {
        /// The path that was attempted.
        path: String,
    },
    
    /// The file is larger than the configured maximum size.
    #[error("File too large: {size} bytes (max: {max_size} bytes)")]
    FileTooLarge {
        /// The size of the file.
        size: u64,
        /// The maximum allowed size.
        max_size: u64,
    },
    
    /// The disk is full.
    #[error("Disk full")]
    DiskFull,
    
    /// The filename is invalid.
    #[error("Invalid file name: {name}")]
    InvalidFileName {
        /// The invalid filename.
        name: String,
    },
}

/// Search-related errors.
#[derive(Error, Debug)]
pub enum SearchError {
    /// The search index was not found or is not available.
    #[error("Index not found")]
    IndexNotFound,
    
    /// The search query was invalid or malformed.
    #[error("Invalid query: {query}")]
    InvalidQuery {
        /// The invalid query string.
        query: String,
    },
    
    /// The search index is corrupted.
    #[error("Index corruption detected")]
    IndexCorruption,
    
    /// The search operation timed out.
    #[error("Search timeout")]
    Timeout,
}

/// AI service errors.
#[derive(Error, Debug)]
pub enum AiError {
    /// The AI service is not available.
    #[error("AI service not available")]
    ServiceUnavailable,
    
    /// The AI service API key is not configured.
    #[error("API key not configured")]
    ApiKeyNotConfigured,
    
    /// The rate limit for the AI service has been exceeded.
    #[error("Rate limit exceeded")]
    RateLimitExceeded,
    
    /// The specified AI model is invalid.
    #[error("Invalid model: {model}")]
    InvalidModel {
        /// The invalid model name.
        model: String,
    },
    
    /// The content is too long for the AI service to process.
    #[error("Content too long: {length} tokens (max: {max_length})")]
    ContentTooLong {
        /// The length of the content in tokens.
        length: usize,
        /// The maximum allowed length in tokens.
        max_length: usize,
    },
}

/// Configuration errors.
#[derive(Error, Debug)]
pub enum ConfigError {
    /// A required configuration key is missing.
    #[error("Missing required configuration: {key}")]
    MissingRequired {
        /// The name of the missing key.
        key: String,
    },
    
    /// A configuration value is invalid.
    #[error("Invalid configuration value for {key}: {value}")]
    InvalidValue {
        /// The name of the key with the invalid value.
        key: String,
        /// The invalid value.
        value: String,
    },
    
    /// The configuration file was not found.
    #[error("Configuration file not found: {path}")]
    FileNotFound {
        /// The path of the configuration file that was not found.
        path: String,
    },
    
    /// The configuration file could not be parsed.
    #[error("Configuration parse error: {message}")]
    ParseError {
        /// The parsing error message.
        message: String,
    },
}

/// Validation errors.
#[derive(Error, Debug)]
pub enum ValidationError {
    /// The email format is invalid.
    #[error("Invalid email format: {email}")]
    InvalidEmail {
        /// The invalid email address.
        email: String,
    },
    
    /// The password is too short.
    #[error("Password too short (minimum {min_length} characters)")]
    PasswordTooShort {
        /// The minimum required password length.
        min_length: usize,
    },
    
    /// The file extension is invalid.
    #[error("Invalid file extension: {extension}")]
    InvalidFileExtension {
        /// The invalid file extension.
        extension: String,
    },
    
    /// A required field is missing.
    #[error("Field required: {field}")]
    FieldRequired {
        /// The name of the required field.
        field: String,
    },
    
    /// A value is outside the allowed range.
    #[error("Value out of range for {field}: {value} (min: {min}, max: {max})")]
    ValueOutOfRange {
        /// The name of the field with the out-of-range value.
        field: String,
        /// The value that is out of range.
        value: String,
        /// The minimum allowed value.
        min: String,
        /// The maximum allowed value.
        max: String,
    },
}

/// Converts an `AppError` into an HTTP `Response`.
///
/// This implementation maps each variant of `AppError` to a specific
/// HTTP status code and a JSON error response body, providing a
/// consistent error handling mechanism for the web server.
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, error_message) = match &self {
            AppError::Auth(auth_error) => match auth_error {
                AuthError::InvalidCredentials => (StatusCode::UNAUTHORIZED, "Invalid credentials"),
                AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired"),
                AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token"),
                AuthError::InsufficientPermissions => (StatusCode::FORBIDDEN, "Insufficient permissions"),
                AuthError::UserNotFound => (StatusCode::NOT_FOUND, "User not found"),
                AuthError::RegistrationDisabled => (StatusCode::FORBIDDEN, "Registration disabled"),
            },
            
            AppError::FileSystem(fs_error) => match fs_error {
                FileSystemError::FileNotFound { .. } => (StatusCode::NOT_FOUND, "File not found"),
                FileSystemError::DirectoryNotFound { .. } => (StatusCode::NOT_FOUND, "Directory not found"),
                FileSystemError::PermissionDenied { .. } => (StatusCode::FORBIDDEN, "Permission denied"),
                FileSystemError::PathTraversal { .. } => (StatusCode::BAD_REQUEST, "Invalid path"),
                FileSystemError::FileTooLarge { .. } => (StatusCode::PAYLOAD_TOO_LARGE, "File too large"),
                FileSystemError::DiskFull => (StatusCode::INSUFFICIENT_STORAGE, "Disk full"),
                FileSystemError::InvalidFileName { .. } => (StatusCode::BAD_REQUEST, "Invalid file name"),
            },
            
            AppError::Search(search_error) => match search_error {
                SearchError::IndexNotFound => (StatusCode::SERVICE_UNAVAILABLE, "Search index not available"),
                SearchError::InvalidQuery { .. } => (StatusCode::BAD_REQUEST, "Invalid search query"),
                SearchError::IndexCorruption => (StatusCode::INTERNAL_SERVER_ERROR, "Search index corrupted"),
                SearchError::Timeout => (StatusCode::REQUEST_TIMEOUT, "Search timeout"),
            },
            
            AppError::Ai(ai_error) => match ai_error {
                AiError::ServiceUnavailable => (StatusCode::SERVICE_UNAVAILABLE, "AI service unavailable"),
                AiError::ApiKeyNotConfigured => (StatusCode::SERVICE_UNAVAILABLE, "AI service not configured"),
                AiError::RateLimitExceeded => (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded"),
                AiError::InvalidModel { .. } => (StatusCode::BAD_REQUEST, "Invalid AI model"),
                AiError::ContentTooLong { .. } => (StatusCode::PAYLOAD_TOO_LARGE, "Content too long"),
            },
            
            AppError::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Configuration error"),
            AppError::Validation(_) => (StatusCode::BAD_REQUEST, "Validation error"),
            AppError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
        };

        let body = Json(json!({
            "success": false,
            "error": error_message,
            "details": self.to_string(),
        }));

        (status, body).into_response()
    }
}

/// A convenience `Result` type alias for the application.
///
/// This type alias uses `AppError` as the default error type, simplifying
/// function signatures throughout the codebase.
pub type Result<T> = std::result::Result<T, AppError>;

/// A helper macro for creating `AuthError` variants of `AppError`.
#[macro_export]
macro_rules! auth_error {
    ($variant:ident) => {
        $crate::error::AppError::Auth($crate::error::AuthError::$variant)
    };
    ($variant:ident, $($arg:expr),*) => {
        $crate::error::AppError::Auth($crate::error::AuthError::$variant { $($arg),* })
    };
}

/// A helper macro for creating `FileSystemError` variants of `AppError`.
#[macro_export]
macro_rules! fs_error {
    ($variant:ident, $($arg:expr),*) => {
        $crate::error::AppError::FileSystem($crate::error::FileSystemError::$variant { $($arg),* })
    };
}

/// A helper macro for creating `ValidationError` variants of `AppError`.
#[macro_export]
macro_rules! validation_error {
    ($variant:ident, $($arg:expr),*) => {
        $crate::error::AppError::Validation($crate::error::ValidationError::$variant { $($arg),* })
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_display() {
        let error = AppError::Auth(AuthError::InvalidCredentials);
        assert_eq!(error.to_string(), "Authentication error: Invalid credentials");
    }

    #[test]
    fn test_file_system_error() {
        let error = AppError::FileSystem(FileSystemError::FileNotFound {
            path: "/test/file.txt".to_string(),
        });
        assert!(error.to_string().contains("File not found: /test/file.txt"));
    }

    #[test]
    fn test_validation_error() {
        let error = AppError::Validation(ValidationError::InvalidEmail {
            email: "invalid-email".to_string(),
        });
        assert!(error.to_string().contains("Invalid email format"));
    }
}