athena_rs 3.26.2

Hyper performant polyglot Database driver
Documentation
//! Shared API response builder for consistent JSON envelope formatting.
//!
//! ## API response contract
//!
//! Every endpoint in the Athena API **must** document and support at least:
//! - **One 2xx response** (e.g. `200 OK`, `201 Created`, `204 No Content`) for success.
//! - **One 5xx response** (e.g. `500 Internal Server Error`, `503 Service Unavailable`) for server-side failures.
//!
//! **Optional:** One or more 3xx responses (e.g. `308 Permanent Redirect`) where redirects are used.
//!
//! Handlers should use the helpers below so that success paths return 2xx and
//! unexpected or backend failures return 5xx via [`internal_error`] or [`service_unavailable`].
//! The OpenAPI spec reflects this contract so every operation lists at least one 2xx and one 5xx.
//!
//! ## Envelope structure
//!
//! Success and error responses use a uniform JSON envelope:
//!
//! ```json
//! {
//!   "status": "success" | "error",
//!   "code": "...",         // optional on error
//!   "message": "...",
//!   "data": { ... },       // present on success
//!   "error": "..."         // present on error
//! }
//! ```
//!
//! This module provides typed builders that set the correct HTTP status codes
//! and produce the envelope automatically.

use actix_web::HttpResponse;
use actix_web::http::StatusCode;
use serde::Serialize;
use serde_json::{Value, json};

use crate::api::headers::response_headers::set_response_trace_id;
use crate::error::ProcessedError;
use crate::error::catalog::contract_fields_for_code;

/// Standard envelope for successful responses.
#[derive(Serialize)]
pub struct ApiSuccessResponse<T: Serialize> {
    pub status: &'static str,
    pub message: String,
    pub data: T,
}

/// Standard envelope for error responses.
#[derive(Serialize)]
pub struct ApiErrorResponse {
    pub status: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_number: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub docs_url: Option<String>,
    pub message: String,
    pub error: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<Value>,
}

fn build_error_response(
    status: StatusCode,
    message: impl Into<String>,
    error: impl Into<String>,
    code: Option<String>,
    data: Option<Value>,
) -> HttpResponse {
    let contract_fields = contract_fields_for_code(code.as_deref());
    HttpResponse::build(status).json(ApiErrorResponse {
        status: "error",
        code: contract_fields.code,
        error_number: contract_fields.error_number,
        docs_url: contract_fields.docs_url,
        message: message.into(),
        error: error.into(),
        data,
    })
}

/// Build a `200 OK` response with the standard success envelope.
///
/// # Example
/// ```ignore
/// api_success("Fetched records", json!({ "rows": rows }))
/// ```
pub fn api_success<T: Serialize>(message: impl Into<String>, data: T) -> HttpResponse {
    HttpResponse::Ok().json(ApiSuccessResponse {
        status: "success",
        message: message.into(),
        data,
    })
}

/// Build a `200 OK` response returning raw data (for backwards compatibility
/// where the envelope is not expected).
pub fn api_ok<T: Serialize>(data: T) -> HttpResponse {
    HttpResponse::Ok().json(data)
}

/// Build a `201 Created` response with the standard success envelope.
pub fn api_created<T: Serialize>(message: impl Into<String>, data: T) -> HttpResponse {
    HttpResponse::Created().json(ApiSuccessResponse {
        status: "success",
        message: message.into(),
        data,
    })
}

/// Build a `202 Accepted` response with the standard success envelope.
pub fn api_accepted<T: Serialize>(message: impl Into<String>, data: T) -> HttpResponse {
    HttpResponse::Accepted().json(ApiSuccessResponse {
        status: "success",
        message: message.into(),
        data,
    })
}

/// Build a `400 Bad Request` response with the standard error envelope.
pub fn bad_request(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::BAD_REQUEST, message, error, None, None)
}

pub fn bad_request_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::BAD_REQUEST, message, error, code, None)
}

/// Build a `400 Bad Request` response for a missing Postgres client registration.
pub fn postgres_client_not_configured(client_name: &str) -> HttpResponse {
    bad_request(
        format!("Client '{client_name}' is not available in the registry"),
        format!("Postgres client '{client_name}' is not configured"),
    )
}

/// Build a `401 Unauthorized` response.
pub fn unauthorized(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::UNAUTHORIZED, message, error, None, None)
}

pub fn unauthorized_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::UNAUTHORIZED, message, error, code, None)
}

/// Build a `403 Forbidden` response.
pub fn forbidden(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::FORBIDDEN, message, error, None, None)
}

pub fn forbidden_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::FORBIDDEN, message, error, code, None)
}

/// Build a `404 Not Found` response.
pub fn not_found(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::NOT_FOUND, message, error, None, None)
}

pub fn not_found_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::NOT_FOUND, message, error, code, None)
}

/// Build a `409 Conflict` response.
pub fn conflict(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::CONFLICT, message, error, None, None)
}

pub fn conflict_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::CONFLICT, message, error, code, None)
}

/// Build a `429 Too Many Requests` response.
pub fn too_many_requests(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::TOO_MANY_REQUESTS, message, error, None, None)
}

pub fn too_many_requests_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::TOO_MANY_REQUESTS, message, error, code, None)
}

/// Build a `500 Internal Server Error` response.
pub fn internal_error(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(
        StatusCode::INTERNAL_SERVER_ERROR,
        message,
        error,
        None,
        None,
    )
}

pub fn internal_error_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(
        StatusCode::INTERNAL_SERVER_ERROR,
        message,
        error,
        code,
        None,
    )
}

/// Build a `503 Service Unavailable` response.
pub fn service_unavailable(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::SERVICE_UNAVAILABLE, message, error, None, None)
}

pub fn service_unavailable_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::SERVICE_UNAVAILABLE, message, error, code, None)
}

/// Build a `502 Bad Gateway` response (e.g. upstream connection failure).
pub fn bad_gateway(message: impl Into<String>, error: impl Into<String>) -> HttpResponse {
    build_error_response(StatusCode::BAD_GATEWAY, message, error, None, None)
}

pub fn bad_gateway_with_code(
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
) -> HttpResponse {
    error_response_with_code(StatusCode::BAD_GATEWAY, message, error, code, None)
}

/// Build an error response with a stable Athena error code and optional metadata payload.
pub fn error_response_with_code(
    status: StatusCode,
    message: impl Into<String>,
    error: impl Into<String>,
    code: impl Into<String>,
    data: Option<Value>,
) -> HttpResponse {
    build_error_response(status, message, error, Some(code.into()), data)
}

/// Build a success response wrapping a `serde_json::Value`.
pub fn api_success_value(message: impl Into<String>, data: Value) -> HttpResponse {
    HttpResponse::Ok().json(json!({
        "status": "success",
        "message": message.into(),
        "data": data
    }))
}

/// Build an error response from a plain string (shorthand).
pub fn api_error(message: impl Into<String>) -> HttpResponse {
    let msg: String = message.into();
    internal_error(&msg, &msg)
}

/// Build an error response from a ProcessedError with proper status code and formatting.
///
/// This function converts a `ProcessedError` (which contains sanitized error information,
/// user-friendly messages, and safe metadata) into an HTTP response with the appropriate
/// status code.
///
/// # Example
/// ```ignore
/// use athena_rs::error::sqlx_parser::process_sqlx_error;
///
/// let processed_error = process_sqlx_error(&sqlx_err);
/// return processed_error(processed_error);
/// ```
pub fn processed_error(error: ProcessedError) -> HttpResponse {
    let status: StatusCode = error.status_code;
    let json: Value = error.to_json();
    let mut response = HttpResponse::build(status).json(json);
    set_response_trace_id(response.headers_mut(), &error.trace_id);
    response
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::body::to_bytes;

    #[actix_web::test]
    async fn error_response_with_code_adds_error_number_and_docs_url() {
        let response = error_response_with_code(
            StatusCode::UNAUTHORIZED,
            "Webhook secret rejected",
            "secret mismatch",
            "BILLING_WEBHOOK_SECRET_INVALID",
            None,
        );

        let body = to_bytes(response.into_body())
            .await
            .expect("body should serialize");
        let json: Value = serde_json::from_slice(&body).expect("error response should be JSON");

        assert_eq!(json["code"], "BILLING_WEBHOOK_SECRET_INVALID");
        assert_eq!(json["error_number"], 4000);
        assert_eq!(json["docs_url"], "https://docs.athena-cluster.com/4000");
    }

    #[actix_web::test]
    async fn not_found_with_code_adds_error_registry_fields() {
        let response = not_found_with_code(
            "Chat resource not found",
            "Room was not found",
            "CHAT_NOT_FOUND",
        );

        let body = to_bytes(response.into_body())
            .await
            .expect("body should serialize");
        let json: Value = serde_json::from_slice(&body).expect("error response should be JSON");

        assert_eq!(json["code"], "CHAT_NOT_FOUND");
        assert_eq!(json["error_number"], 5002);
        assert_eq!(json["docs_url"], "https://docs.athena-cluster.com/5002");
    }
}