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}