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;
#[derive(Serialize)]
pub struct ApiSuccessResponse<T: Serialize> {
pub status: &'static str,
pub message: String,
pub data: T,
}
#[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,
})
}
pub fn api_success<T: Serialize>(message: impl Into<String>, data: T) -> HttpResponse {
HttpResponse::Ok().json(ApiSuccessResponse {
status: "success",
message: message.into(),
data,
})
}
pub fn api_ok<T: Serialize>(data: T) -> HttpResponse {
HttpResponse::Ok().json(data)
}
pub fn api_created<T: Serialize>(message: impl Into<String>, data: T) -> HttpResponse {
HttpResponse::Created().json(ApiSuccessResponse {
status: "success",
message: message.into(),
data,
})
}
pub fn api_accepted<T: Serialize>(message: impl Into<String>, data: T) -> HttpResponse {
HttpResponse::Accepted().json(ApiSuccessResponse {
status: "success",
message: message.into(),
data,
})
}
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)
}
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"),
)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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,
)
}
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)
}
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)
}
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)
}
pub fn api_success_value(message: impl Into<String>, data: Value) -> HttpResponse {
HttpResponse::Ok().json(json!({
"status": "success",
"message": message.into(),
"data": data
}))
}
pub fn api_error(message: impl Into<String>) -> HttpResponse {
let msg: String = message.into();
internal_error(&msg, &msg)
}
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");
}
}