use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiResponse<T> {
pub data: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ResponseMeta>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResponseMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, serde_json::Value>,
}
impl ResponseMeta {
pub fn new() -> Self {
Self {
request_id: None,
timestamp: chrono::Utc::now(),
extra: std::collections::HashMap::new(),
}
}
pub fn with_request_id(mut self, request_id: String) -> Self {
self.request_id = Some(request_id);
self
}
pub fn with_extra(mut self, key: String, value: serde_json::Value) -> Self {
self.extra.insert(key, value);
self
}
}
impl Default for ResponseMeta {
fn default() -> Self {
Self::new()
}
}
impl<T> ApiResponse<T> {
pub fn new(data: T) -> Self {
Self { data, meta: None }
}
pub fn with_meta(data: T, meta: ResponseMeta) -> Self {
Self {
data,
meta: Some(meta),
}
}
}
impl<T> IntoResponse for ApiResponse<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
Json(self).into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PaginatedResponse<T> {
pub items: Vec<T>,
pub pagination: PaginationMeta,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PaginationMeta {
pub total: i64,
pub offset: i64,
pub limit: i64,
pub has_more: bool,
}
impl<T> PaginatedResponse<T> {
pub fn new(items: Vec<T>, total: i64, offset: i64, limit: i64) -> Self {
let has_more = offset + items.len() as i64 > total.min(offset + limit);
Self {
items,
pagination: PaginationMeta {
total,
offset,
limit,
has_more,
},
}
}
}
impl<T> IntoResponse for PaginatedResponse<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
Json(self).into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EmptyResponse {
pub message: String,
}
impl EmptyResponse {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
pub fn success() -> Self {
Self {
message: "Success".to_string(),
}
}
}
impl IntoResponse for EmptyResponse {
fn into_response(self) -> Response {
Json(self).into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HealthResponse {
pub status: HealthStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checks: Option<std::collections::HashMap<String, ComponentHealth>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ComponentHealth {
pub status: HealthStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics: Option<std::collections::HashMap<String, serde_json::Value>>,
}
impl HealthResponse {
pub fn healthy() -> Self {
Self {
status: HealthStatus::Healthy,
version: None,
checks: None,
}
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn with_check(mut self, name: impl Into<String>, health: ComponentHealth) -> Self {
if self.checks.is_none() {
self.checks = Some(std::collections::HashMap::new());
}
self.checks.as_mut().unwrap().insert(name.into(), health);
self
}
pub fn compute_status(mut self) -> Self {
if let Some(checks) = &self.checks {
let has_unhealthy = checks.values().any(|c| c.status == HealthStatus::Unhealthy);
let has_degraded = checks.values().any(|c| c.status == HealthStatus::Degraded);
self.status = if has_unhealthy {
HealthStatus::Unhealthy
} else if has_degraded {
HealthStatus::Degraded
} else {
HealthStatus::Healthy
};
}
self
}
}
impl IntoResponse for HealthResponse {
fn into_response(self) -> Response {
let status_code = match self.status {
HealthStatus::Healthy => StatusCode::OK,
HealthStatus::Degraded => StatusCode::OK, HealthStatus::Unhealthy => StatusCode::SERVICE_UNAVAILABLE,
};
(status_code, Json(self)).into_response()
}
}
impl ComponentHealth {
pub fn healthy() -> Self {
Self {
status: HealthStatus::Healthy,
message: None,
metrics: None,
}
}
pub fn degraded(message: impl Into<String>) -> Self {
Self {
status: HealthStatus::Degraded,
message: Some(message.into()),
metrics: None,
}
}
pub fn unhealthy(message: impl Into<String>) -> Self {
Self {
status: HealthStatus::Unhealthy,
message: Some(message.into()),
metrics: None,
}
}
pub fn with_metrics(
mut self,
metrics: std::collections::HashMap<String, serde_json::Value>,
) -> Self {
self.metrics = Some(metrics);
self
}
}
pub fn ok<T>(data: T) -> ApiResponse<T> {
ApiResponse::new(data)
}
pub fn created<T>(data: T) -> (StatusCode, Json<ApiResponse<T>>)
where
T: Serialize,
{
(StatusCode::CREATED, Json(ApiResponse::new(data)))
}
pub fn no_content() -> StatusCode {
StatusCode::NO_CONTENT
}
pub fn deleted() -> (StatusCode, Json<EmptyResponse>) {
(
StatusCode::OK,
Json(EmptyResponse::new("Resource deleted successfully")),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_response_creation() {
let response = ApiResponse::new("test data");
assert_eq!(response.data, "test data");
assert!(response.meta.is_none());
}
#[test]
fn test_paginated_response() {
let items = vec![1, 2, 3];
let response = PaginatedResponse::new(items, 10, 0, 5);
assert_eq!(response.items.len(), 3);
assert_eq!(response.pagination.total, 10);
assert_eq!(response.pagination.offset, 0);
assert_eq!(response.pagination.limit, 5);
}
#[test]
fn test_health_response_status_computation() {
let response = HealthResponse::healthy()
.with_check("db", ComponentHealth::healthy())
.with_check("cache", ComponentHealth::degraded("Slow response"))
.compute_status();
assert_eq!(response.status, HealthStatus::Degraded);
}
#[test]
fn test_response_meta() {
let meta = ResponseMeta::new()
.with_request_id("req-123".to_string())
.with_extra("key".to_string(), serde_json::json!("value"));
assert_eq!(meta.request_id, Some("req-123".to_string()));
assert!(meta.extra.contains_key("key"));
}
}