lmrc-http-common 0.3.16

Common HTTP utilities and patterns for LMRC Stack applications
Documentation
//! Standard HTTP response wrappers

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::{Deserialize, Serialize};

/// Standard success response wrapper
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuccessResponse<T> {
    /// Success flag (always true)
    pub success: bool,
    /// Response data
    pub data: T,
    /// Optional metadata
    #[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()
    }
}

/// Paginated response wrapper
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResponse<T> {
    /// Success flag (always true)
    pub success: bool,
    /// Response data items
    pub data: Vec<T>,
    /// Pagination metadata
    pub pagination: PaginationMeta,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationMeta {
    /// Current page number (1-indexed)
    pub page: u64,
    /// Items per page
    pub per_page: u64,
    /// Total number of items
    pub total: u64,
    /// Total number of pages
    pub total_pages: u64,
    /// Whether there is a next page
    pub has_next: bool,
    /// Whether there is a previous page
    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()
    }
}

/// Empty success response (for DELETE, etc.)
#[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()
    }
}

/// Created response (201) with location header
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()));
    }
}