use crate::errors::HttpResult;
use crate::response::{ElifResponse, ElifStatusCode, ResponseBody};
use axum::body::Bytes;
use serde::Serialize;
use tracing;
#[derive(Debug)]
pub struct ResponseBuilder {
status: Option<ElifStatusCode>,
headers: Vec<(String, String)>,
body: Option<ResponseBody>,
}
impl ResponseBuilder {
pub fn new() -> Self {
Self {
status: None,
headers: Vec::new(),
body: None,
}
}
pub fn ok(mut self) -> Self {
self.status = Some(ElifStatusCode::OK);
self
}
pub fn created(mut self) -> Self {
self.status = Some(ElifStatusCode::CREATED);
self
}
pub fn accepted(mut self) -> Self {
self.status = Some(ElifStatusCode::ACCEPTED);
self
}
pub fn no_content(mut self) -> Self {
self.status = Some(ElifStatusCode::NO_CONTENT);
self
}
pub fn bad_request(mut self) -> Self {
self.status = Some(ElifStatusCode::BAD_REQUEST);
self
}
pub fn unauthorized(mut self) -> Self {
self.status = Some(ElifStatusCode::UNAUTHORIZED);
self
}
pub fn forbidden(mut self) -> Self {
self.status = Some(ElifStatusCode::FORBIDDEN);
self
}
pub fn not_found(mut self) -> Self {
self.status = Some(ElifStatusCode::NOT_FOUND);
self
}
pub fn unprocessable_entity(mut self) -> Self {
self.status = Some(ElifStatusCode::UNPROCESSABLE_ENTITY);
self
}
pub fn internal_server_error(mut self) -> Self {
self.status = Some(ElifStatusCode::INTERNAL_SERVER_ERROR);
self
}
pub fn status(mut self, status: ElifStatusCode) -> Self {
self.status = Some(status);
self
}
pub fn json<T: Serialize>(mut self, data: T) -> Self {
match serde_json::to_value(&data) {
Ok(value) => {
self.body = Some(ResponseBody::Json(value));
self.headers
.push(("content-type".to_string(), "application/json".to_string()));
self
}
Err(err) => {
tracing::error!("JSON serialization failed: {}", err);
self.status = Some(ElifStatusCode::INTERNAL_SERVER_ERROR);
self.body = Some(ResponseBody::Text(format!(
"JSON serialization failed: {}",
err
)));
self
}
}
}
pub fn text<S: Into<String>>(mut self, text: S) -> Self {
self.body = Some(ResponseBody::Text(text.into()));
self.headers.push((
"content-type".to_string(),
"text/plain; charset=utf-8".to_string(),
));
self
}
pub fn html<S: Into<String>>(mut self, html: S) -> Self {
self.body = Some(ResponseBody::Text(html.into()));
self.headers.push((
"content-type".to_string(),
"text/html; charset=utf-8".to_string(),
));
self
}
pub fn bytes(mut self, bytes: Bytes) -> Self {
self.body = Some(ResponseBody::Bytes(bytes));
self
}
pub fn redirect<S: Into<String>>(mut self, location: S) -> Self {
self.headers.push(("location".to_string(), location.into()));
if self.status.is_none() {
self.status = Some(ElifStatusCode::FOUND);
}
self
}
pub fn permanent(mut self) -> Self {
self.status = Some(ElifStatusCode::MOVED_PERMANENTLY);
self
}
pub fn temporary(mut self) -> Self {
self.status = Some(ElifStatusCode::FOUND);
self
}
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.headers.push((key.into(), value.into()));
self
}
pub fn location<S: Into<String>>(mut self, url: S) -> Self {
self.headers.push(("location".to_string(), url.into()));
self
}
pub fn cache_control<S: Into<String>>(mut self, value: S) -> Self {
self.headers
.push(("cache-control".to_string(), value.into()));
self
}
pub fn content_type<S: Into<String>>(mut self, content_type: S) -> Self {
self.headers
.push(("content-type".to_string(), content_type.into()));
self
}
pub fn cookie<S: Into<String>>(mut self, cookie_value: S) -> Self {
self.headers
.push(("set-cookie".to_string(), cookie_value.into()));
self
}
pub fn error<S: Into<String>>(mut self, message: S) -> Self {
let error_data = serde_json::json!({
"error": {
"message": message.into(),
"timestamp": chrono::Utc::now().to_rfc3339()
}
});
self.body = Some(ResponseBody::Json(error_data));
self.headers
.push(("content-type".to_string(), "application/json".to_string()));
self
}
pub fn validation_error<T: Serialize>(mut self, errors: T) -> Self {
let error_data = serde_json::json!({
"error": {
"type": "validation",
"details": errors,
"timestamp": chrono::Utc::now().to_rfc3339()
}
});
self.body = Some(ResponseBody::Json(error_data));
self.headers
.push(("content-type".to_string(), "application/json".to_string()));
if self.status.is_none() {
self.status = Some(ElifStatusCode::UNPROCESSABLE_ENTITY);
}
self
}
pub fn not_found_with_message<S: Into<String>>(mut self, message: S) -> Self {
let error_data = serde_json::json!({
"error": {
"type": "not_found",
"message": message.into(),
"timestamp": chrono::Utc::now().to_rfc3339()
}
});
self.body = Some(ResponseBody::Json(error_data));
self.headers
.push(("content-type".to_string(), "application/json".to_string()));
self.status = Some(ElifStatusCode::NOT_FOUND);
self
}
pub fn cors(mut self, origin: &str) -> Self {
self.headers.push((
"access-control-allow-origin".to_string(),
origin.to_string(),
));
self
}
pub fn cors_with_credentials(mut self, origin: &str) -> Self {
self.headers.push((
"access-control-allow-origin".to_string(),
origin.to_string(),
));
self.headers.push((
"access-control-allow-credentials".to_string(),
"true".to_string(),
));
self
}
pub fn with_security_headers(mut self) -> Self {
self.headers.extend([
("x-content-type-options".to_string(), "nosniff".to_string()),
("x-frame-options".to_string(), "DENY".to_string()),
("x-xss-protection".to_string(), "1; mode=block".to_string()),
(
"referrer-policy".to_string(),
"strict-origin-when-cross-origin".to_string(),
),
]);
self
}
pub fn send(self) -> HttpResult<ElifResponse> {
Ok(self.build())
}
pub fn finish(self) -> HttpResult<ElifResponse> {
Ok(self.build())
}
pub fn build(self) -> ElifResponse {
let mut response = ElifResponse::new();
if let Some(status) = self.status {
response = response.status(status);
}
let body_sets_content_type = matches!(
self.body,
Some(ResponseBody::Json(_)) | Some(ResponseBody::Text(_))
);
if let Some(body) = self.body {
match body {
ResponseBody::Empty => {}
ResponseBody::Text(text) => {
response = response.text(text);
}
ResponseBody::Bytes(bytes) => {
response = response.bytes(bytes);
}
ResponseBody::Json(value) => {
response = response.json_value(value);
}
}
}
let has_explicit_content_type = self
.headers
.iter()
.any(|(k, _)| k.to_lowercase() == "content-type");
for (key, value) in self.headers {
if key.to_lowercase() == "content-type"
&& body_sets_content_type
&& !has_explicit_content_type
{
continue;
}
if let (Ok(name), Ok(val)) = (
crate::response::ElifHeaderName::from_str(&key),
crate::response::ElifHeaderValue::from_str(&value),
) {
response.headers_mut().append(name, val);
} else {
return ElifResponse::internal_server_error();
}
}
response
}
}
impl Default for ResponseBuilder {
fn default() -> Self {
Self::new()
}
}
impl From<ResponseBuilder> for ElifResponse {
fn from(builder: ResponseBuilder) -> Self {
builder.build()
}
}
pub fn response() -> ResponseBuilder {
ResponseBuilder::new()
}
pub fn json_response<T: Serialize>(data: T) -> ResponseBuilder {
response().json(data)
}
pub fn text_response<S: Into<String>>(content: S) -> ResponseBuilder {
response().text(content)
}
pub fn redirect_response<S: Into<String>>(location: S) -> ResponseBuilder {
response().redirect(location)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_basic_response_builder() {
let resp: ElifResponse = response().text("Hello World").ok().into();
assert_eq!(resp.status_code(), ElifStatusCode::OK);
}
#[test]
fn test_json_response() {
let data = json!({"name": "Alice", "age": 30});
let resp: ElifResponse = response().json(data).into();
assert_eq!(resp.status_code(), ElifStatusCode::OK);
}
#[test]
fn test_status_helpers() {
let resp: ElifResponse = response().text("Created").created().into();
assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
let resp: ElifResponse = response().text("Not Found").not_found().into();
assert_eq!(resp.status_code(), ElifStatusCode::NOT_FOUND);
}
#[test]
fn test_redirect_helpers() {
let resp: ElifResponse = response().redirect("/login").into();
assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
let resp: ElifResponse = response().redirect("/users").permanent().into();
assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
}
#[test]
fn test_redirect_method_call_order_independence() {
let resp1: ElifResponse = response().redirect("/test").permanent().into();
assert_eq!(resp1.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
assert!(resp1.has_header("location"));
let resp2: ElifResponse = response().permanent().redirect("/test").into();
assert_eq!(resp2.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
assert!(resp2.has_header("location"));
assert_eq!(resp1.status_code(), resp2.status_code());
}
#[test]
fn test_temporary_method_call_order_independence() {
let resp1: ElifResponse = response().redirect("/test").temporary().into();
assert_eq!(resp1.status_code(), ElifStatusCode::FOUND);
assert!(resp1.has_header("location"));
let resp2: ElifResponse = response().temporary().redirect("/test").into();
assert_eq!(resp2.status_code(), ElifStatusCode::FOUND);
assert!(resp2.has_header("location"));
assert_eq!(resp1.status_code(), resp2.status_code());
}
#[test]
fn test_redirect_status_override_behavior() {
let resp: ElifResponse = response().redirect("/default").into();
assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
let resp: ElifResponse = response().permanent().redirect("/perm").into();
assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
let resp: ElifResponse = response().temporary().redirect("/temp").into();
assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
let resp: ElifResponse = response().redirect("/test").permanent().into();
assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
let resp: ElifResponse = response().redirect("/test").permanent().temporary().into();
assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
}
#[test]
fn test_header_chaining() {
let resp: ElifResponse = response()
.text("Hello")
.header("x-custom", "value")
.cache_control("no-cache")
.into();
assert!(resp.has_header("x-custom"));
assert!(resp.has_header("cache-control"));
}
#[test]
fn test_complex_chaining() {
let user_data = json!({"id": 1, "name": "Alice"});
let resp: ElifResponse = response()
.json(user_data)
.created()
.location("/users/1")
.cache_control("no-cache")
.header("x-custom", "test")
.into();
assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
assert!(resp.has_header("location"));
assert!(resp.has_header("cache-control"));
assert!(resp.has_header("x-custom"));
}
#[test]
fn test_error_responses() {
let resp: ElifResponse = response()
.error("Something went wrong")
.internal_server_error()
.into();
assert_eq!(resp.status_code(), ElifStatusCode::INTERNAL_SERVER_ERROR);
let validation_errors = json!({"email": ["Email is required"]});
let resp: ElifResponse = response().validation_error(validation_errors).into();
assert_eq!(resp.status_code(), ElifStatusCode::UNPROCESSABLE_ENTITY);
}
#[test]
fn test_global_helpers() {
let data = json!({"message": "Hello"});
let resp: ElifResponse = json_response(data).ok().into();
assert_eq!(resp.status_code(), ElifStatusCode::OK);
let resp: ElifResponse = text_response("Hello World").into();
assert_eq!(resp.status_code(), ElifStatusCode::OK);
let resp: ElifResponse = redirect_response("/home").into();
assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
}
#[test]
fn test_cors_helpers() {
let resp: ElifResponse = response().json(json!({"data": "test"})).cors("*").into();
assert!(resp.has_header("access-control-allow-origin"));
}
#[test]
fn test_security_headers() {
let resp: ElifResponse = response()
.text("Secure content")
.with_security_headers()
.into();
assert!(resp.has_header("x-content-type-options"));
assert!(resp.has_header("x-frame-options"));
assert!(resp.has_header("x-xss-protection"));
assert!(resp.has_header("referrer-policy"));
}
#[test]
fn test_multi_value_headers() {
let resp: ElifResponse = response()
.text("Hello")
.header("set-cookie", "session=abc123; Path=/")
.header("set-cookie", "theme=dark; Path=/")
.header("set-cookie", "lang=en; Path=/")
.into();
assert!(resp.has_header("set-cookie"));
assert_eq!(resp.status_code(), ElifStatusCode::OK);
}
#[test]
fn test_cookie_helper_method() {
let resp: ElifResponse = response()
.json(json!({"user": "alice"}))
.cookie("session=12345; HttpOnly; Secure")
.cookie("csrf=token123; SameSite=Strict")
.cookie("theme=dark; Path=/")
.created()
.into();
assert!(resp.has_header("set-cookie"));
assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
}
#[test]
fn test_terminal_methods() {
let result: HttpResult<ElifResponse> =
response().json(json!({"data": "test"})).created().send();
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
let result: HttpResult<ElifResponse> = response()
.text("Hello World")
.cache_control("no-cache")
.finish();
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.status_code(), ElifStatusCode::OK);
assert!(resp.has_header("cache-control"));
}
#[test]
fn test_laravel_style_chaining() {
let result: HttpResult<ElifResponse> = response()
.json(json!({"user_id": 123}))
.created()
.location("/users/123")
.cookie("session=abc123; HttpOnly")
.header("x-custom", "value")
.send();
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
assert!(resp.has_header("location"));
assert!(resp.has_header("set-cookie"));
assert!(resp.has_header("x-custom"));
}
#[test]
fn test_json_serialization_error_handling() {
use std::collections::HashMap;
let valid_data = HashMap::from([("key", "value")]);
let resp: ElifResponse = response().json(valid_data).into();
assert_eq!(resp.status_code(), ElifStatusCode::OK);
}
#[test]
fn test_header_append_vs_insert_behavior() {
let resp: ElifResponse = response()
.json(json!({"test": "data"}))
.header("x-custom", "value1")
.header("x-custom", "value2")
.header("x-custom", "value3")
.into();
assert!(resp.has_header("x-custom"));
assert_eq!(resp.status_code(), ElifStatusCode::OK);
}
}