Skip to main content

nestforge_core/
error.rs

1use axum::{
2    http::StatusCode,
3    response::{IntoResponse, Response},
4    Json,
5};
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::ValidationErrors;
10
11/**
12* ErrorBody = standard JSON error response shape.
13*
14* Keeping this simple and clean for now:
15* {
16*   "statusCode": 500,
17*   "error": "Internal Server Error",
18*   "message": "Something went wrong"
19* }
20*/
21#[derive(Serialize)]
22struct ErrorBody {
23    #[serde(rename = "statusCode")]
24    status_code: u16,
25    error: String,
26    code: &'static str,
27    message: String,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    details: Option<Value>,
30    #[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
31    request_id: Option<String>,
32}
33
34/**
35* HttpException = framework error type
36* This lets controllers return proper HTTP errors without manually building responses.
37*/
38#[derive(Debug, Clone)]
39pub struct HttpException {
40    pub status: StatusCode,
41    pub code: &'static str,
42    pub message: String,
43    pub details: Option<Value>,
44    pub request_id: Option<String>,
45}
46
47impl HttpException {
48    /*
49    Generic constructor
50    */
51    pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
52        Self {
53            status,
54            code,
55            message: message.into(),
56            details: None,
57            request_id: None,
58        }
59    }
60
61    pub fn with_details(
62        status: StatusCode,
63        code: &'static str,
64        message: impl Into<String>,
65        details: Value,
66    ) -> Self {
67        Self {
68            status,
69            code,
70            message: message.into(),
71            details: Some(details),
72            request_id: None,
73        }
74    }
75
76    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
77        self.request_id = Some(request_id.into());
78        self
79    }
80
81    pub fn with_optional_request_id(mut self, request_id: Option<String>) -> Self {
82        self.request_id = request_id;
83        self
84    }
85
86    /*
87    Helper constructors (clean DX for controllers)
88    */
89    pub fn bad_request(message: impl Into<String>) -> Self {
90        Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
91    }
92
93    pub fn bad_request_validation(errors: ValidationErrors) -> Self {
94        let message = "Validation failed".to_string();
95        let details = serde_json::to_value(errors).unwrap_or(Value::Null);
96        Self::with_details(
97            StatusCode::BAD_REQUEST,
98            "validation_failed",
99            message,
100            details,
101        )
102    }
103
104    pub fn unauthorized(message: impl Into<String>) -> Self {
105        Self::new(StatusCode::UNAUTHORIZED, "unauthorized", message)
106    }
107
108    pub fn forbidden(message: impl Into<String>) -> Self {
109        Self::new(StatusCode::FORBIDDEN, "forbidden", message)
110    }
111
112    pub fn not_found(message: impl Into<String>) -> Self {
113        Self::new(StatusCode::NOT_FOUND, "not_found", message)
114    }
115
116    pub fn internal_server_error(message: impl Into<String>) -> Self {
117        Self::new(
118            StatusCode::INTERNAL_SERVER_ERROR,
119            "internal_server_error",
120            message,
121        )
122    }
123}
124
125/**
126* IntoResponse makes HttpException directly returnable from axum handlers.
127*
128* So handlers can return:
129* Result<Json<T>, HttpException>
130* and axum knows how to turn the error into a real HTTP response.
131*/
132impl IntoResponse for HttpException {
133    fn into_response(self) -> Response {
134        let error_name = self
135            .status
136            .canonical_reason()
137            .unwrap_or("Error")
138            .to_string();
139
140        let body = ErrorBody {
141            status_code: self.status.as_u16(),
142            error: error_name,
143            code: self.code,
144            message: self.message,
145            details: self.details,
146            request_id: self.request_id,
147        };
148
149        (self.status, Json(body)).into_response()
150    }
151}