File size: 2,800 Bytes
71217a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use serde_json::json;

#[derive(Debug, Serialize)]
#[allow(dead_code)]
pub struct ErrorPayload {
    pub status: String,
    pub code: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<serde_json::Value>,
}

pub fn error_payload(
    message: &str,
    code: &str,
    details: Option<serde_json::Value>,
) -> serde_json::Value {
    let mut payload = json!({
        "status": "error",
        "code": code,
        "message": message,
    });
    if let Some(d) = details {
        payload["details"] = d;
    }
    payload
}

pub struct AppError {
    pub status_code: StatusCode,
    pub body: serde_json::Value,
}

impl AppError {
    pub fn new(status_code: StatusCode, message: &str, code: &str) -> Self {
        Self {
            status_code,
            body: json!({ "detail": error_payload(message, code, None) }),
        }
    }

    #[allow(dead_code)]
    pub fn with_details(
        status_code: StatusCode,
        message: &str,
        code: &str,
        details: serde_json::Value,
    ) -> Self {
        Self {
            status_code,
            body: json!({ "detail": error_payload(message, code, Some(details)) }),
        }
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        (self.status_code, axum::Json(self.body)).into_response()
    }
}

pub fn http_error(status_code: StatusCode, message: &str, code: &str) -> AppError {
    AppError::new(status_code, message, code)
}

// --- Typed error handling ---

#[derive(Debug, thiserror::Error)]
pub enum AppErrorKind {
    #[error("Database error: {0}")]
    Database(#[from] rusqlite::Error),

    #[error("Pool error: {0}")]
    Pool(#[from] r2d2::Error),

    #[error("Telegram API error: {0}")]
    Telegram(String),

    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Configuration error: {0}")]
    #[allow(dead_code)]
    Config(String),

    #[error("{0}")]
    Other(String),
}

impl From<AppErrorKind> for AppError {
    fn from(kind: AppErrorKind) -> Self {
        let (status_code, code) = match &kind {
            AppErrorKind::Database(_) | AppErrorKind::Pool(_) => {
                (StatusCode::INTERNAL_SERVER_ERROR, "database_error")
            }
            AppErrorKind::Telegram(_) => (StatusCode::BAD_GATEWAY, "telegram_error"),
            AppErrorKind::Http(_) => (StatusCode::BAD_GATEWAY, "http_error"),
            AppErrorKind::Config(_) => (StatusCode::SERVICE_UNAVAILABLE, "config_error"),
            AppErrorKind::Other(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error"),
        };
        AppError::new(status_code, &kind.to_string(), code)
    }
}