use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuccessResponse<T> {
pub success: bool,
pub data: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<serde_json::Value>,
}
impl<T> SuccessResponse<T> {
pub fn new(data: T) -> Self {
Self {
success: true,
data,
meta: None,
}
}
pub fn with_meta(mut self, meta: serde_json::Value) -> Self {
self.meta = Some(meta);
self
}
}
impl<T> IntoResponse for SuccessResponse<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
Json(self).into_response()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResponse<T> {
pub success: bool,
pub data: Vec<T>,
pub pagination: PaginationMeta,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationMeta {
pub page: u64,
pub per_page: u64,
pub total: u64,
pub total_pages: u64,
pub has_next: bool,
pub has_prev: bool,
}
impl PaginationMeta {
pub fn new(page: u64, per_page: u64, total: u64) -> Self {
let total_pages = (total + per_page - 1) / per_page.max(1);
Self {
page,
per_page,
total,
total_pages,
has_next: page < total_pages,
has_prev: page > 1,
}
}
}
impl<T> PaginatedResponse<T> {
pub fn new(data: Vec<T>, page: u64, per_page: u64, total: u64) -> Self {
Self {
success: true,
data,
pagination: PaginationMeta::new(page, per_page, total),
}
}
}
impl<T> IntoResponse for PaginatedResponse<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
Json(self).into_response()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmptyResponse {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl EmptyResponse {
pub fn new() -> Self {
Self {
success: true,
message: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}
impl Default for EmptyResponse {
fn default() -> Self {
Self::new()
}
}
impl IntoResponse for EmptyResponse {
fn into_response(self) -> Response {
Json(self).into_response()
}
}
pub struct CreatedResponse<T> {
pub data: T,
pub location: Option<String>,
}
impl<T> CreatedResponse<T> {
pub fn new(data: T) -> Self {
Self {
data,
location: None,
}
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
}
impl<T> IntoResponse for CreatedResponse<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
let mut response = (StatusCode::CREATED, Json(SuccessResponse::new(self.data))).into_response();
if let Some(location) = self.location
&& let Ok(header_value) = location.parse() {
response.headers_mut().insert("Location", header_value);
}
response
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pagination_meta() {
let meta = PaginationMeta::new(2, 10, 45);
assert_eq!(meta.page, 2);
assert_eq!(meta.per_page, 10);
assert_eq!(meta.total, 45);
assert_eq!(meta.total_pages, 5);
assert!(meta.has_next);
assert!(meta.has_prev);
let first_page = PaginationMeta::new(1, 10, 45);
assert!(!first_page.has_prev);
assert!(first_page.has_next);
let last_page = PaginationMeta::new(5, 10, 45);
assert!(last_page.has_prev);
assert!(!last_page.has_next);
}
#[test]
fn test_empty_response() {
let resp = EmptyResponse::new();
assert!(resp.success);
assert!(resp.message.is_none());
let resp = EmptyResponse::new().with_message("Resource deleted");
assert_eq!(resp.message, Some("Resource deleted".to_string()));
}
}