pub mod html;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResponseType {
Json,
Text,
Html,
Xml,
Javascript,
File,
}
impl Default for ResponseType {
fn default() -> Self {
Self::Json
}
}
pub trait TypedServiceResult: Send + Sync {
fn message(&self) -> &str;
fn code(&self) -> u16;
fn response_type(&self) -> ResponseType;
fn serialize(&self) -> Result<String, serde_json::Error>;
fn data_json(&self) -> Option<Value> {
None
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ServiceResult<T: Serialize> {
pub status: String,
pub message: String,
pub data: Option<T>,
#[serde(skip)]
pub code: u16,
#[serde(skip)]
pub response_type: ResponseType,
#[serde(skip)]
naked: bool,
}
impl<T: Serialize> ServiceResult<T> {
pub fn new(
status: impl Into<String>,
message: impl Into<String>,
data: Option<T>,
code: u16,
) -> Self {
Self {
status: status.into(),
message: message.into(),
data,
code,
response_type: ResponseType::Json,
naked: false,
}
}
pub fn ok(message: impl Into<String>, data: T) -> Self {
Self::new("success", message, Some(data), 200)
}
pub fn ok_empty(message: impl Into<String>) -> Self
where
T: Default,
{
Self::new("success", message, None, 200)
}
pub fn stripped(mut self) -> Self {
self.naked = true;
self
}
pub fn with_response_type(mut self, rt: ResponseType) -> Self {
self.response_type = rt;
self
}
pub fn with_code(mut self, code: u16) -> Self {
self.code = code;
self
}
pub fn boxed(self) -> Box<dyn TypedServiceResult>
where
T: Send + Sync + 'static,
{
Box::new(self)
}
pub fn serialize_inherent(&self) -> Result<String, serde_json::Error> {
if self.naked {
return serde_json::to_string(&self.data);
}
let wrapper = serde_json::json!({ "status": self.status, "message": self.message, "data": self.data });
serde_json::to_string(&wrapper)
}
}
impl<T: Serialize + Send + Sync> TypedServiceResult for ServiceResult<T> {
fn message(&self) -> &str {
&self.message
}
fn code(&self) -> u16 {
self.code
}
fn response_type(&self) -> ResponseType {
self.response_type
}
fn serialize(&self) -> Result<String, serde_json::Error> {
self.serialize_inherent()
}
fn data_json(&self) -> Option<Value> {
self.data
.as_ref()
.and_then(|d| serde_json::to_value(d).ok())
}
}
pub struct GenericTypedResult {
pub message: String,
pub data: Option<Value>,
pub code: u16,
pub response_type: ResponseType,
}
impl TypedServiceResult for GenericTypedResult {
fn message(&self) -> &str {
&self.message
}
fn code(&self) -> u16 {
self.code
}
fn response_type(&self) -> ResponseType {
self.response_type
}
fn serialize(&self) -> Result<String, serde_json::Error> {
match &self.data {
Some(v) => serde_json::to_string(v),
None => Ok("null".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ErrorResult {
pub message: String,
pub data: Option<Value>,
pub code: u16,
}
impl ErrorResult {
pub fn new(message: impl Into<String>, data: Option<Value>, code: u16) -> Self {
Self {
message: message.into(),
data,
code,
}
}
pub fn forbidden(msg: impl Into<String>) -> Self {
Self::new(msg, None, 403)
}
pub fn service_unavailable(msg: impl Into<String>) -> Self {
Self::new(msg, None, 503)
}
pub fn bad_request(msg: impl Into<String>) -> Self {
Self::new(msg, None, 400)
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self::new(msg, None, 404)
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::new(msg, None, 500)
}
pub fn from_error<E: std::fmt::Display + ?Sized>(err: &E, code: u16) -> Self {
Self::new(err.to_string(), None, code)
}
pub fn of(err: &anyhow::Error) -> Self {
let msg = err.to_string();
let lower = msg.to_lowercase();
if lower.contains("validation") || lower.contains("invalid") || lower.contains("not found")
{
Self::new(msg, None, 400)
} else {
Self::new(msg, None, 500)
}
}
pub fn to_service_result(&self) -> ServiceResult<Value> {
ServiceResult::new("error", self.message.clone(), self.data.clone(), self.code)
}
pub fn code(&self) -> u16 {
self.code
}
}
impl std::fmt::Display for ErrorResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error {}: {}", self.code, self.message)
}
}
impl std::error::Error for ErrorResult {}
impl From<sea_orm::DbErr> for ErrorResult {
fn from(err: sea_orm::DbErr) -> Self {
match err {
sea_orm::DbErr::RecordNotFound(_) => Self::not_found("Could not find resource"),
sea_orm::DbErr::Custom(e) => Self::new(e, None, 503),
sea_orm::DbErr::RecordNotInserted => Self::service_unavailable("Record not inserted"),
sea_orm::DbErr::RbacError(_) => Self::forbidden("Access denied"),
sea_orm::DbErr::AccessDenied {
permission,
resource,
} => Self::forbidden(format!("Access denied: {} on {}", permission, resource)),
others => {
tracing::error!(target: "framework", "Database error: {:?}", others);
Self::internal("An unknown error has occurred. Please try again later")
},
}
}
}
impl TypedServiceResult for ErrorResult {
fn message(&self) -> &str {
&self.message
}
fn code(&self) -> u16 {
self.code
}
fn response_type(&self) -> ResponseType {
ResponseType::Json
}
fn serialize(&self) -> Result<String, serde_json::Error> {
self.to_service_result().serialize_inherent()
}
}
pub fn build_response(
result: &dyn TypedServiceResult,
ctx: Arc<crate::logging::CorrelationContext>,
) -> http::Response<String> {
let body = result
.serialize()
.unwrap_or_else(|_| result.message().to_string());
let mut builder = http::Response::builder()
.status(result.code())
.header("X-Request-ID", ctx.request_id())
.header("X-Correlation-ID", ctx.correlation_id())
.header("X-Correlation-Flow", ctx.flow().to_string());
let content_type = match result.response_type() {
ResponseType::Json => "application/json",
ResponseType::Html => "text/html",
ResponseType::Xml => "application/xml",
ResponseType::Javascript => "application/javascript",
ResponseType::File => "application/octet-stream",
ResponseType::Text => "text/plain",
};
builder = builder.header("Content-Type", content_type);
if result.response_type() == ResponseType::File {
builder = builder.header(
"Content-Disposition",
format!(
"attachment; filename=\"{}\"",
body.rsplit('/').next().unwrap_or("file")
),
);
}
builder.body(body).unwrap()
}
pub fn build_response_service<T: Serialize + Send + Sync>(
result: &ServiceResult<T>,
ctx: Arc<crate::logging::CorrelationContext>,
) -> http::Response<String> {
build_response(result as &dyn TypedServiceResult, ctx)
}
pub fn error_response(
err: &ErrorResult,
ctx: Arc<crate::logging::CorrelationContext>,
) -> http::Response<String> {
let sr = err.to_service_result();
build_response(&sr, ctx)
}