use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PaginationRequest {
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default)]
pub offset: usize,
}
fn default_limit() -> usize {
20
}
impl PaginationRequest {
pub fn new(limit: usize, offset: usize) -> Self {
Self {
limit: limit.min(100),
offset,
}
}
pub fn clamped_limit(&self) -> usize {
self.limit.min(100)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationResponse {
pub total: usize,
pub count: usize,
pub offset: usize,
pub limit: usize,
pub has_more: bool,
}
impl PaginationResponse {
pub fn new(total: usize, count: usize, offset: usize, limit: usize) -> Self {
Self {
total,
count,
offset,
limit,
has_more: offset + count < total,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SortDirection {
#[default]
Asc,
Desc,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SortRequest {
pub field: String,
#[serde(default)]
pub direction: SortDirection,
}
impl SortRequest {
pub fn new(field: impl Into<String>, direction: SortDirection) -> Self {
Self {
field: field.into(),
direction,
}
}
pub fn asc(field: impl Into<String>) -> Self {
Self::new(field, SortDirection::Asc)
}
pub fn desc(field: impl Into<String>) -> Self {
Self::new(field, SortDirection::Desc)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TimeRangeFilter {
pub since: Option<DateTime<Utc>>,
pub until: Option<DateTime<Utc>>,
pub as_of: Option<DateTime<Utc>>,
}
impl TimeRangeFilter {
pub fn new() -> Self {
Self::default()
}
pub fn since(mut self, since: DateTime<Utc>) -> Self {
self.since = Some(since);
self
}
pub fn until(mut self, until: DateTime<Utc>) -> Self {
self.until = Some(until);
self
}
pub fn as_of(mut self, as_of: DateTime<Utc>) -> Self {
self.as_of = Some(as_of);
self
}
pub fn has_constraints(&self) -> bool {
self.since.is_some() || self.until.is_some() || self.as_of.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorSeverity {
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
ValidationError,
InvalidInput,
MissingField,
InvalidFormat,
Unauthorized,
InvalidToken,
TokenExpired,
Forbidden,
InsufficientPermissions,
QuotaExceeded,
NotFound,
EntityNotFound,
ResourceNotFound,
Conflict,
DuplicateEntry,
ConcurrencyConflict,
VersionMismatch,
RateLimited,
TooManyRequests,
InternalError,
DatabaseError,
ServiceUnavailable,
}
impl ErrorCode {
pub fn http_status(&self) -> u16 {
match self {
ErrorCode::ValidationError
| ErrorCode::InvalidInput
| ErrorCode::MissingField
| ErrorCode::InvalidFormat => 400,
ErrorCode::Unauthorized | ErrorCode::InvalidToken | ErrorCode::TokenExpired => 401,
ErrorCode::Forbidden
| ErrorCode::InsufficientPermissions
| ErrorCode::QuotaExceeded => 403,
ErrorCode::NotFound | ErrorCode::EntityNotFound | ErrorCode::ResourceNotFound => 404,
ErrorCode::Conflict
| ErrorCode::DuplicateEntry
| ErrorCode::ConcurrencyConflict
| ErrorCode::VersionMismatch => 409,
ErrorCode::RateLimited | ErrorCode::TooManyRequests => 429,
ErrorCode::InternalError | ErrorCode::DatabaseError | ErrorCode::ServiceUnavailable => {
500
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldError {
pub field: String,
pub message: String,
pub code: Option<String>,
}
impl FieldError {
pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
field: field.into(),
message: message.into(),
code: None,
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub code: ErrorCode,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub field_errors: Vec<FieldError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
pub timestamp: DateTime<Utc>,
}
impl ErrorResponse {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
field_errors: Vec::new(),
request_id: None,
timestamp: Utc::now(),
}
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
pub fn with_field_errors(mut self, errors: Vec<FieldError>) -> Self {
self.field_errors = errors;
self
}
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
self.request_id = Some(request_id.into());
self
}
pub fn validation_error(message: impl Into<String>) -> Self {
Self::new(ErrorCode::ValidationError, message)
}
pub fn not_found(entity: impl Into<String>) -> Self {
Self::new(ErrorCode::NotFound, format!("{} not found", entity.into()))
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Conflict, message)
}
pub fn internal_error() -> Self {
Self::new(ErrorCode::InternalError, "An internal error occurred")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuccessResponse<T> {
pub data: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ResponseMeta>,
}
impl<T> SuccessResponse<T> {
pub fn new(data: T) -> Self {
Self { data, meta: None }
}
pub fn with_meta(mut self, meta: ResponseMeta) -> Self {
self.meta = Some(meta);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub processing_time_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecation_warning: Option<String>,
}
impl ResponseMeta {
pub fn new() -> Self {
Self {
processing_time_ms: None,
api_version: None,
deprecation_warning: None,
}
}
pub fn with_processing_time(mut self, ms: u64) -> Self {
self.processing_time_ms = Some(ms);
self
}
}
impl Default for ResponseMeta {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResponse<T> {
pub items: Vec<T>,
pub pagination: PaginationResponse,
}
impl<T> PaginatedResponse<T> {
pub fn new(items: Vec<T>, total: usize, offset: usize, limit: usize) -> Self {
let count = items.len();
Self {
items,
pagination: PaginationResponse::new(total, count, offset, limit),
}
}
pub fn empty() -> Self {
Self {
items: Vec::new(),
pagination: PaginationResponse::new(0, 0, 0, 20),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchItemResult<T> {
pub index: usize,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorResponse>,
}
impl<T> BatchItemResult<T> {
pub fn success(index: usize, data: T) -> Self {
Self {
index,
success: true,
data: Some(data),
error: None,
}
}
pub fn failure(index: usize, error: ErrorResponse) -> Self {
Self {
index,
success: false,
data: None,
error: Some(error),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchResponse<T> {
pub results: Vec<BatchItemResult<T>>,
pub total: usize,
pub successful: usize,
pub failed: usize,
}
impl<T> BatchResponse<T> {
pub fn new(results: Vec<BatchItemResult<T>>) -> Self {
let total = results.len();
let successful = results.iter().filter(|r| r.success).count();
let failed = total - successful;
Self {
results,
total,
successful,
failed,
}
}
pub fn all_successful(&self) -> bool {
self.failed == 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthResponse {
pub status: HealthStatus,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_seconds: Option<u64>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub components: Vec<ComponentHealth>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_time_ms: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pagination_request_clamping() {
let pagination = PaginationRequest::new(200, 0);
assert_eq!(pagination.clamped_limit(), 100);
}
#[test]
fn test_pagination_response_has_more() {
let response = PaginationResponse::new(100, 20, 0, 20);
assert!(response.has_more);
let response = PaginationResponse::new(100, 20, 80, 20);
assert!(!response.has_more);
}
#[test]
fn test_error_code_http_status() {
assert_eq!(ErrorCode::ValidationError.http_status(), 400);
assert_eq!(ErrorCode::Unauthorized.http_status(), 401);
assert_eq!(ErrorCode::Forbidden.http_status(), 403);
assert_eq!(ErrorCode::NotFound.http_status(), 404);
assert_eq!(ErrorCode::Conflict.http_status(), 409);
assert_eq!(ErrorCode::RateLimited.http_status(), 429);
assert_eq!(ErrorCode::InternalError.http_status(), 500);
}
#[test]
fn test_error_response_serialization() {
let error =
ErrorResponse::validation_error("Invalid email format").with_field_errors(vec![
FieldError::new("email", "must be a valid email address"),
]);
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("VALIDATION_ERROR"));
assert!(json.contains("email"));
}
#[test]
fn test_time_range_filter() {
let filter = TimeRangeFilter::new();
assert!(!filter.has_constraints());
let filter = filter.since(Utc::now());
assert!(filter.has_constraints());
}
#[test]
fn test_paginated_response() {
let items = vec!["a", "b", "c"];
let response = PaginatedResponse::new(items, 10, 0, 3);
assert_eq!(response.items.len(), 3);
assert_eq!(response.pagination.total, 10);
assert!(response.pagination.has_more);
}
#[test]
fn test_batch_response() {
let results = vec![
BatchItemResult::success(0, "item1"),
BatchItemResult::failure(1, ErrorResponse::validation_error("Invalid")),
BatchItemResult::success(2, "item3"),
];
let batch = BatchResponse::new(results);
assert_eq!(batch.total, 3);
assert_eq!(batch.successful, 2);
assert_eq!(batch.failed, 1);
assert!(!batch.all_successful());
}
}