lmrc_http_common/
response.rs

1//! Standard HTTP response wrappers
2
3use axum::{
4    http::StatusCode,
5    response::{IntoResponse, Response},
6    Json,
7};
8use serde::{Deserialize, Serialize};
9
10/// Standard success response wrapper
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SuccessResponse<T> {
13    /// Success flag (always true)
14    pub success: bool,
15    /// Response data
16    pub data: T,
17    /// Optional metadata
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub meta: Option<serde_json::Value>,
20}
21
22impl<T> SuccessResponse<T> {
23    pub fn new(data: T) -> Self {
24        Self {
25            success: true,
26            data,
27            meta: None,
28        }
29    }
30
31    pub fn with_meta(mut self, meta: serde_json::Value) -> Self {
32        self.meta = Some(meta);
33        self
34    }
35}
36
37impl<T> IntoResponse for SuccessResponse<T>
38where
39    T: Serialize,
40{
41    fn into_response(self) -> Response {
42        Json(self).into_response()
43    }
44}
45
46/// Paginated response wrapper
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct PaginatedResponse<T> {
49    /// Success flag (always true)
50    pub success: bool,
51    /// Response data items
52    pub data: Vec<T>,
53    /// Pagination metadata
54    pub pagination: PaginationMeta,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct PaginationMeta {
59    /// Current page number (1-indexed)
60    pub page: u64,
61    /// Items per page
62    pub per_page: u64,
63    /// Total number of items
64    pub total: u64,
65    /// Total number of pages
66    pub total_pages: u64,
67    /// Whether there is a next page
68    pub has_next: bool,
69    /// Whether there is a previous page
70    pub has_prev: bool,
71}
72
73impl PaginationMeta {
74    pub fn new(page: u64, per_page: u64, total: u64) -> Self {
75        let total_pages = (total + per_page - 1) / per_page.max(1);
76        Self {
77            page,
78            per_page,
79            total,
80            total_pages,
81            has_next: page < total_pages,
82            has_prev: page > 1,
83        }
84    }
85}
86
87impl<T> PaginatedResponse<T> {
88    pub fn new(data: Vec<T>, page: u64, per_page: u64, total: u64) -> Self {
89        Self {
90            success: true,
91            data,
92            pagination: PaginationMeta::new(page, per_page, total),
93        }
94    }
95}
96
97impl<T> IntoResponse for PaginatedResponse<T>
98where
99    T: Serialize,
100{
101    fn into_response(self) -> Response {
102        Json(self).into_response()
103    }
104}
105
106/// Empty success response (for DELETE, etc.)
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct EmptyResponse {
109    pub success: bool,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub message: Option<String>,
112}
113
114impl EmptyResponse {
115    pub fn new() -> Self {
116        Self {
117            success: true,
118            message: None,
119        }
120    }
121
122    pub fn with_message(mut self, message: impl Into<String>) -> Self {
123        self.message = Some(message.into());
124        self
125    }
126}
127
128impl Default for EmptyResponse {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl IntoResponse for EmptyResponse {
135    fn into_response(self) -> Response {
136        Json(self).into_response()
137    }
138}
139
140/// Created response (201) with location header
141pub struct CreatedResponse<T> {
142    pub data: T,
143    pub location: Option<String>,
144}
145
146impl<T> CreatedResponse<T> {
147    pub fn new(data: T) -> Self {
148        Self {
149            data,
150            location: None,
151        }
152    }
153
154    pub fn with_location(mut self, location: impl Into<String>) -> Self {
155        self.location = Some(location.into());
156        self
157    }
158}
159
160impl<T> IntoResponse for CreatedResponse<T>
161where
162    T: Serialize,
163{
164    fn into_response(self) -> Response {
165        let mut response = (StatusCode::CREATED, Json(SuccessResponse::new(self.data))).into_response();
166
167        if let Some(location) = self.location
168            && let Ok(header_value) = location.parse() {
169                response.headers_mut().insert("Location", header_value);
170        }
171
172        response
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_pagination_meta() {
182        let meta = PaginationMeta::new(2, 10, 45);
183        assert_eq!(meta.page, 2);
184        assert_eq!(meta.per_page, 10);
185        assert_eq!(meta.total, 45);
186        assert_eq!(meta.total_pages, 5);
187        assert!(meta.has_next);
188        assert!(meta.has_prev);
189
190        let first_page = PaginationMeta::new(1, 10, 45);
191        assert!(!first_page.has_prev);
192        assert!(first_page.has_next);
193
194        let last_page = PaginationMeta::new(5, 10, 45);
195        assert!(last_page.has_prev);
196        assert!(!last_page.has_next);
197    }
198
199    #[test]
200    fn test_empty_response() {
201        let resp = EmptyResponse::new();
202        assert!(resp.success);
203        assert!(resp.message.is_none());
204
205        let resp = EmptyResponse::new().with_message("Resource deleted");
206        assert_eq!(resp.message, Some("Resource deleted".to_string()));
207    }
208}