llm_registry_api/
responses.rs

1//! API response types
2//!
3//! This module defines standard response wrappers and helper functions
4//! for creating consistent HTTP responses.
5
6use axum::{
7    http::StatusCode,
8    response::{IntoResponse, Response},
9    Json,
10};
11use serde::{Deserialize, Serialize};
12
13/// Standard success response wrapper
14#[derive(Debug, Serialize, Deserialize)]
15pub struct ApiResponse<T> {
16    /// Response data
17    pub data: T,
18
19    /// Response metadata
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub meta: Option<ResponseMeta>,
22}
23
24/// Response metadata
25#[derive(Debug, Serialize, Deserialize)]
26pub struct ResponseMeta {
27    /// Request ID for tracking
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub request_id: Option<String>,
30
31    /// Timestamp of response
32    pub timestamp: chrono::DateTime<chrono::Utc>,
33
34    /// Additional metadata fields
35    #[serde(flatten)]
36    pub extra: std::collections::HashMap<String, serde_json::Value>,
37}
38
39impl ResponseMeta {
40    /// Create new response metadata
41    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    /// Set request ID
50    pub fn with_request_id(mut self, request_id: String) -> Self {
51        self.request_id = Some(request_id);
52        self
53    }
54
55    /// Add extra field
56    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    /// Create a new API response
70    pub fn new(data: T) -> Self {
71        Self { data, meta: None }
72    }
73
74    /// Create a response with metadata
75    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/// Paginated response wrapper
93#[derive(Debug, Serialize, Deserialize)]
94pub struct PaginatedResponse<T> {
95    /// List of items
96    pub items: Vec<T>,
97
98    /// Pagination metadata
99    pub pagination: PaginationMeta,
100}
101
102/// Pagination metadata
103#[derive(Debug, Serialize, Deserialize)]
104pub struct PaginationMeta {
105    /// Total number of items (without pagination)
106    pub total: i64,
107
108    /// Current offset
109    pub offset: i64,
110
111    /// Current limit
112    pub limit: i64,
113
114    /// Whether there are more results
115    pub has_more: bool,
116}
117
118impl<T> PaginatedResponse<T> {
119    /// Create a new paginated response
120    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/// Empty response for operations with no return data
145#[derive(Debug, Serialize, Deserialize)]
146pub struct EmptyResponse {
147    /// Success message
148    pub message: String,
149}
150
151impl EmptyResponse {
152    /// Create a new empty response
153    pub fn new(message: impl Into<String>) -> Self {
154        Self {
155            message: message.into(),
156        }
157    }
158
159    /// Create a default success response
160    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/// Health check response
174#[derive(Debug, Serialize, Deserialize)]
175pub struct HealthResponse {
176    /// Service status
177    pub status: HealthStatus,
178
179    /// Service version
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub version: Option<String>,
182
183    /// Component health checks
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub checks: Option<std::collections::HashMap<String, ComponentHealth>>,
186}
187
188/// Health status
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "lowercase")]
191pub enum HealthStatus {
192    /// Service is healthy
193    Healthy,
194    /// Service is degraded but operational
195    Degraded,
196    /// Service is unhealthy
197    Unhealthy,
198}
199
200/// Component health status
201#[derive(Debug, Serialize, Deserialize)]
202pub struct ComponentHealth {
203    /// Component status
204    pub status: HealthStatus,
205
206    /// Optional message
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub message: Option<String>,
209
210    /// Optional metrics
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub metrics: Option<std::collections::HashMap<String, serde_json::Value>>,
213}
214
215impl HealthResponse {
216    /// Create a healthy response
217    pub fn healthy() -> Self {
218        Self {
219            status: HealthStatus::Healthy,
220            version: None,
221            checks: None,
222        }
223    }
224
225    /// Create a response with version
226    pub fn with_version(mut self, version: impl Into<String>) -> Self {
227        self.version = Some(version.into());
228        self
229    }
230
231    /// Add a component health check
232    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    /// Determine overall health status from component checks
241    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, // Still 200 but degraded
263            HealthStatus::Unhealthy => StatusCode::SERVICE_UNAVAILABLE,
264        };
265
266        (status_code, Json(self)).into_response()
267    }
268}
269
270impl ComponentHealth {
271    /// Create a healthy component
272    pub fn healthy() -> Self {
273        Self {
274            status: HealthStatus::Healthy,
275            message: None,
276            metrics: None,
277        }
278    }
279
280    /// Create a degraded component
281    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    /// Create an unhealthy component
290    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    /// Add metrics
299    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
308/// Helper function to create a success response
309pub fn ok<T>(data: T) -> ApiResponse<T> {
310    ApiResponse::new(data)
311}
312
313/// Helper function to create a created response (201)
314pub fn created<T>(data: T) -> (StatusCode, Json<ApiResponse<T>>)
315where
316    T: Serialize,
317{
318    (StatusCode::CREATED, Json(ApiResponse::new(data)))
319}
320
321/// Helper function to create a no content response (204)
322pub fn no_content() -> StatusCode {
323    StatusCode::NO_CONTENT
324}
325
326/// Helper function to create a deleted response
327pub 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}