reinhardt_rest/response.rs
1//! REST API response types
2//!
3//! Re-exports pagination types from reinhardt-pagination and provides
4//! REST-specific response utilities compatible with Django REST Framework.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9// Re-export PaginatedResponse from reinhardt-pagination (via reinhardt-core)
10pub use reinhardt_core::pagination::PaginatedResponse;
11
12/// DRF-style API response
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ApiResponse<T> {
15 /// Response data
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub data: Option<T>,
18
19 /// Error message (if any)
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub error: Option<String>,
22
23 /// Error details (if any)
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub errors: Option<HashMap<String, Vec<String>>>,
26
27 /// Status code
28 pub status: u16,
29
30 /// Message (for informational responses)
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub message: Option<String>,
33}
34
35impl<T> ApiResponse<T> {
36 /// Create a successful response
37 ///
38 /// # Examples
39 ///
40 /// ```
41 /// use reinhardt_rest::ApiResponse;
42 /// use serde::{Serialize, Deserialize};
43 ///
44 /// #[derive(Debug, Serialize, Deserialize)]
45 /// struct User {
46 /// id: i64,
47 /// name: String,
48 /// }
49 ///
50 /// let user = User { id: 1, name: "Alice".to_string() };
51 /// let response = ApiResponse::success(user);
52 ///
53 /// assert_eq!(response.status, 200);
54 /// assert!(response.data.is_some());
55 /// assert!(response.error.is_none());
56 /// ```
57 pub fn success(data: T) -> Self {
58 Self {
59 data: Some(data),
60 error: None,
61 errors: None,
62 status: 200,
63 message: None,
64 }
65 }
66 /// Create a success response with custom status
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// use reinhardt_rest::ApiResponse;
72 /// use serde::{Serialize, Deserialize};
73 ///
74 /// #[derive(Debug, Serialize, Deserialize)]
75 /// struct NewUser {
76 /// id: i64,
77 /// name: String,
78 /// }
79 ///
80 /// let user = NewUser { id: 1, name: "Bob".to_string() };
81 /// let response = ApiResponse::success_with_status(user, 201);
82 ///
83 /// assert_eq!(response.status, 201);
84 /// assert!(response.data.is_some());
85 /// ```
86 pub fn success_with_status(data: T, status: u16) -> Self {
87 Self {
88 data: Some(data),
89 error: None,
90 errors: None,
91 status,
92 message: None,
93 }
94 }
95 /// Create an error response
96 ///
97 /// # Examples
98 ///
99 /// ```
100 /// use reinhardt_rest::ApiResponse;
101 ///
102 /// let response: ApiResponse<String> = ApiResponse::error("Database connection failed", 500);
103 ///
104 /// assert_eq!(response.status, 500);
105 /// assert!(response.data.is_none());
106 /// assert_eq!(response.error.as_ref().unwrap(), "Database connection failed");
107 /// ```
108 pub fn error(message: impl Into<String>, status: u16) -> Self {
109 Self {
110 data: None,
111 error: Some(message.into()),
112 errors: None,
113 status,
114 message: None,
115 }
116 }
117 /// Create a validation error response
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// use reinhardt_rest::ApiResponse;
123 /// use std::collections::HashMap;
124 ///
125 /// let mut errors = HashMap::new();
126 /// errors.insert("email".to_string(), vec!["Invalid email format".to_string()]);
127 /// errors.insert("age".to_string(), vec!["Must be 18 or older".to_string()]);
128 ///
129 /// let response: ApiResponse<String> = ApiResponse::validation_error(errors);
130 ///
131 /// assert_eq!(response.status, 400);
132 /// assert!(response.errors.is_some());
133 /// assert_eq!(response.error.as_ref().unwrap(), "Validation failed");
134 /// ```
135 pub fn validation_error(errors: HashMap<String, Vec<String>>) -> Self {
136 Self {
137 data: None,
138 error: Some("Validation failed".to_string()),
139 errors: Some(errors),
140 status: 400,
141 message: None,
142 }
143 }
144 /// Create a not found response
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// use reinhardt_rest::ApiResponse;
150 ///
151 /// let response: ApiResponse<String> = ApiResponse::not_found();
152 ///
153 /// assert_eq!(response.status, 404);
154 /// assert!(response.data.is_none());
155 /// assert_eq!(response.error.as_ref().unwrap(), "Not found");
156 /// ```
157 pub fn not_found() -> Self {
158 Self {
159 data: None,
160 error: Some("Not found".to_string()),
161 errors: None,
162 status: 404,
163 message: None,
164 }
165 }
166 /// Create an unauthorized response
167 ///
168 /// # Examples
169 ///
170 /// ```
171 /// use reinhardt_rest::ApiResponse;
172 ///
173 /// let response: ApiResponse<String> = ApiResponse::unauthorized();
174 ///
175 /// assert_eq!(response.status, 401);
176 /// assert!(response.data.is_none());
177 /// assert_eq!(response.error.as_ref().unwrap(), "Unauthorized");
178 /// ```
179 pub fn unauthorized() -> Self {
180 Self {
181 data: None,
182 error: Some("Unauthorized".to_string()),
183 errors: None,
184 status: 401,
185 message: None,
186 }
187 }
188 /// Create a forbidden response
189 ///
190 /// # Examples
191 ///
192 /// ```
193 /// use reinhardt_rest::ApiResponse;
194 ///
195 /// let response: ApiResponse<String> = ApiResponse::forbidden();
196 ///
197 /// assert_eq!(response.status, 403);
198 /// assert!(response.data.is_none());
199 /// assert_eq!(response.error.as_ref().unwrap(), "Forbidden");
200 /// ```
201 pub fn forbidden() -> Self {
202 Self {
203 data: None,
204 error: Some("Forbidden".to_string()),
205 errors: None,
206 status: 403,
207 message: None,
208 }
209 }
210 /// Add a message to the response
211 ///
212 /// # Examples
213 ///
214 /// ```
215 /// use reinhardt_rest::ApiResponse;
216 /// use serde::{Serialize, Deserialize};
217 ///
218 /// #[derive(Debug, Serialize, Deserialize)]
219 /// struct Item {
220 /// id: i64,
221 /// }
222 ///
223 /// let item = Item { id: 1 };
224 /// let response = ApiResponse::success(item).with_message("Item created successfully");
225 ///
226 /// assert_eq!(response.message.as_ref().unwrap(), "Item created successfully");
227 /// ```
228 pub fn with_message(mut self, message: impl Into<String>) -> Self {
229 self.message = Some(message.into());
230 self
231 }
232 /// Convert to JSON
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use reinhardt_rest::ApiResponse;
238 /// use serde::{Serialize, Deserialize};
239 ///
240 /// #[derive(Debug, Serialize, Deserialize)]
241 /// struct Data {
242 /// value: i32,
243 /// }
244 ///
245 /// let response = ApiResponse::success(Data { value: 42 });
246 /// let json = response.to_json().unwrap();
247 ///
248 /// assert!(json.contains("\"value\":42"));
249 /// assert!(json.contains("\"status\":200"));
250 /// ```
251 pub fn to_json(&self) -> Result<String, serde_json::Error>
252 where
253 T: Serialize,
254 {
255 serde_json::to_string(self)
256 }
257 /// Convert to pretty JSON
258 ///
259 /// # Examples
260 ///
261 /// ```
262 /// use reinhardt_rest::ApiResponse;
263 /// use serde::{Serialize, Deserialize};
264 ///
265 /// #[derive(Debug, Serialize, Deserialize)]
266 /// struct Data {
267 /// value: i32,
268 /// }
269 ///
270 /// let response = ApiResponse::success(Data { value: 42 });
271 /// let json = response.to_json_pretty().unwrap();
272 ///
273 /// assert!(json.contains("\"value\": 42"));
274 /// assert!(json.contains('\n')); // Checks for pretty formatting
275 /// ```
276 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error>
277 where
278 T: Serialize,
279 {
280 serde_json::to_string_pretty(self)
281 }
282}
283
284/// Response builder for REST API responses
285pub struct ResponseBuilder<T> {
286 data: Option<T>,
287 error: Option<String>,
288 errors: Option<HashMap<String, Vec<String>>>,
289 status: u16,
290 message: Option<String>,
291}
292
293impl<T> ResponseBuilder<T> {
294 /// Create a new response builder
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use reinhardt_rest::ResponseBuilder;
300 ///
301 /// let builder = ResponseBuilder::<String>::new();
302 /// let response = builder.build();
303 ///
304 /// assert_eq!(response.status, 200);
305 /// assert!(response.data.is_none());
306 /// ```
307 pub fn new() -> Self {
308 Self {
309 data: None,
310 error: None,
311 errors: None,
312 status: 200,
313 message: None,
314 }
315 }
316 /// Set response data
317 ///
318 /// # Examples
319 ///
320 /// ```
321 /// use reinhardt_rest::ResponseBuilder;
322 ///
323 /// let response = ResponseBuilder::new()
324 /// .data("Success data")
325 /// .build();
326 ///
327 /// assert!(response.data.is_some());
328 /// assert_eq!(response.data.unwrap(), "Success data");
329 /// ```
330 pub fn data(mut self, data: T) -> Self {
331 self.data = Some(data);
332 self
333 }
334 /// Set error message
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use reinhardt_rest::ResponseBuilder;
340 ///
341 /// let response = ResponseBuilder::<String>::new()
342 /// .error("Something went wrong")
343 /// .status(500)
344 /// .build();
345 ///
346 /// assert_eq!(response.error.as_ref().unwrap(), "Something went wrong");
347 /// assert_eq!(response.status, 500);
348 /// ```
349 pub fn error(mut self, error: impl Into<String>) -> Self {
350 self.error = Some(error.into());
351 self
352 }
353 /// Set validation errors
354 ///
355 /// # Examples
356 ///
357 /// ```
358 /// use reinhardt_rest::ResponseBuilder;
359 /// use std::collections::HashMap;
360 ///
361 /// let mut errors = HashMap::new();
362 /// errors.insert("email".to_string(), vec!["Invalid format".to_string()]);
363 ///
364 /// let response = ResponseBuilder::<String>::new()
365 /// .errors(errors)
366 /// .status(400)
367 /// .build();
368 ///
369 /// assert_eq!(response.status, 400);
370 /// assert!(response.errors.is_some());
371 /// ```
372 pub fn errors(mut self, errors: HashMap<String, Vec<String>>) -> Self {
373 self.errors = Some(errors);
374 self
375 }
376 /// Set status code
377 ///
378 /// # Examples
379 ///
380 /// ```
381 /// use reinhardt_rest::ResponseBuilder;
382 ///
383 /// let response = ResponseBuilder::<String>::new()
384 /// .status(201)
385 /// .build();
386 ///
387 /// assert_eq!(response.status, 201);
388 /// ```
389 pub fn status(mut self, status: u16) -> Self {
390 self.status = status;
391 self
392 }
393 /// Set message
394 ///
395 /// # Examples
396 ///
397 /// ```
398 /// use reinhardt_rest::ResponseBuilder;
399 ///
400 /// let response = ResponseBuilder::<String>::new()
401 /// .message("Operation completed successfully")
402 /// .build();
403 ///
404 /// assert_eq!(response.message.unwrap(), "Operation completed successfully");
405 /// ```
406 pub fn message(mut self, message: impl Into<String>) -> Self {
407 self.message = Some(message.into());
408 self
409 }
410 /// Build the response
411 ///
412 /// # Examples
413 ///
414 /// ```
415 /// use reinhardt_rest::ResponseBuilder;
416 ///
417 /// let response = ResponseBuilder::<String>::new()
418 /// .data("Success".to_string())
419 /// .status(200)
420 /// .build();
421 ///
422 /// assert_eq!(response.status, 200);
423 /// assert!(response.data.is_some());
424 /// ```
425 pub fn build(self) -> ApiResponse<T> {
426 ApiResponse {
427 data: self.data,
428 error: self.error,
429 errors: self.errors,
430 status: self.status,
431 message: self.message,
432 }
433 }
434}
435
436impl<T> Default for ResponseBuilder<T> {
437 fn default() -> Self {
438 Self::new()
439 }
440}
441
442/// Trait for converting to REST API responses
443pub trait IntoApiResponse<T> {
444 /// Converts this value into an `ApiResponse<T>`.
445 fn into_api_response(self) -> ApiResponse<T>;
446}
447
448impl<T> IntoApiResponse<T> for Result<T, String> {
449 fn into_api_response(self) -> ApiResponse<T> {
450 match self {
451 Ok(data) => ApiResponse::success(data),
452 Err(err) => ApiResponse::error(err, 500),
453 }
454 }
455}
456
457impl<T> IntoApiResponse<T> for Option<T> {
458 fn into_api_response(self) -> ApiResponse<T> {
459 match self {
460 Some(data) => ApiResponse::success(data),
461 None => ApiResponse::not_found(),
462 }
463 }
464}
465
466// Tests are located in the underlying specialized crates:
467// - Response functionality tests: `reinhardt-http/tests/`
468// - Integration tests: `tests/integration/`
469
470// All tests have been moved to specialized crates
471// This module only contains re-export functionality