athena_rs 2.9.1

Database gateway API
Documentation
//! Error post-processing module for sanitizing and formatting errors.
//!
//! This module provides utilities for:
//! - Categorizing errors by type
//! - Sanitizing SQL queries from error messages
//! - Converting technical errors to user-friendly messages
//! - Generating structured error responses with trace IDs and metadata

use actix_web::http::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use std::fmt;

pub mod hints;
pub mod sql_sanitizer;
pub mod sqlx_parser;
pub mod tokio_postgres_parser;

/// Categorizes errors by their nature for proper handling and response formatting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCategory {
    /// Database constraint violations (unique, foreign key, check, etc.)
    DatabaseConstraint,
    /// Database connection issues (pool exhaustion, timeouts, disconnects)
    DatabaseConnection,
    /// SQL syntax errors or invalid queries
    QuerySyntax,
    /// Authentication failures
    Authentication,
    /// Authorization/permission issues
    Authorization,
    /// Requested resource not found
    NotFound,
    /// Input validation errors
    Validation,
    /// Unexpected internal errors
    Internal,
    /// Rate limiting or throttling
    RateLimiting,
}

impl fmt::Display for ErrorCategory {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ErrorCategory::DatabaseConstraint => write!(f, "database_constraint"),
            ErrorCategory::DatabaseConnection => write!(f, "database_connection"),
            ErrorCategory::QuerySyntax => write!(f, "query_syntax"),
            ErrorCategory::Authentication => write!(f, "authentication"),
            ErrorCategory::Authorization => write!(f, "authorization"),
            ErrorCategory::NotFound => write!(f, "not_found"),
            ErrorCategory::Validation => write!(f, "validation"),
            ErrorCategory::Internal => write!(f, "internal"),
            ErrorCategory::RateLimiting => write!(f, "rate_limiting"),
        }
    }
}

/// A processed, sanitized error ready for client response.
///
/// This structure ensures:
/// - No SQL queries are exposed
/// - User-friendly messages
/// - Proper HTTP status codes
/// - Actionable hints when available
/// - Structured metadata for debugging
#[derive(Debug, Clone)]
pub struct ProcessedError {
    /// Error category for classification
    pub category: ErrorCategory,
    /// HTTP status code to return
    pub status_code: StatusCode,
    /// Machine-readable error code
    pub error_code: &'static str,
    /// User-friendly error message
    pub message: String,
    /// Optional actionable hint for resolving the error
    pub hint: Option<String>,
    /// Trace ID for correlating with server logs
    pub trace_id: String,
    /// Additional safe metadata (no SQL or sensitive data)
    pub metadata: Map<String, Value>,
}

impl ProcessedError {
    /// Creates a new ProcessedError with the given parameters.
    pub fn new(
        category: ErrorCategory,
        status_code: StatusCode,
        error_code: &'static str,
        message: impl Into<String>,
        trace_id: impl Into<String>,
    ) -> Self {
        Self {
            category,
            status_code,
            error_code,
            message: message.into(),
            hint: None,
            trace_id: trace_id.into(),
            metadata: Map::new(),
        }
    }

    /// Sets an actionable hint for this error.
    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
        self.hint = Some(hint.into());
        self
    }

    /// Adds a metadata key-value pair.
    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
        self.metadata.insert(key.into(), value);
        self
    }

    /// Adds multiple metadata entries at once.
    pub fn with_metadata_map(mut self, metadata: Map<String, Value>) -> Self {
        self.metadata.extend(metadata);
        self
    }

    /// Converts this error into a JSON response value.
    pub fn to_json(&self) -> Value {
        let mut details = self.metadata.clone();
        details.insert("category".to_string(), json!(self.category));
        details.insert("trace_id".to_string(), json!(self.trace_id));

        let mut response: Value = json!({
            "status": "error",
            "code": self.error_code,
            "message": self.message,
            "details": details,
            "trace_id": self.trace_id,
            "status_code": self.status_code.as_u16(),
        });

        if let Some(hint) = &self.hint {
            response["hint"] = json!(hint);
        }

        response
    }
}

impl fmt::Display for ProcessedError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "[{}] {}: {}",
            self.error_code, self.category, self.message
        )
    }
}

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

/// Generates a trace ID for error correlation.
///
/// This creates a short, URL-safe identifier for tracking errors
/// across logs and responses.
pub fn generate_trace_id() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_micros();
    format!("{:x}", timestamp & 0xFFFFFFFF)
}