1use axum::{
7 http::StatusCode,
8 response::{IntoResponse, Response},
9 Json,
10};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Serialize, Deserialize)]
15pub struct ApiResponse<T> {
16 pub data: T,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub meta: Option<ResponseMeta>,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
26pub struct ResponseMeta {
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub request_id: Option<String>,
30
31 pub timestamp: chrono::DateTime<chrono::Utc>,
33
34 #[serde(flatten)]
36 pub extra: std::collections::HashMap<String, serde_json::Value>,
37}
38
39impl ResponseMeta {
40 pub fn new() -> Self {
42 Self {
43 request_id: None,
44 timestamp: chrono::Utc::now(),
45 extra: std::collections::HashMap::new(),
46 }
47 }
48
49 pub fn with_request_id(mut self, request_id: String) -> Self {
51 self.request_id = Some(request_id);
52 self
53 }
54
55 pub fn with_extra(mut self, key: String, value: serde_json::Value) -> Self {
57 self.extra.insert(key, value);
58 self
59 }
60}
61
62impl Default for ResponseMeta {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl<T> ApiResponse<T> {
69 pub fn new(data: T) -> Self {
71 Self { data, meta: None }
72 }
73
74 pub fn with_meta(data: T, meta: ResponseMeta) -> Self {
76 Self {
77 data,
78 meta: Some(meta),
79 }
80 }
81}
82
83impl<T> IntoResponse for ApiResponse<T>
84where
85 T: Serialize,
86{
87 fn into_response(self) -> Response {
88 Json(self).into_response()
89 }
90}
91
92#[derive(Debug, Serialize, Deserialize)]
94pub struct PaginatedResponse<T> {
95 pub items: Vec<T>,
97
98 pub pagination: PaginationMeta,
100}
101
102#[derive(Debug, Serialize, Deserialize)]
104pub struct PaginationMeta {
105 pub total: i64,
107
108 pub offset: i64,
110
111 pub limit: i64,
113
114 pub has_more: bool,
116}
117
118impl<T> PaginatedResponse<T> {
119 pub fn new(items: Vec<T>, total: i64, offset: i64, limit: i64) -> Self {
121 let has_more = offset + items.len() as i64 > total.min(offset + limit);
122
123 Self {
124 items,
125 pagination: PaginationMeta {
126 total,
127 offset,
128 limit,
129 has_more,
130 },
131 }
132 }
133}
134
135impl<T> IntoResponse for PaginatedResponse<T>
136where
137 T: Serialize,
138{
139 fn into_response(self) -> Response {
140 Json(self).into_response()
141 }
142}
143
144#[derive(Debug, Serialize, Deserialize)]
146pub struct EmptyResponse {
147 pub message: String,
149}
150
151impl EmptyResponse {
152 pub fn new(message: impl Into<String>) -> Self {
154 Self {
155 message: message.into(),
156 }
157 }
158
159 pub fn success() -> Self {
161 Self {
162 message: "Success".to_string(),
163 }
164 }
165}
166
167impl IntoResponse for EmptyResponse {
168 fn into_response(self) -> Response {
169 Json(self).into_response()
170 }
171}
172
173#[derive(Debug, Serialize, Deserialize)]
175pub struct HealthResponse {
176 pub status: HealthStatus,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub version: Option<String>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub checks: Option<std::collections::HashMap<String, ComponentHealth>>,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "lowercase")]
191pub enum HealthStatus {
192 Healthy,
194 Degraded,
196 Unhealthy,
198}
199
200#[derive(Debug, Serialize, Deserialize)]
202pub struct ComponentHealth {
203 pub status: HealthStatus,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub message: Option<String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub metrics: Option<std::collections::HashMap<String, serde_json::Value>>,
213}
214
215impl HealthResponse {
216 pub fn healthy() -> Self {
218 Self {
219 status: HealthStatus::Healthy,
220 version: None,
221 checks: None,
222 }
223 }
224
225 pub fn with_version(mut self, version: impl Into<String>) -> Self {
227 self.version = Some(version.into());
228 self
229 }
230
231 pub fn with_check(mut self, name: impl Into<String>, health: ComponentHealth) -> Self {
233 if self.checks.is_none() {
234 self.checks = Some(std::collections::HashMap::new());
235 }
236 self.checks.as_mut().unwrap().insert(name.into(), health);
237 self
238 }
239
240 pub fn compute_status(mut self) -> Self {
242 if let Some(checks) = &self.checks {
243 let has_unhealthy = checks.values().any(|c| c.status == HealthStatus::Unhealthy);
244 let has_degraded = checks.values().any(|c| c.status == HealthStatus::Degraded);
245
246 self.status = if has_unhealthy {
247 HealthStatus::Unhealthy
248 } else if has_degraded {
249 HealthStatus::Degraded
250 } else {
251 HealthStatus::Healthy
252 };
253 }
254 self
255 }
256}
257
258impl IntoResponse for HealthResponse {
259 fn into_response(self) -> Response {
260 let status_code = match self.status {
261 HealthStatus::Healthy => StatusCode::OK,
262 HealthStatus::Degraded => StatusCode::OK, HealthStatus::Unhealthy => StatusCode::SERVICE_UNAVAILABLE,
264 };
265
266 (status_code, Json(self)).into_response()
267 }
268}
269
270impl ComponentHealth {
271 pub fn healthy() -> Self {
273 Self {
274 status: HealthStatus::Healthy,
275 message: None,
276 metrics: None,
277 }
278 }
279
280 pub fn degraded(message: impl Into<String>) -> Self {
282 Self {
283 status: HealthStatus::Degraded,
284 message: Some(message.into()),
285 metrics: None,
286 }
287 }
288
289 pub fn unhealthy(message: impl Into<String>) -> Self {
291 Self {
292 status: HealthStatus::Unhealthy,
293 message: Some(message.into()),
294 metrics: None,
295 }
296 }
297
298 pub fn with_metrics(
300 mut self,
301 metrics: std::collections::HashMap<String, serde_json::Value>,
302 ) -> Self {
303 self.metrics = Some(metrics);
304 self
305 }
306}
307
308pub fn ok<T>(data: T) -> ApiResponse<T> {
310 ApiResponse::new(data)
311}
312
313pub fn created<T>(data: T) -> (StatusCode, Json<ApiResponse<T>>)
315where
316 T: Serialize,
317{
318 (StatusCode::CREATED, Json(ApiResponse::new(data)))
319}
320
321pub fn no_content() -> StatusCode {
323 StatusCode::NO_CONTENT
324}
325
326pub fn deleted() -> (StatusCode, Json<EmptyResponse>) {
328 (
329 StatusCode::OK,
330 Json(EmptyResponse::new("Resource deleted successfully")),
331 )
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_api_response_creation() {
340 let response = ApiResponse::new("test data");
341 assert_eq!(response.data, "test data");
342 assert!(response.meta.is_none());
343 }
344
345 #[test]
346 fn test_paginated_response() {
347 let items = vec![1, 2, 3];
348 let response = PaginatedResponse::new(items, 10, 0, 5);
349
350 assert_eq!(response.items.len(), 3);
351 assert_eq!(response.pagination.total, 10);
352 assert_eq!(response.pagination.offset, 0);
353 assert_eq!(response.pagination.limit, 5);
354 }
355
356 #[test]
357 fn test_health_response_status_computation() {
358 let response = HealthResponse::healthy()
359 .with_check("db", ComponentHealth::healthy())
360 .with_check("cache", ComponentHealth::degraded("Slow response"))
361 .compute_status();
362
363 assert_eq!(response.status, HealthStatus::Degraded);
364 }
365
366 #[test]
367 fn test_response_meta() {
368 let meta = ResponseMeta::new()
369 .with_request_id("req-123".to_string())
370 .with_extra("key".to_string(), serde_json::json!("value"));
371
372 assert_eq!(meta.request_id, Some("req-123".to_string()));
373 assert!(meta.extra.contains_key("key"));
374 }
375}