use bytes::Bytes;
use chrono::{DateTime, Utc};
use http_body_util::Full;
use hyper::header::{self, HeaderName, HeaderValue};
use hyper::{Response, StatusCode};
use serde::Serialize;
pub type ResponseBody = Full<Bytes>;
#[derive(Debug, Clone, Serialize)]
pub struct ApiError {
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl ApiError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: Some(message.into()),
}
}
pub fn with_code(code: impl Into<String>) -> Self {
Self {
code: code.into(),
message: None,
}
}
pub fn with_optional_message(code: impl Into<String>, message: Option<String>) -> Self {
Self {
code: code.into(),
message,
}
}
pub fn with_message(code: impl Into<String>, message: Option<String>) -> Self {
Self::with_optional_message(code, message)
}
}
#[derive(Serialize)]
struct Envelope<T: Serialize> {
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
errors: Option<Vec<ApiError>>,
meta: Meta,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Meta {
timestamp: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
correlation_id: Option<String>,
}
pub fn success<T>(status: StatusCode, data: T) -> Response<ResponseBody>
where
T: Serialize,
{
build(status, true, Some(data), None, None, None)
}
pub fn failure(status: StatusCode, errors: Vec<ApiError>) -> Response<ResponseBody> {
build::<()>(status, false, None, None, Some(errors), None)
}
pub fn not_found(path: String) -> Response<ResponseBody> {
#[derive(Serialize)]
struct NotFoundData {
path: String,
}
let payload = NotFoundData { path };
let errors = vec![ApiError::new(
"NOT_FOUND",
"The requested resource was not found",
)];
build(
StatusCode::NOT_FOUND,
false,
Some(payload),
Some("Resource not found".to_owned()),
Some(errors),
None,
)
}
pub fn empty(status: StatusCode) -> Response<ResponseBody> {
let mut response = Response::new(Full::new(Bytes::new()));
*response.status_mut() = status;
response
}
pub fn with_correlation_id<T>(
status: StatusCode,
success: bool,
data: Option<T>,
message: Option<String>,
errors: Option<Vec<ApiError>>,
correlation_id: Option<String>,
) -> Response<ResponseBody>
where
T: Serialize,
{
build(status, success, data, message, errors, correlation_id)
}
fn build<T>(
status: StatusCode,
success: bool,
data: Option<T>,
message: Option<String>,
errors: Option<Vec<ApiError>>,
correlation_id: Option<String>,
) -> Response<ResponseBody>
where
T: Serialize,
{
let header_correlation_id = correlation_id.clone();
let envelope = Envelope {
success,
data,
message,
errors,
meta: Meta {
timestamp: Utc::now(),
correlation_id,
},
};
let body = match serde_json::to_vec(&envelope) {
Ok(bytes) => bytes,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::error!(error = ?err, "failed to serialize response envelope");
#[cfg(not(feature = "tracing"))]
let _ = &err;
return fallback_text_response(StatusCode::INTERNAL_SERVER_ERROR);
}
};
let mut builder = Response::builder().status(status);
builder = builder.header(header::CONTENT_TYPE, "application/json");
if let Some(id) = header_correlation_id.as_ref() {
let header_value = match HeaderValue::from_str(id) {
Ok(value) => value,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::error!(error = ?err, "invalid x-request-id header value");
#[cfg(not(feature = "tracing"))]
let _ = &err;
return fallback_text_response(StatusCode::INTERNAL_SERVER_ERROR);
}
};
builder = builder.header(HeaderName::from_static("x-request-id"), header_value);
}
match builder.body(Full::from(Bytes::from(body))) {
Ok(response) => response,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::error!(error = ?err, "failed to build HTTP response");
#[cfg(not(feature = "tracing"))]
let _ = &err;
fallback_text_response(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
fn fallback_text_response(status: StatusCode) -> Response<ResponseBody> {
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(Full::from(Bytes::from_static(b"internal server error")))
.expect("static response")
}