rok-core 0.6.1

Core primitives for the rok ecosystem — errors, crypto, i18n, config, DI, and more
Documentation
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::Serialize;
use std::collections::HashMap;

/// Pagination metadata for collection responses.
#[derive(Debug, Clone, Serialize)]
pub struct PaginationMeta {
    pub total: i64,
    pub page: i64,
    pub per_page: i64,
    pub last_page: i64,
}

impl PaginationMeta {
    pub fn new(total: i64, page: i64, per_page: i64) -> Self {
        let last_page = if per_page > 0 {
            (total + per_page - 1) / per_page
        } else {
            1
        };
        Self {
            total,
            page,
            per_page,
            last_page,
        }
    }
}

/// Standard API response envelope for the Rok ecosystem.
///
/// Produces consistent JSON shapes:
/// - Success: `{ "data": ... }`
/// - Paginated: `{ "data": [...], "meta": { "total", "page", "perPage", "lastPage" } }`
/// - Created: `{ "data": ..., "meta": { "message": "Resource created" } }`
/// - Error: `{ "error": { "code", "message", "statusCode" } }`
/// - Validation: `{ "error": { "code": "E_VALIDATION_FAILURE", "message", "statusCode": 422, "details": {...} } }`
/// - No Content: empty body, 204 status
pub struct ApiResponse {
    status: StatusCode,
    body: serde_json::Value,
}

impl ApiResponse {
    /// 200 OK with data envelope.
    pub fn ok<T: Serialize>(data: T) -> Self {
        Self {
            status: StatusCode::OK,
            body: serde_json::json!({ "data": data }),
        }
    }

    /// 201 Created with data envelope.
    pub fn created<T: Serialize>(data: T) -> Self {
        Self {
            status: StatusCode::CREATED,
            body: serde_json::json!({
                "data": data,
                "meta": { "message": "Resource created" }
            }),
        }
    }

    /// 204 No Content — empty body.
    pub fn no_content() -> Self {
        Self {
            status: StatusCode::NO_CONTENT,
            body: serde_json::Value::Null,
        }
    }

    /// 200 OK with paginated data.
    pub fn paginated<T: Serialize>(data: Vec<T>, pagination: PaginationMeta) -> Self {
        Self {
            status: StatusCode::OK,
            body: serde_json::json!({
                "data": data,
                "meta": pagination,
            }),
        }
    }

    /// Error response with custom status, code, and message.
    pub fn error(code: &'static str, message: impl Into<String>, status: u16) -> Self {
        let status_code = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
        Self {
            status: status_code,
            body: serde_json::json!({
                "error": {
                    "code": code,
                    "message": message.into(),
                    "statusCode": status,
                }
            }),
        }
    }

    /// 422 Validation error response.
    pub fn validation(message: impl Into<String>, errors: HashMap<String, Vec<String>>) -> Self {
        Self {
            status: StatusCode::UNPROCESSABLE_ENTITY,
            body: serde_json::json!({
                "error": {
                    "code": "E_VALIDATION_FAILURE",
                    "message": message.into(),
                    "statusCode": 422,
                    "details": errors,
                }
            }),
        }
    }

    /// Convert from a `StatusCode` with no body.
    pub fn from_status(status: StatusCode) -> Self {
        Self {
            status,
            body: serde_json::Value::Null,
        }
    }

    /// Access the response status code.
    pub fn status_code(&self) -> StatusCode {
        self.status
    }

    /// Consume and return the inner JSON value (for testing).
    pub fn into_body(self) -> serde_json::Value {
        self.body
    }
}

impl IntoResponse for ApiResponse {
    fn into_response(self) -> Response {
        if self.body.is_null() {
            self.status.into_response()
        } else {
            (self.status, Json(self.body)).into_response()
        }
    }
}