Skip to main content

pipedash_web/
error.rs

1use axum::{
2    http::StatusCode,
3    response::{
4        IntoResponse,
5        Response,
6    },
7    Json,
8};
9use pipedash_core::domain::DomainError;
10use serde::Serialize;
11
12#[derive(Debug, Serialize)]
13pub struct ApiError {
14    pub code: String,
15    pub message: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub details: Option<String>,
18}
19
20impl ApiError {
21    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
22        Self {
23            code: code.into(),
24            message: message.into(),
25            details: None,
26        }
27    }
28}
29
30#[derive(Debug)]
31pub struct AppError {
32    pub status: StatusCode,
33    pub error: ApiError,
34}
35
36impl AppError {
37    pub fn new(status: StatusCode, error: ApiError) -> Self {
38        Self { status, error }
39    }
40
41    pub fn not_found(message: impl Into<String>) -> Self {
42        Self::new(StatusCode::NOT_FOUND, ApiError::new("NOT_FOUND", message))
43    }
44
45    pub fn bad_request(message: impl Into<String>) -> Self {
46        Self::new(
47            StatusCode::BAD_REQUEST,
48            ApiError::new("BAD_REQUEST", message),
49        )
50    }
51
52    pub fn internal(message: impl Into<String>) -> Self {
53        Self::new(
54            StatusCode::INTERNAL_SERVER_ERROR,
55            ApiError::new("INTERNAL_ERROR", message),
56        )
57    }
58
59    pub fn conflict(message: impl Into<String>) -> Self {
60        Self::new(StatusCode::CONFLICT, ApiError::new("CONFLICT", message))
61    }
62
63    pub fn not_initialized() -> Self {
64        Self::new(
65            StatusCode::SERVICE_UNAVAILABLE,
66            ApiError::new(
67                "NOT_INITIALIZED",
68                "Application not initialized - setup required",
69            ),
70        )
71    }
72}
73
74impl IntoResponse for AppError {
75    fn into_response(self) -> Response {
76        (self.status, Json(self.error)).into_response()
77    }
78}
79
80impl From<DomainError> for AppError {
81    fn from(err: DomainError) -> Self {
82        let status = match &err {
83            DomainError::ProviderNotFound(_)
84            | DomainError::PipelineNotFound(_)
85            | DomainError::NotFound(_) => StatusCode::NOT_FOUND,
86            DomainError::InvalidConfig(_) | DomainError::InvalidProviderType(_) => {
87                StatusCode::BAD_REQUEST
88            }
89            DomainError::AuthenticationFailed(_) => StatusCode::UNAUTHORIZED,
90            DomainError::ProviderAuthenticationFailed(_) | DomainError::PermissionDenied(_) => {
91                StatusCode::FORBIDDEN
92            }
93            DomainError::NotSupported(_) => StatusCode::NOT_IMPLEMENTED,
94            _ => StatusCode::INTERNAL_SERVER_ERROR,
95        };
96        AppError::new(status, ApiError::new(err.code(), err.to_string()))
97    }
98}
99
100impl From<anyhow::Error> for AppError {
101    fn from(err: anyhow::Error) -> Self {
102        AppError::internal(err.to_string())
103    }
104}
105
106pub type ApiResult<T> = Result<T, AppError>;