use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::fmt;
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title = "Unified API response for all endpoints",
example = json!({
"status": "success",
"type": "single",
"dataType": "Contact",
"source": "/api/v1/contacts",
"data": {
"id": "507f1f77bcf86cd799439011",
"name": "John Doe"
}
})
))]
#[serde(rename_all = "camelCase")]
pub struct UnifiedApiResponse {
pub status: ResponseStatus,
#[serde(rename = "type")]
pub response_type: ResponseType,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ApiError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination: Option<PaginationMeta>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<ResponseMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub batch_results: Option<BatchResults>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title = "Response status indicator"))]
#[serde(rename_all = "lowercase")]
pub enum ResponseStatus {
Success,
Error,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title = "Type of response data"))]
#[serde(rename_all = "lowercase")]
pub enum ResponseType {
Single,
List,
Paginated,
Batch,
Empty,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title = "Results of a batch operation"))]
#[serde(rename_all = "camelCase")]
pub struct BatchResults {
pub succeeded: Vec<Value>,
pub failed: Vec<BatchErrorItem>,
pub total: usize,
pub success_count: usize,
pub failure_count: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title = "Error details for a failed batch item"))]
#[serde(rename_all = "camelCase")]
pub struct BatchErrorItem {
#[cfg_attr(feature = "openapi", schema(example = 42))]
pub index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
pub id: Option<String>,
pub item: Value,
pub error: ApiError,
}
impl UnifiedApiResponse {
pub fn single<T: Serialize>(item: T, data_type: &str, source: &str) -> Self {
Self {
status: ResponseStatus::Success,
response_type: ResponseType::Single,
data_type: Some(data_type.to_string()),
source: Some(source.to_string()),
data: Some(serde_json::to_value(item).unwrap()),
error: None,
pagination: None,
metadata: None,
batch_results: None,
}
}
pub fn list<T: Serialize>(items: Vec<T>, data_type: &str, source: &str) -> Self {
Self {
status: ResponseStatus::Success,
response_type: ResponseType::List,
data_type: Some(format!("List<{}>", data_type)),
source: Some(source.to_string()),
data: Some(serde_json::to_value(items).unwrap()),
error: None,
pagination: None,
metadata: None,
batch_results: None,
}
}
pub fn paginated<T: Serialize>(
items: Vec<T>,
pagination: PaginationMeta,
data_type: &str,
source: &str
) -> Self {
Self {
status: ResponseStatus::Success,
response_type: ResponseType::Paginated,
data_type: Some(format!("Paginated<{}>", data_type)),
source: Some(source.to_string()),
data: Some(serde_json::to_value(items).unwrap()),
error: None,
pagination: Some(pagination),
metadata: None,
batch_results: None,
}
}
pub fn error(error: ApiError, source: &str) -> Self {
Self {
status: ResponseStatus::Error,
response_type: ResponseType::Empty,
data_type: None,
source: Some(source.to_string()),
data: None,
error: Some(error),
pagination: None,
metadata: None,
batch_results: None,
}
}
pub fn empty(source: &str) -> Self {
Self {
status: ResponseStatus::Success,
response_type: ResponseType::Empty,
data_type: None,
source: Some(source.to_string()),
data: None,
error: None,
pagination: None,
metadata: None,
batch_results: None,
}
}
pub fn batch<T: Serialize, E: Serialize>(
succeeded: Vec<T>,
failed: Vec<(usize, Option<String>, E, ApiError)>,
data_type: &str,
source: &str
) -> Self {
let succeeded_values: Vec<Value> = succeeded
.into_iter()
.map(|item| serde_json::to_value(item).unwrap())
.collect();
let failed_items: Vec<BatchErrorItem> = failed
.into_iter()
.map(|(index, id, item, error)| BatchErrorItem {
index,
id,
item: serde_json::to_value(item).unwrap(),
error,
})
.collect();
let total = succeeded_values.len() + failed_items.len();
let success_count = succeeded_values.len();
let failure_count = failed_items.len();
Self {
status: if failure_count == 0 {
ResponseStatus::Success
} else {
ResponseStatus::Error
},
response_type: ResponseType::Batch,
data_type: Some(format!("Batch<{}>", data_type)),
source: Some(source.to_string()),
data: None,
error: None,
pagination: None,
metadata: None,
batch_results: Some(BatchResults {
succeeded: succeeded_values,
failed: failed_items,
total,
success_count,
failure_count,
}),
}
}
pub fn with_metadata(mut self, metadata: ResponseMetadata) -> Self {
self.metadata = Some(metadata);
self
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Response returned from synchronization endpoints",
example = json!({
"status": "success",
"message": "Synchronization started successfully"
})
))]
pub struct SyncResponse {
#[cfg_attr(feature = "openapi", schema(example = "success"))]
pub status: String,
#[cfg_attr(feature = "openapi", schema(example = "Contact sync started in background"))]
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Response containing data retrieved from cache"
))]
pub struct CacheResponse {
#[cfg_attr(feature = "openapi", schema(example = "cache"))]
pub source: String,
pub data: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = 42))]
pub count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_info: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Error response when cache operation fails"
))]
pub struct CacheErrorResponse {
#[cfg_attr(feature = "openapi", schema(example = "Redis connection timeout"))]
pub error: String,
#[cfg_attr(feature = "openapi", schema(example = "cache"))]
pub source: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Statistics about cached data for an account",
example = json!({
"source": "cache",
"account_id": "507f1f77bcf86cd799439011",
"stats": {
"summary": {
"total_contacts": 150
},
"groups": {
"sales": 50,
"support": 30,
"engineering": 70
}
}
})
))]
pub struct CacheStatsResponse {
pub source: String,
#[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
pub account_id: String,
pub stats: BTreeMap<String, BTreeMap<String, usize>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Pagination parameters for list requests"))]
#[serde(rename_all = "camelCase")]
pub struct PaginationParams {
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(example = 0, minimum = 0))]
pub page: u32,
#[serde(default = "default_page_size")]
#[cfg_attr(feature = "openapi", schema(example = 20, minimum = 1, maximum = 100))]
pub page_size: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Metadata about pagination",
example = json!({
"total": 150,
"page": 0,
"pageSize": 20,
"totalPages": 8,
"hasNext": true,
"hasPrevious": false
})
))]
#[serde(rename_all = "camelCase")]
pub struct PaginationMeta {
pub total: u64,
pub page: u32,
pub page_size: u32,
pub total_pages: u32,
pub has_next: bool,
pub has_previous: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Sort order direction"))]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
#[cfg_attr(feature = "openapi", schema(rename = "asc"))]
Asc,
#[cfg_attr(feature = "openapi", schema(rename = "desc"))]
Desc,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Sorting parameters for list requests"))]
#[serde(rename_all = "camelCase")]
pub struct SortParams {
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "name"))]
pub sort_by: Option<String>,
#[serde(default = "default_sort_order")]
pub sort_order: SortOrder,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Time range filter for queries"))]
#[serde(rename_all = "camelCase")]
pub struct TimeRange {
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime, example = "2024-01-01T00:00:00Z"))]
pub start: Option<chrono::DateTime<chrono::Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime, example = "2024-12-31T23:59:59Z"))]
pub end: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Standard error response",
example = json!({
"code": "VALIDATION_ERROR",
"message": "Invalid input provided",
"timestamp": 1704067200
})
))]
#[serde(rename_all = "camelCase")]
pub struct ApiError {
#[cfg_attr(feature = "openapi", schema(example = "VALIDATION_ERROR"))]
pub code: String,
#[cfg_attr(feature = "openapi", schema(example = "Invalid email format"))]
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<ErrorDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "req_1234567890"))]
pub request_id: Option<String>,
#[cfg_attr(feature = "openapi", schema(value_type = i64, example = 1704067200))]
pub timestamp: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Additional details about an error"))]
#[serde(rename_all = "camelCase")]
pub struct ErrorDetails {
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "email"))]
pub field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "Invalid format"))]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub validation_errors: Vec<ValidationError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Details about a field validation error"))]
#[serde(rename_all = "camelCase")]
pub struct ValidationError {
#[cfg_attr(feature = "openapi", schema(example = "email"))]
pub field: String,
#[cfg_attr(feature = "openapi", schema(example = "format"))]
pub code: String,
#[cfg_attr(feature = "openapi", schema(example = "Email must be a valid email address"))]
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Metadata about the API response"))]
#[serde(rename_all = "camelCase")]
pub struct ResponseMetadata {
#[cfg_attr(feature = "openapi", schema(example = "req_1234567890"))]
pub request_id: String,
#[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime, example = "2024-01-01T00:00:00Z"))]
pub timestamp: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "1.0.0"))]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = 42))]
pub processing_time_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Parameters for search queries"))]
#[serde(rename_all = "camelCase")]
pub struct SearchParams {
#[cfg_attr(feature = "openapi", schema(example = "john doe"))]
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = json!(["name", "email"])))]
pub fields: Option<Vec<String>>,
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(example = false))]
pub fuzzy: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = true))]
pub highlight: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="Dynamic filter parameters",
example = json!({
"status": "active",
"category": "premium"
})
))]
#[serde(rename_all = "camelCase")]
pub struct FilterParams {
#[serde(flatten)]
pub filters: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(
title ="ID wrapper for consistent handling",
value_type = String,
example = "507f1f77bcf86cd799439011"
))]
pub struct Id(pub String);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(title ="Common parameters for list endpoints"))]
#[serde(rename_all = "camelCase")]
pub struct ListRequest {
#[serde(flatten)]
pub pagination: PaginationParams,
#[serde(flatten)]
pub sort: SortParams,
#[serde(skip_serializing_if = "Option::is_none")]
pub search: Option<SearchParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_range: Option<TimeRange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<FilterParams>,
}
fn default_page_size() -> u32 {
20
}
fn default_sort_order() -> SortOrder {
SortOrder::Asc
}
impl Default for PaginationParams {
fn default() -> Self {
Self {
page: 0,
page_size: default_page_size(),
}
}
}
impl PaginationParams {
pub fn new(page: u32, page_size: u32) -> Self {
Self { page, page_size }
}
pub fn offset(&self) -> u64 {
(self.page as u64) * (self.page_size as u64)
}
pub fn limit(&self) -> u64 {
self.page_size as u64
}
}
impl PaginationMeta {
pub fn new(total: u64, page: u32, page_size: u32) -> Self {
let total_pages = ((total as f64) / (page_size as f64)).ceil() as u32;
Self {
total,
page,
page_size,
total_pages,
has_next: page < total_pages.saturating_sub(1),
has_previous: page > 0,
}
}
}
impl Default for SortOrder {
fn default() -> Self {
Self::Asc
}
}
impl fmt::Display for SortOrder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SortOrder::Asc => write!(f, "asc"),
SortOrder::Desc => write!(f, "desc"),
}
}
}
impl Default for SortParams {
fn default() -> Self {
Self {
sort_by: None,
sort_order: default_sort_order(),
}
}
}
impl ApiError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
details: None,
request_id: None,
timestamp: Some(chrono::Utc::now().timestamp()),
}
}
pub fn with_field(mut self, field: impl Into<String>) -> Self {
self.details = Some(ErrorDetails {
field: Some(field.into()),
reason: None,
validation_errors: Vec::new(),
});
self
}
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
self.request_id = Some(request_id.into());
self
}
}
impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for Id {
fn from(s: String) -> Self {
Id(s)
}
}
impl From<&str> for Id {
fn from(s: &str) -> Self {
Id(s.to_string())
}
}
impl TimeRange {
pub fn new(start: Option<chrono::DateTime<chrono::Utc>>, end: Option<chrono::DateTime<chrono::Utc>>) -> Self {
Self { start, end }
}
pub fn is_valid(&self) -> bool {
match (self.start, self.end) {
(Some(start), Some(end)) => start <= end,
_ => true,
}
}
}