rs-zero 0.2.7

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
use axum::{Json, http::StatusCode, response::IntoResponse};
use serde::Serialize;

/// Uniform API response used by rs-zero REST services.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ApiResponse<T>
where
    T: Serialize,
{
    pub success: bool,
    pub err_code: String,
    pub err_message: String,
    pub data: Option<T>,
}

impl<T> ApiResponse<T>
where
    T: Serialize,
{
    /// Creates a successful response.
    pub fn success(data: T) -> Self {
        Self {
            success: true,
            err_code: "0".to_string(),
            err_message: String::new(),
            data: Some(data),
        }
    }

    /// Creates a successful response without data.
    pub fn success_no_data() -> Self {
        Self {
            success: true,
            err_code: "0".to_string(),
            err_message: String::new(),
            data: None,
        }
    }

    /// Creates a failed response.
    pub fn fail(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            success: false,
            err_code: code.into(),
            err_message: message.into(),
            data: None,
        }
    }
}

impl<T> IntoResponse for ApiResponse<T>
where
    T: Serialize,
{
    fn into_response(self) -> axum::response::Response {
        (StatusCode::OK, Json(self)).into_response()
    }
}

/// Uniform paginated response.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct PageResponse<T>
where
    T: Serialize,
{
    pub success: bool,
    pub err_code: String,
    pub err_message: String,
    pub data: Vec<T>,
    pub total: i64,
    pub page_size: i64,
    pub has_more: bool,
    pub current_page: i64,
    pub total_pages: i64,
}

impl<T> PageResponse<T>
where
    T: Serialize,
{
    /// Creates a successful paginated response.
    pub fn success(data: Vec<T>, total: i64, page_size: i64, current_page: i64) -> Self {
        Self {
            success: true,
            err_code: "0".to_string(),
            err_message: String::new(),
            data,
            total,
            page_size,
            has_more: total > page_size * current_page,
            current_page,
            total_pages: if page_size <= 0 {
                0
            } else {
                (total + page_size - 1) / page_size
            },
        }
    }

    /// Creates an empty successful page.
    pub fn empty() -> Self {
        Self {
            success: true,
            err_code: "0".to_string(),
            err_message: String::new(),
            data: Vec::new(),
            total: 0,
            page_size: 0,
            has_more: false,
            current_page: 0,
            total_pages: 0,
        }
    }
}

impl<T> IntoResponse for PageResponse<T>
where
    T: Serialize,
{
    fn into_response(self) -> axum::response::Response {
        (StatusCode::OK, Json(self)).into_response()
    }
}

#[cfg(test)]
mod tests {
    use super::{ApiResponse, PageResponse};

    #[test]
    fn response_success_uses_zero_error_code() {
        let response = ApiResponse::success("ok");

        assert!(response.success);
        assert_eq!(response.err_code, "0");
        assert_eq!(response.data, Some("ok"));
    }

    #[test]
    fn page_response_calculates_total_pages() {
        let response = PageResponse::success(vec![1, 2], 11, 5, 2);

        assert!(response.has_more);
        assert_eq!(response.total_pages, 3);
    }
}