use http::StatusCode;
use serde::Serialize;
use crate::body::{body_from_bytes, body_from_string, BoxBody};
use crate::response::IntoResponse;
#[derive(Debug, Clone)]
pub struct JsonError {
pub status: StatusCode,
pub message: String,
}
#[derive(Serialize)]
struct JsonErrorBody {
error: JsonErrorInner,
}
#[derive(Serialize)]
struct JsonErrorInner {
status: u16,
message: String,
}
impl JsonError {
pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
JsonError {
status,
message: message.into(),
}
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, message)
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(StatusCode::UNAUTHORIZED, message)
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::new(StatusCode::FORBIDDEN, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(StatusCode::NOT_FOUND, message)
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(StatusCode::CONFLICT, message)
}
pub fn unprocessable(message: impl Into<String>) -> Self {
Self::new(StatusCode::UNPROCESSABLE_ENTITY, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
}
}
impl IntoResponse for JsonError {
fn into_response(self) -> http::Response<BoxBody> {
let body = JsonErrorBody {
error: JsonErrorInner {
status: self.status.as_u16(),
message: self.message,
},
};
match serde_json::to_vec(&body) {
Ok(bytes) => {
let body = body_from_bytes(bytes::Bytes::from(bytes));
let mut res = http::Response::new(body);
*res.status_mut() = self.status;
res.headers_mut().insert(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static("application/json"),
);
res
}
Err(e) => {
let mut res = http::Response::new(body_from_string(format!(
"error serialization failed: {e}"
)));
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
res
}
}
}
}
impl std::fmt::Display for JsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {}: {}",
self.status.as_u16(),
self.status,
self.message
)
}
}
impl std::error::Error for JsonError {}
impl From<(StatusCode, String)> for JsonError {
fn from((status, message): (StatusCode, String)) -> Self {
JsonError { status, message }
}
}
#[cfg(feature = "openapi")]
impl typeway_openapi::ErrorResponses for JsonError {
fn error_responses() -> indexmap::IndexMap<String, typeway_openapi::spec::Response> {
use typeway_openapi::spec::*;
let mut content = indexmap::IndexMap::new();
let mut properties = indexmap::IndexMap::new();
let mut error_props = indexmap::IndexMap::new();
error_props.insert("status".to_string(), Schema::integer());
error_props.insert("message".to_string(), Schema::string());
properties.insert(
"error".to_string(),
Schema {
schema_type: Some("object".into()),
properties: Some(error_props),
..Default::default()
},
);
content.insert(
"application/json".to_string(),
MediaType {
schema: Some(Schema {
schema_type: Some("object".into()),
properties: Some(properties),
description: Some("JSON error response".into()),
..Default::default()
}),
example: None,
},
);
let mut responses = indexmap::IndexMap::new();
responses.insert(
"4XX".to_string(),
Response {
description: "Client error".to_string(),
content: content.clone(),
},
);
responses.insert(
"5XX".to_string(),
Response {
description: "Server error".to_string(),
content,
},
);
responses
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_error_response() {
let err = JsonError::not_found("user not found");
let res = err.into_response();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(
res.headers().get("content-type").unwrap(),
"application/json"
);
}
#[test]
fn json_error_from_tuple() {
let err: JsonError = (StatusCode::BAD_REQUEST, "bad input".to_string()).into();
assert_eq!(err.status, StatusCode::BAD_REQUEST);
assert_eq!(err.message, "bad input");
}
}