//! Standard API response types. use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; /// Standard API response wrapper. #[derive(Debug, Serialize, Deserialize)] pub struct ApiResponse { /// Whether the request succeeded pub success: bool, /// Response data (present on success) #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, /// Response metadata #[serde(skip_serializing_if = "Option::is_none")] pub meta: Option, } /// Response metadata. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponseMeta { /// Unique request ID for tracing pub request_id: String, /// Response timestamp pub timestamp: DateTime, /// Request processing time in milliseconds #[serde(skip_serializing_if = "Option::is_none")] pub latency_ms: Option, /// Pagination info (if applicable) #[serde(skip_serializing_if = "Option::is_none")] pub pagination: Option, } /// Pagination metadata. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginationMeta { /// Total number of items pub total: u64, /// Current page (1-indexed) pub page: u32, /// Items per page pub per_page: u32, /// Total number of pages pub total_pages: u32, /// Whether there's a next page pub has_next: bool, /// Whether there's a previous page pub has_prev: bool, } impl ApiResponse { /// Create a successful response with data. pub fn success(data: T) -> Self { Self { success: true, data: Some(data), meta: Some(ResponseMeta { request_id: uuid::Uuid::new_v4().to_string(), timestamp: Utc::now(), latency_ms: None, pagination: None, }), } } /// Create a successful response with data and pagination. pub fn success_paginated(data: T, pagination: PaginationMeta) -> Self { Self { success: true, data: Some(data), meta: Some(ResponseMeta { request_id: uuid::Uuid::new_v4().to_string(), timestamp: Utc::now(), latency_ms: None, pagination: Some(pagination), }), } } /// Set the request ID. pub fn with_request_id(mut self, request_id: String) -> Self { if let Some(ref mut meta) = self.meta { meta.request_id = request_id; } self } /// Set the latency. pub fn with_latency(mut self, latency_ms: u64) -> Self { if let Some(ref mut meta) = self.meta { meta.latency_ms = Some(latency_ms); } self } } impl IntoResponse for ApiResponse { fn into_response(self) -> Response { (StatusCode::OK, Json(self)).into_response() } } /// Response for created resources (201). pub struct Created(pub ApiResponse); impl IntoResponse for Created { fn into_response(self) -> Response { (StatusCode::CREATED, Json(self.0)).into_response() } } /// Response for accepted requests (202). pub struct Accepted(pub ApiResponse); impl IntoResponse for Accepted { fn into_response(self) -> Response { (StatusCode::ACCEPTED, Json(self.0)).into_response() } } /// Response for no content (204). pub struct NoContent; impl IntoResponse for NoContent { fn into_response(self) -> Response { StatusCode::NO_CONTENT.into_response() } } /// Empty success response. #[derive(Debug, Serialize, Deserialize)] pub struct EmptyResponse; impl ApiResponse { /// Create an empty success response. pub fn empty() -> Self { Self { success: true, data: None, meta: Some(ResponseMeta { request_id: uuid::Uuid::new_v4().to_string(), timestamp: Utc::now(), latency_ms: None, pagination: None, }), } } } /// Pagination query parameters. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginationParams { /// Page number (1-indexed, default: 1) #[serde(default = "default_page")] pub page: u32, /// Items per page (default: 20, max: 100) #[serde(default = "default_per_page")] pub per_page: u32, } fn default_page() -> u32 { 1 } fn default_per_page() -> u32 { 20 } impl Default for PaginationParams { fn default() -> Self { Self { page: 1, per_page: 20, } } } impl PaginationParams { /// Get the offset for database queries. pub fn offset(&self) -> u64 { ((self.page.saturating_sub(1)) * self.per_page) as u64 } /// Get the limit for database queries. pub fn limit(&self) -> u32 { self.per_page.min(100) } /// Create pagination metadata from results. pub fn to_meta(&self, total: u64) -> PaginationMeta { let total_pages = ((total as f64) / (self.per_page as f64)).ceil() as u32; PaginationMeta { total, page: self.page, per_page: self.per_page, total_pages, has_next: self.page < total_pages, has_prev: self.page > 1, } } } /// Sorting parameters. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SortParams { /// Field to sort by pub sort_by: Option, /// Sort direction (asc or desc) #[serde(default = "default_sort_order")] pub sort_order: SortOrder, } fn default_sort_order() -> SortOrder { SortOrder::Desc } /// Sort direction. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum SortOrder { Asc, Desc, } impl Default for SortOrder { fn default() -> Self { Self::Desc } } /// Health check response. #[derive(Debug, Serialize, Deserialize)] pub struct HealthResponse { /// Overall health status pub status: HealthStatus, /// Service version pub version: String, /// Uptime in seconds pub uptime_seconds: u64, /// Individual service health #[serde(skip_serializing_if = "Option::is_none")] pub services: Option>, } /// Health status enum. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum HealthStatus { Healthy, Degraded, Unhealthy, } /// Individual service health. #[derive(Debug, Serialize, Deserialize)] pub struct ServiceHealth { /// Service name pub name: String, /// Health status pub status: HealthStatus, /// Optional message #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, /// Response time in milliseconds #[serde(skip_serializing_if = "Option::is_none")] pub latency_ms: Option, }