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;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCategory {
DatabaseConstraint,
DatabaseConnection,
QuerySyntax,
Authentication,
Authorization,
NotFound,
Validation,
Internal,
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"),
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessedError {
pub category: ErrorCategory,
pub status_code: StatusCode,
pub error_code: &'static str,
pub message: String,
pub hint: Option<String>,
pub trace_id: String,
pub metadata: Map<String, Value>,
}
impl ProcessedError {
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(),
}
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn with_metadata_map(mut self, metadata: Map<String, Value>) -> Self {
self.metadata.extend(metadata);
self
}
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 = 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 {}
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)
}