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
13 *
14 * The standard JSON error response structure used by NestForge.
15 * Provides consistent error formatting across all HTTP error responses.
16 *
17 * # JSON Structure
18 * ```json
19 * {
20 *   "statusCode": 500,
21 *   "error": "Internal Server Error",
22 *   "message": "Something went wrong"
23 * }
24 * ```
25 */
26#[derive(Serialize)]
27struct ErrorBody {
28    #[serde(rename = "statusCode")]
29    status_code: u16,
30    error: String,
31    code: &'static str,
32    message: String,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    details: Option<Value>,
35    #[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
36    request_id: Option<String>,
37}
38
39/**
40 * HttpException
41 *
42 * The framework's primary error type for HTTP responses.
43 * Enables controllers to return proper HTTP error responses
44 * without manually constructing response objects.
45 *
46 * # Fields
47 * - `status`: The HTTP status code
48 * - `code`: A machine-readable error code for programmatic error handling
49 * - `message`: A human-readable error message
50 * - `details`: Optional additional error details (often validation errors)
51 * - `request_id`: Optional request ID for correlation
52 */
53#[derive(Debug, Clone)]
54pub struct HttpException {
55    pub status: StatusCode,
56    pub code: &'static str,
57    pub message: String,
58    pub details: Option<Value>,
59    pub request_id: Option<String>,
60}
61
62impl HttpException {
63    /**
64     * Generic constructor for creating an HttpException.
65     *
66     * # Arguments
67     * - `status`: The HTTP status code
68     * - `code`: A machine-readable error code
69     * - `message`: A human-readable error message
70     */
71    pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
72        Self {
73            status,
74            code,
75            message: message.into(),
76            details: None,
77            request_id: None,
78        }
79    }
80
81    /**
82     * Constructor that includes additional error details.
83     *
84     * # Arguments
85     * - `status`: The HTTP status code
86     * - `code`: A machine-readable error code
87     * - `message`: A human-readable error message
88     * - `details`: Additional error details (often used for validation errors)
89     */
90    pub fn with_details(
91        status: StatusCode,
92        code: &'static str,
93        message: impl Into<String>,
94        details: Value,
95    ) -> Self {
96        Self {
97            status,
98            code,
99            message: message.into(),
100            details: Some(details),
101            request_id: None,
102        }
103    }
104
105    /**
106     * Attaches a request ID to the exception for correlation.
107     */
108    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
109        self.request_id = Some(request_id.into());
110        self
111    }
112
113    /**
114     * Attaches an optional request ID to the exception.
115     */
116    pub fn with_optional_request_id(mut self, request_id: Option<String>) -> Self {
117        self.request_id = request_id;
118        self
119    }
120
121    /**
122     * Creates a 400 Bad Request error.
123     */
124    pub fn bad_request(message: impl Into<String>) -> Self {
125        Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
126    }
127
128    /**
129     * Creates a 400 Bad Request error with validation errors.
130     */
131    pub fn bad_request_validation(errors: ValidationErrors) -> Self {
132        let message = "Validation failed".to_string();
133        let details = serde_json::to_value(errors).unwrap_or(Value::Null);
134        Self::with_details(
135            StatusCode::BAD_REQUEST,
136            "validation_failed",
137            message,
138            details,
139        )
140    }
141
142    /**
143     * Creates a 401 Unauthorized error.
144     */
145    pub fn unauthorized(message: impl Into<String>) -> Self {
146        Self::new(StatusCode::UNAUTHORIZED, "unauthorized", message)
147    }
148
149    /**
150     * Creates a 403 Forbidden error.
151     */
152    pub fn forbidden(message: impl Into<String>) -> Self {
153        Self::new(StatusCode::FORBIDDEN, "forbidden", message)
154    }
155
156    /**
157     * Creates a 404 Not Found error.
158     */
159    pub fn not_found(message: impl Into<String>) -> Self {
160        Self::new(StatusCode::NOT_FOUND, "not_found", message)
161    }
162
163    /**
164     * Creates a 500 Internal Server Error.
165     */
166    pub fn internal_server_error(message: impl Into<String>) -> Self {
167        Self::new(
168            StatusCode::INTERNAL_SERVER_ERROR,
169            "internal_server_error",
170            message,
171        )
172    }
173}
174
175/**
176 * IntoResponse Implementation
177 *
178 * Makes HttpException directly returnable from axum handlers.
179 * This enables returning `Result<Json<T>, HttpException>` from controllers
180 * and having axum automatically convert the error into a proper HTTP response.
181 */
182impl IntoResponse for HttpException {
183    fn into_response(self) -> Response {
184        let error_name = self
185            .status
186            .canonical_reason()
187            .unwrap_or("Error")
188            .to_string();
189
190        let body = ErrorBody {
191            status_code: self.status.as_u16(),
192            error: error_name,
193            code: self.code,
194            message: self.message,
195            details: self.details,
196            request_id: self.request_id,
197        };
198
199        (self.status, Json(body)).into_response()
200    }
201}