Skip to main content

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