use std::collections::HashMap;
use axum::{
http::{HeaderMap, HeaderName, HeaderValue, StatusCode},
response::{Json, Response, IntoResponse},
body::{Body, Bytes},
};
use serde::Serialize;
use crate::error::{HttpError, HttpResult};
#[derive(Debug)]
pub struct ElifResponse {
status: StatusCode,
headers: HeaderMap,
body: ResponseBody,
}
#[derive(Debug)]
pub enum ResponseBody {
Empty,
Text(String),
Bytes(Bytes),
Json(serde_json::Value),
}
impl ElifResponse {
pub fn new() -> Self {
Self {
status: StatusCode::OK,
headers: HeaderMap::new(),
body: ResponseBody::Empty,
}
}
pub fn with_status(status: StatusCode) -> Self {
Self {
status,
headers: HeaderMap::new(),
body: ResponseBody::Empty,
}
}
pub fn status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
pub fn header<K, V>(mut self, key: K, value: V) -> HttpResult<Self>
where
K: TryInto<HeaderName>,
K::Error: std::fmt::Display,
V: TryInto<HeaderValue>,
V::Error: std::fmt::Display,
{
let header_name = key.try_into()
.map_err(|e| HttpError::internal(format!("Invalid header name: {}", e)))?;
let header_value = value.try_into()
.map_err(|e| HttpError::internal(format!("Invalid header value: {}", e)))?;
self.headers.insert(header_name, header_value);
Ok(self)
}
pub fn content_type(self, content_type: &str) -> HttpResult<Self> {
self.header("content-type", content_type)
}
pub fn text<S: Into<String>>(mut self, text: S) -> Self {
self.body = ResponseBody::Text(text.into());
self
}
pub fn bytes(mut self, bytes: Bytes) -> Self {
self.body = ResponseBody::Bytes(bytes);
self
}
pub fn json<T: Serialize>(mut self, data: &T) -> HttpResult<Self> {
let json_value = serde_json::to_value(data)
.map_err(|e| HttpError::internal(format!("JSON serialization failed: {}", e)))?;
self.body = ResponseBody::Json(json_value);
Ok(self)
}
pub fn json_value(mut self, value: serde_json::Value) -> Self {
self.body = ResponseBody::Json(value);
self
}
pub fn build(mut self) -> HttpResult<Response<Body>> {
if !self.headers.contains_key("content-type") {
match &self.body {
ResponseBody::Json(_) => {
self = self.content_type("application/json")?;
}
ResponseBody::Text(_) => {
self = self.content_type("text/plain; charset=utf-8")?;
}
_ => {}
}
}
let body = match self.body {
ResponseBody::Empty => Body::empty(),
ResponseBody::Text(text) => Body::from(text),
ResponseBody::Bytes(bytes) => Body::from(bytes),
ResponseBody::Json(value) => {
let json_string = serde_json::to_string(&value)
.map_err(|e| HttpError::internal(format!("JSON serialization failed: {}", e)))?;
Body::from(json_string)
}
};
let mut response = Response::builder()
.status(self.status);
for (key, value) in self.headers.iter() {
response = response.header(key, value);
}
response.body(body)
.map_err(|e| HttpError::internal(format!("Failed to build response: {}", e)))
}
}
impl Default for ElifResponse {
fn default() -> Self {
Self::new()
}
}
impl ElifResponse {
pub fn ok() -> Self {
Self::with_status(StatusCode::OK)
}
pub fn created() -> Self {
Self::with_status(StatusCode::CREATED)
}
pub fn no_content() -> Self {
Self::with_status(StatusCode::NO_CONTENT)
}
pub fn bad_request() -> Self {
Self::with_status(StatusCode::BAD_REQUEST)
}
pub fn unauthorized() -> Self {
Self::with_status(StatusCode::UNAUTHORIZED)
}
pub fn forbidden() -> Self {
Self::with_status(StatusCode::FORBIDDEN)
}
pub fn not_found() -> Self {
Self::with_status(StatusCode::NOT_FOUND)
}
pub fn unprocessable_entity() -> Self {
Self::with_status(StatusCode::UNPROCESSABLE_ENTITY)
}
pub fn internal_server_error() -> Self {
Self::with_status(StatusCode::INTERNAL_SERVER_ERROR)
}
pub fn json_ok<T: Serialize>(data: &T) -> HttpResult<Response<Body>> {
Self::ok().json(data)?.build()
}
pub fn json_error(status: StatusCode, message: &str) -> HttpResult<Response<Body>> {
let error_data = serde_json::json!({
"error": {
"code": status.as_u16(),
"message": message
}
});
Self::with_status(status)
.json_value(error_data)
.build()
}
pub fn validation_error<T: Serialize>(errors: &T) -> HttpResult<Response<Body>> {
let error_data = serde_json::json!({
"error": {
"code": 422,
"message": "Validation failed",
"details": errors
}
});
Self::unprocessable_entity()
.json_value(error_data)
.build()
}
}
pub trait IntoElifResponse {
fn into_elif_response(self) -> HttpResult<ElifResponse>;
}
impl IntoElifResponse for String {
fn into_elif_response(self) -> HttpResult<ElifResponse> {
Ok(ElifResponse::ok().text(self))
}
}
impl IntoElifResponse for &str {
fn into_elif_response(self) -> HttpResult<ElifResponse> {
Ok(ElifResponse::ok().text(self))
}
}
impl IntoElifResponse for StatusCode {
fn into_elif_response(self) -> HttpResult<ElifResponse> {
Ok(ElifResponse::with_status(self))
}
}
impl IntoResponse for ElifResponse {
fn into_response(self) -> Response {
match self.build() {
Ok(response) => response,
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("Response build failed: {}", e)).into_response()
}
}
}
}
impl ElifResponse {
pub fn redirect_permanent(location: &str) -> HttpResult<Self> {
Ok(Self::with_status(StatusCode::MOVED_PERMANENTLY)
.header("location", location)?)
}
pub fn redirect_temporary(location: &str) -> HttpResult<Self> {
Ok(Self::with_status(StatusCode::FOUND)
.header("location", location)?)
}
pub fn redirect_see_other(location: &str) -> HttpResult<Self> {
Ok(Self::with_status(StatusCode::SEE_OTHER)
.header("location", location)?)
}
}
impl ElifResponse {
pub fn download(filename: &str, content: Bytes) -> HttpResult<Self> {
let content_disposition = format!("attachment; filename=\"{}\"", filename);
Ok(Self::ok()
.header("content-disposition", content_disposition)?
.header("content-type", "application/octet-stream")?
.bytes(content))
}
pub fn file_inline(filename: &str, content_type: &str, content: Bytes) -> HttpResult<Self> {
let content_disposition = format!("inline; filename=\"{}\"", filename);
Ok(Self::ok()
.header("content-disposition", content_disposition)?
.header("content-type", content_type)?
.bytes(content))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_basic_response_building() {
let response = ElifResponse::ok()
.text("Hello, World!");
assert_eq!(response.status, StatusCode::OK);
match response.body {
ResponseBody::Text(text) => assert_eq!(text, "Hello, World!"),
_ => panic!("Expected text body"),
}
}
#[test]
fn test_json_response() {
let data = json!({
"name": "John Doe",
"age": 30
});
let response = ElifResponse::ok()
.json_value(data.clone());
match response.body {
ResponseBody::Json(value) => assert_eq!(value, data),
_ => panic!("Expected JSON body"),
}
}
#[test]
fn test_status_codes() {
assert_eq!(ElifResponse::created().status, StatusCode::CREATED);
assert_eq!(ElifResponse::not_found().status, StatusCode::NOT_FOUND);
assert_eq!(ElifResponse::internal_server_error().status, StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn test_headers() {
let response = ElifResponse::ok()
.header("x-custom-header", "test-value")
.unwrap();
assert!(response.headers.contains_key("x-custom-header"));
assert_eq!(
response.headers.get("x-custom-header").unwrap(),
&HeaderValue::from_static("test-value")
);
}
#[test]
fn test_redirect_responses() {
let redirect = ElifResponse::redirect_permanent("/new-location").unwrap();
assert_eq!(redirect.status, StatusCode::MOVED_PERMANENTLY);
assert!(redirect.headers.contains_key("location"));
}
}