elif_http/response/
response.rs

1//! Response abstraction for building HTTP responses
2//!
3//! Provides fluent response building with status codes, headers, and JSON serialization.
4
5use super::{ElifHeaderMap, ElifHeaderName, ElifHeaderValue, ElifStatusCode};
6use crate::errors::{HttpError, HttpResult};
7use axum::{
8    body::{Body, Bytes},
9    response::{IntoResponse, Response},
10};
11use serde::Serialize;
12
13/// Response builder for creating HTTP responses with fluent API
14#[derive(Debug)]
15pub struct ElifResponse {
16    status: ElifStatusCode,
17    headers: ElifHeaderMap,
18    body: ResponseBody,
19}
20
21/// Response body types
22#[derive(Debug)]
23pub enum ResponseBody {
24    Empty,
25    Text(String),
26    Bytes(Bytes),
27    Json(serde_json::Value),
28}
29
30impl ElifResponse {
31    /// Create new response with OK status
32    pub fn new() -> Self {
33        Self {
34            status: ElifStatusCode::OK,
35            headers: ElifHeaderMap::new(),
36            body: ResponseBody::Empty,
37        }
38    }
39
40    /// Create response with specific status code
41    pub fn with_status(status: ElifStatusCode) -> Self {
42        Self {
43            status,
44            headers: ElifHeaderMap::new(),
45            body: ResponseBody::Empty,
46        }
47    }
48
49    /// Set response status code (consuming)
50    pub fn status(mut self, status: ElifStatusCode) -> Self {
51        self.status = status;
52        self
53    }
54
55    /// Set response status code (borrowing - for middleware use)
56    pub fn set_status(&mut self, status: ElifStatusCode) {
57        self.status = status;
58    }
59
60    /// Get response status code
61    pub fn status_code(&self) -> ElifStatusCode {
62        self.status
63    }
64
65    /// Get response headers
66    pub fn headers(&self) -> &ElifHeaderMap {
67        &self.headers
68    }
69
70    /// Get mutable reference to response headers
71    pub fn headers_mut(&mut self) -> &mut ElifHeaderMap {
72        &mut self.headers
73    }
74
75    /// Check if response has a specific header
76    pub fn has_header<K: AsRef<str>>(&self, key: K) -> bool {
77        self.headers.contains_key_str(key.as_ref())
78    }
79
80    /// Get header value by name
81    pub fn get_header<K: AsRef<str>>(&self, key: K) -> Option<&ElifHeaderValue> {
82        self.headers.get_str(key.as_ref())
83    }
84
85    // Simple panic-safe convenience methods for development ease
86
87    /// Add header to response (Simple - never panics)
88    ///
89    /// Simple equivalent: `$response->header('X-Custom', 'value')`
90    /// Returns 500 error response on invalid header names/values
91    pub fn with_header<K, V>(self, key: K, value: V) -> Self
92    where
93        K: AsRef<str>,
94        V: AsRef<str>,
95    {
96        self.header(key, value).unwrap_or_else(|err| {
97            tracing::error!("Header creation failed in with_header: {}", err);
98            ElifResponse::internal_server_error()
99        })
100    }
101
102    /// Set JSON body (Simple - never panics)
103    ///
104    /// Simple equivalent: `$response->json($data)`
105    /// Returns 500 error response on serialization failure
106    pub fn with_json<T: Serialize>(self, data: &T) -> Self {
107        self.json(data).unwrap_or_else(|err| {
108            tracing::error!("JSON serialization failed in with_json: {}", err);
109            ElifResponse::internal_server_error()
110        })
111    }
112
113    /// Set text body (Simple - never fails)
114    ///
115    /// Simple equivalent: `response($text)`
116    pub fn with_text<S: AsRef<str>>(mut self, content: S) -> Self {
117        self.body = ResponseBody::Text(content.as_ref().to_string());
118        self
119    }
120
121    /// Set HTML body with content-type (Simple - never fails)
122    ///
123    /// Simple equivalent: `response($html)->header('Content-Type', 'text/html')`
124    pub fn with_html<S: AsRef<str>>(self, content: S) -> Self {
125        self.with_text(content)
126            .with_header("content-type", "text/html; charset=utf-8")
127    }
128
129    /// Add header to response (consuming)
130    pub fn header<K, V>(mut self, key: K, value: V) -> HttpResult<Self>
131    where
132        K: AsRef<str>,
133        V: AsRef<str>,
134    {
135        let header_name = ElifHeaderName::from_str(key.as_ref())
136            .map_err(|e| HttpError::internal(format!("Invalid header name: {}", e)))?;
137        let header_value = ElifHeaderValue::from_str(value.as_ref())
138            .map_err(|e| HttpError::internal(format!("Invalid header value: {}", e)))?;
139
140        self.headers.insert(header_name, header_value);
141        Ok(self)
142    }
143
144    /// Add header to response (borrowing - for middleware use)
145    pub fn add_header<K, V>(&mut self, key: K, value: V) -> HttpResult<()>
146    where
147        K: AsRef<str>,
148        V: AsRef<str>,
149    {
150        let header_name = ElifHeaderName::from_str(key.as_ref())
151            .map_err(|e| HttpError::internal(format!("Invalid header name: {}", e)))?;
152        let header_value = ElifHeaderValue::from_str(value.as_ref())
153            .map_err(|e| HttpError::internal(format!("Invalid header value: {}", e)))?;
154
155        self.headers.insert(header_name, header_value);
156        Ok(())
157    }
158
159    /// Remove header from response
160    pub fn remove_header<K: AsRef<str>>(&mut self, key: K) -> Option<ElifHeaderValue> {
161        self.headers.remove_header(key.as_ref())
162    }
163
164    /// Set Content-Type header (consuming)
165    pub fn content_type(self, content_type: &str) -> HttpResult<Self> {
166        self.header("content-type", content_type)
167    }
168
169    /// Set Content-Type header (borrowing - for middleware use)
170    pub fn set_content_type(&mut self, content_type: &str) -> HttpResult<()> {
171        self.add_header("content-type", content_type)
172    }
173
174    /// Set response body as text (consuming)
175    pub fn text<S: Into<String>>(mut self, text: S) -> Self {
176        self.body = ResponseBody::Text(text.into());
177        self
178    }
179
180    /// Set response body as text (borrowing - for middleware use)
181    pub fn set_text<S: Into<String>>(&mut self, text: S) {
182        self.body = ResponseBody::Text(text.into());
183    }
184
185    /// Set response body as bytes (consuming)
186    pub fn bytes(mut self, bytes: Bytes) -> Self {
187        self.body = ResponseBody::Bytes(bytes);
188        self
189    }
190
191    /// Set response body as bytes (borrowing - for middleware use)
192    pub fn set_bytes(&mut self, bytes: Bytes) {
193        self.body = ResponseBody::Bytes(bytes);
194    }
195
196    /// Set response body as JSON (consuming)
197    pub fn json<T: Serialize>(mut self, data: &T) -> HttpResult<Self> {
198        let json_value = serde_json::to_value(data)
199            .map_err(|e| HttpError::internal(format!("JSON serialization failed: {}", e)))?;
200        self.body = ResponseBody::Json(json_value);
201        Ok(self)
202    }
203
204    /// Set response body as JSON (borrowing - for middleware use)
205    pub fn set_json<T: Serialize>(&mut self, data: &T) -> HttpResult<()> {
206        let json_value = serde_json::to_value(data)
207            .map_err(|e| HttpError::internal(format!("JSON serialization failed: {}", e)))?;
208        self.body = ResponseBody::Json(json_value);
209        Ok(())
210    }
211
212    /// Set response body as raw JSON value (consuming)
213    pub fn json_value(mut self, value: serde_json::Value) -> Self {
214        self.body = ResponseBody::Json(value);
215        self
216    }
217
218    /// Set response body as raw JSON value (borrowing - for middleware use)
219    pub fn set_json_value(&mut self, value: serde_json::Value) {
220        self.body = ResponseBody::Json(value);
221    }
222
223    /// Build the response
224    pub fn build(mut self) -> HttpResult<Response<Body>> {
225        // Set default content type based on body type
226        if !self.headers.contains_key_str("content-type") {
227            match &self.body {
228                ResponseBody::Json(_) => {
229                    self = self.content_type("application/json")?;
230                }
231                ResponseBody::Text(_) => {
232                    self = self.content_type("text/plain; charset=utf-8")?;
233                }
234                _ => {}
235            }
236        }
237
238        let body = match self.body {
239            ResponseBody::Empty => Body::empty(),
240            ResponseBody::Text(text) => Body::from(text),
241            ResponseBody::Bytes(bytes) => Body::from(bytes),
242            ResponseBody::Json(value) => {
243                let json_string = serde_json::to_string(&value).map_err(|e| {
244                    HttpError::internal(format!("JSON serialization failed: {}", e))
245                })?;
246                Body::from(json_string)
247            }
248        };
249
250        let mut response = Response::builder().status(self.status.to_axum());
251
252        // Add headers
253        for (key, value) in self.headers.iter() {
254            response = response.header(key.to_axum(), value.to_axum());
255        }
256
257        response
258            .body(body)
259            .map_err(|e| HttpError::internal(format!("Failed to build response: {}", e)))
260    }
261
262    /// Convert ElifResponse to Axum Response for backward compatibility
263    pub(crate) fn into_axum_response(self) -> Response<Body> {
264        IntoResponse::into_response(self)
265    }
266
267    /// Convert Axum Response to ElifResponse for backward compatibility
268    pub(crate) async fn from_axum_response(response: Response<Body>) -> Self {
269        let (parts, body) = response.into_parts();
270
271        // Extract body bytes
272        let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
273            Ok(bytes) => bytes,
274            Err(_) => Bytes::new(),
275        };
276
277        let mut elif_response = Self::with_status(ElifStatusCode::from_axum(parts.status));
278        let headers = parts.headers.clone();
279        elif_response.headers = ElifHeaderMap::from_axum(parts.headers);
280
281        // Try to determine body type based on content-type header
282        if let Some(content_type) = headers.get("content-type") {
283            if let Ok(content_type_str) = content_type.to_str() {
284                if content_type_str.contains("application/json") {
285                    // Try to parse as JSON
286                    if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&body_bytes)
287                    {
288                        elif_response.body = ResponseBody::Json(json_value);
289                    } else {
290                        elif_response.body = ResponseBody::Bytes(body_bytes);
291                    }
292                } else if content_type_str.starts_with("text/") {
293                    // Parse as text
294                    match String::from_utf8(body_bytes.to_vec()) {
295                        Ok(text) => elif_response.body = ResponseBody::Text(text),
296                        Err(_) => elif_response.body = ResponseBody::Bytes(body_bytes),
297                    }
298                } else {
299                    elif_response.body = ResponseBody::Bytes(body_bytes);
300                }
301            } else {
302                elif_response.body = ResponseBody::Bytes(body_bytes);
303            }
304        } else if body_bytes.is_empty() {
305            elif_response.body = ResponseBody::Empty;
306        } else {
307            elif_response.body = ResponseBody::Bytes(body_bytes);
308        }
309
310        elif_response
311    }
312}
313
314impl Default for ElifResponse {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320/// Convenience methods for common response types
321impl ElifResponse {
322    /// Create 200 OK response
323    pub fn ok() -> Self {
324        Self::with_status(ElifStatusCode::OK)
325    }
326
327    /// Create 201 Created response
328    pub fn created() -> Self {
329        Self::with_status(ElifStatusCode::CREATED)
330    }
331
332    /// Create 204 No Content response
333    pub fn no_content() -> Self {
334        Self::with_status(ElifStatusCode::NO_CONTENT)
335    }
336
337    /// Create 400 Bad Request response
338    pub fn bad_request() -> Self {
339        Self::with_status(ElifStatusCode::BAD_REQUEST)
340    }
341
342    /// Create 401 Unauthorized response
343    pub fn unauthorized() -> Self {
344        Self::with_status(ElifStatusCode::UNAUTHORIZED)
345    }
346
347    /// Create 403 Forbidden response
348    pub fn forbidden() -> Self {
349        Self::with_status(ElifStatusCode::FORBIDDEN)
350    }
351
352    /// Create 404 Not Found response
353    pub fn not_found() -> Self {
354        Self::with_status(ElifStatusCode::NOT_FOUND)
355    }
356
357    /// Create 422 Unprocessable Entity response
358    pub fn unprocessable_entity() -> Self {
359        Self::with_status(ElifStatusCode::UNPROCESSABLE_ENTITY)
360    }
361
362    /// Create 500 Internal Server Error response
363    pub fn internal_server_error() -> Self {
364        Self::with_status(ElifStatusCode::INTERNAL_SERVER_ERROR)
365    }
366
367    /// Create JSON response with data
368    pub fn json_ok<T: Serialize>(data: &T) -> HttpResult<Response<Body>> {
369        Self::ok().json(data)?.build()
370    }
371
372    /// Create JSON error response
373    pub fn json_error(status: ElifStatusCode, message: &str) -> HttpResult<Response<Body>> {
374        let error_data = serde_json::json!({
375            "error": {
376                "code": status.as_u16(),
377                "message": message
378            }
379        });
380
381        Self::with_status(status).json_value(error_data).build()
382    }
383
384    /// Create validation error response
385    pub fn validation_error<T: Serialize>(errors: &T) -> HttpResult<Response<Body>> {
386        let error_data = serde_json::json!({
387            "error": {
388                "code": 422,
389                "message": "Validation failed",
390                "details": errors
391            }
392        });
393
394        Self::unprocessable_entity().json_value(error_data).build()
395    }
396}
397
398/// Helper trait for converting types to ElifResponse
399pub trait IntoElifResponse {
400    fn into_response(self) -> ElifResponse;
401}
402
403impl IntoElifResponse for String {
404    fn into_response(self) -> ElifResponse {
405        ElifResponse::ok().text(self)
406    }
407}
408
409impl IntoElifResponse for &str {
410    fn into_response(self) -> ElifResponse {
411        ElifResponse::ok().text(self)
412    }
413}
414
415impl IntoElifResponse for ElifStatusCode {
416    fn into_response(self) -> ElifResponse {
417        ElifResponse::with_status(self)
418    }
419}
420
421impl IntoElifResponse for ElifResponse {
422    fn into_response(self) -> ElifResponse {
423        self
424    }
425}
426
427/// Convert ElifResponse to Axum Response
428impl IntoResponse for ElifResponse {
429    fn into_response(self) -> Response {
430        match self.build() {
431            Ok(response) => response,
432            Err(e) => {
433                // Fallback error response
434                (
435                    ElifStatusCode::INTERNAL_SERVER_ERROR.to_axum(),
436                    format!("Response build failed: {}", e),
437                )
438                    .into_response()
439            }
440        }
441    }
442}
443
444/// Redirect response builders
445impl ElifResponse {
446    /// Create 301 Moved Permanently redirect
447    pub fn redirect_permanent(location: &str) -> HttpResult<Self> {
448        Self::with_status(ElifStatusCode::MOVED_PERMANENTLY).header("location", location)
449    }
450
451    /// Create 302 Found (temporary) redirect
452    pub fn redirect_temporary(location: &str) -> HttpResult<Self> {
453        Self::with_status(ElifStatusCode::FOUND).header("location", location)
454    }
455
456    /// Create 303 See Other redirect
457    pub fn redirect_see_other(location: &str) -> HttpResult<Self> {
458        Self::with_status(ElifStatusCode::SEE_OTHER).header("location", location)
459    }
460}
461
462/// File download response builders
463impl ElifResponse {
464    /// Create file download response
465    pub fn download(filename: &str, content: Bytes) -> HttpResult<Self> {
466        let content_disposition = format!("attachment; filename=\"{}\"", filename);
467
468        Ok(Self::ok()
469            .header("content-disposition", content_disposition)?
470            .header("content-type", "application/octet-stream")?
471            .bytes(content))
472    }
473
474    /// Create inline file response (display in browser)
475    pub fn file_inline(filename: &str, content_type: &str, content: Bytes) -> HttpResult<Self> {
476        let content_disposition = format!("inline; filename=\"{}\"", filename);
477
478        Ok(Self::ok()
479            .header("content-disposition", content_disposition)?
480            .header("content-type", content_type)?
481            .bytes(content))
482    }
483
484    /// Create file response from filesystem path
485    pub fn file<P: AsRef<std::path::Path>>(path: P) -> HttpResult<Self> {
486        let path = path.as_ref();
487        let content = std::fs::read(path)
488            .map_err(|e| HttpError::internal(format!("Failed to read file: {}", e)))?;
489
490        let mime_type = Self::guess_mime_type(path);
491
492        Ok(Self::ok()
493            .header("content-type", mime_type)?
494            .bytes(Bytes::from(content)))
495    }
496
497    /// Guess MIME type from file extension
498    fn guess_mime_type(path: &std::path::Path) -> &'static str {
499        match path.extension().and_then(|ext| ext.to_str()) {
500            Some("html") | Some("htm") => "text/html; charset=utf-8",
501            Some("css") => "text/css",
502            Some("js") => "application/javascript",
503            Some("json") => "application/json",
504            Some("xml") => "application/xml",
505            Some("pdf") => "application/pdf",
506            Some("txt") => "text/plain; charset=utf-8",
507            Some("png") => "image/png",
508            Some("jpg") | Some("jpeg") => "image/jpeg",
509            Some("gif") => "image/gif",
510            Some("svg") => "image/svg+xml",
511            Some("ico") => "image/x-icon",
512            Some("woff") => "font/woff",
513            Some("woff2") => "font/woff2",
514            Some("ttf") => "font/ttf",
515            Some("mp4") => "video/mp4",
516            Some("webm") => "video/webm",
517            Some("mp3") => "audio/mpeg",
518            Some("wav") => "audio/wav",
519            Some("zip") => "application/zip",
520            Some("tar") => "application/x-tar",
521            Some("gz") => "application/gzip",
522            _ => "application/octet-stream",
523        }
524    }
525}
526
527/// Enhanced response helper methods for common patterns
528impl ElifResponse {
529    /// Create JSON response with data and optional status
530    pub fn json_with_status<T: Serialize>(status: ElifStatusCode, data: &T) -> HttpResult<Self> {
531        Self::with_status(status).json(data)
532    }
533
534    /// Create JSON response from serde_json::Value
535    pub fn json_raw(value: serde_json::Value) -> Self {
536        Self::ok().json_value(value)
537    }
538
539    /// Create JSON response from serde_json::Value with status
540    pub fn json_raw_with_status(status: ElifStatusCode, value: serde_json::Value) -> Self {
541        Self::with_status(status).json_value(value)
542    }
543
544    /// Create text response with custom content type
545    pub fn text_with_type(content: &str, content_type: &str) -> HttpResult<Self> {
546        Self::ok()
547            .text(content)
548            .header("content-type", content_type)
549    }
550
551    /// Create XML response
552    pub fn xml<S: AsRef<str>>(content: S) -> HttpResult<Self> {
553        Self::text_with_type(content.as_ref(), "application/xml; charset=utf-8")
554    }
555
556    /// Create CSV response
557    pub fn csv<S: AsRef<str>>(content: S) -> HttpResult<Self> {
558        Self::text_with_type(content.as_ref(), "text/csv; charset=utf-8")
559    }
560
561    /// Create JavaScript response
562    pub fn javascript<S: AsRef<str>>(content: S) -> HttpResult<Self> {
563        Self::text_with_type(content.as_ref(), "application/javascript; charset=utf-8")
564    }
565
566    /// Create CSS response
567    pub fn css<S: AsRef<str>>(content: S) -> HttpResult<Self> {
568        Self::text_with_type(content.as_ref(), "text/css; charset=utf-8")
569    }
570
571    /// Create streaming response with chunked transfer encoding
572    pub fn stream() -> HttpResult<Self> {
573        Self::ok().header("transfer-encoding", "chunked")
574    }
575
576    /// Create Server-Sent Events (SSE) response
577    pub fn sse() -> HttpResult<Self> {
578        Self::ok()
579            .header("content-type", "text/event-stream")?
580            .header("cache-control", "no-cache")?
581            .header("connection", "keep-alive")
582    }
583
584    /// Create JSONP response with callback
585    pub fn jsonp<T: Serialize>(callback: &str, data: &T) -> HttpResult<Self> {
586        let json_data = serde_json::to_string(data)
587            .map_err(|e| HttpError::internal(format!("JSON serialization failed: {}", e)))?;
588
589        let jsonp_content = format!("{}({});", callback, json_data);
590
591        Self::ok()
592            .text(jsonp_content)
593            .header("content-type", "application/javascript; charset=utf-8")
594    }
595
596    /// Create image response from bytes with format detection
597    pub fn image(content: Bytes, format: ImageFormat) -> HttpResult<Self> {
598        let content_type = match format {
599            ImageFormat::Png => "image/png",
600            ImageFormat::Jpeg => "image/jpeg",
601            ImageFormat::Gif => "image/gif",
602            ImageFormat::WebP => "image/webp",
603            ImageFormat::Svg => "image/svg+xml",
604        };
605
606        Ok(Self::ok()
607            .header("content-type", content_type)?
608            .bytes(content))
609    }
610
611    /// Create binary response with custom MIME type
612    pub fn binary(content: Bytes, mime_type: &str) -> HttpResult<Self> {
613        Ok(Self::ok().header("content-type", mime_type)?.bytes(content))
614    }
615
616    /// Create CORS preflight response
617    pub fn cors_preflight(
618        allowed_origins: &[&str],
619        allowed_methods: &[&str],
620        allowed_headers: &[&str],
621        max_age: Option<u32>,
622    ) -> HttpResult<Self> {
623        let mut response = Self::no_content()
624            .header("access-control-allow-origin", allowed_origins.join(","))?
625            .header("access-control-allow-methods", allowed_methods.join(","))?
626            .header("access-control-allow-headers", allowed_headers.join(","))?;
627
628        if let Some(max_age) = max_age {
629            response = response.header("access-control-max-age", max_age.to_string())?;
630        }
631
632        Ok(response)
633    }
634
635    /// Add CORS headers to existing response
636    pub fn with_cors(
637        mut self,
638        origin: &str,
639        credentials: bool,
640        exposed_headers: Option<&[&str]>,
641    ) -> HttpResult<Self> {
642        self = self.header("access-control-allow-origin", origin)?;
643
644        if credentials {
645            self = self.header("access-control-allow-credentials", "true")?;
646        }
647
648        if let Some(headers) = exposed_headers {
649            self = self.header("access-control-expose-headers", headers.join(","))?;
650        }
651
652        Ok(self)
653    }
654
655    /// Create response with caching headers
656    pub fn with_cache(mut self, max_age: u32, public: bool) -> HttpResult<Self> {
657        let cache_control = if public {
658            format!("public, max-age={}", max_age)
659        } else {
660            format!("private, max-age={}", max_age)
661        };
662
663        self = self.header("cache-control", cache_control)?;
664
665        // Add ETag for cache validation (simple content-based)
666        let etag = format!("\"{}\"", self.generate_etag());
667        self = self.header("etag", etag)?;
668
669        Ok(self)
670    }
671
672    /// Generate content-based ETag including response body
673    fn generate_etag(&self) -> String {
674        use std::collections::hash_map::DefaultHasher;
675        use std::hash::{Hash, Hasher};
676
677        let mut hasher = DefaultHasher::new();
678        self.status.as_u16().hash(&mut hasher);
679
680        // Hash header keys and values for ETag generation
681        for (key, value) in self.headers.iter() {
682            key.as_str().hash(&mut hasher);
683            if let Ok(value_str) = value.to_str() {
684                value_str.hash(&mut hasher);
685            }
686        }
687
688        // Hash the response body for a correct content-based ETag
689        match &self.body {
690            ResponseBody::Empty => "empty".hash(&mut hasher),
691            ResponseBody::Text(text) => text.hash(&mut hasher),
692            ResponseBody::Bytes(bytes) => bytes.hash(&mut hasher),
693            ResponseBody::Json(value) => {
694                // Use canonical JSON string representation for consistent hashing
695                if let Ok(json_string) = serde_json::to_string(value) {
696                    json_string.hash(&mut hasher);
697                } else {
698                    // Fallback if serialization fails
699                    "invalid_json".hash(&mut hasher);
700                }
701            }
702        }
703
704        format!("{:x}", hasher.finish())
705    }
706
707    /// Create conditional response based on If-None-Match header
708    pub fn conditional(self, request_etag: Option<&str>) -> Self {
709        if let Some(request_etag) = request_etag {
710            let response_etag = self.generate_etag();
711            let response_etag_quoted = format!("\"{}\"", response_etag);
712
713            if request_etag == response_etag_quoted || request_etag == "*" {
714                return ElifResponse::with_status(ElifStatusCode::NOT_MODIFIED);
715            }
716        }
717        self
718    }
719
720    /// Add security headers to response
721    pub fn with_security_headers(mut self) -> HttpResult<Self> {
722        self = self.header("x-content-type-options", "nosniff")?;
723        self = self.header("x-frame-options", "DENY")?;
724        self = self.header("x-xss-protection", "1; mode=block")?;
725        self = self.header("referrer-policy", "strict-origin-when-cross-origin")?;
726        self = self.header("content-security-policy", "default-src 'self'")?;
727        Ok(self)
728    }
729
730    /// Add performance headers
731    pub fn with_performance_headers(mut self) -> HttpResult<Self> {
732        self = self.header("x-dns-prefetch-control", "on")?;
733        self = self.header("x-powered-by", "elif.rs")?;
734        Ok(self)
735    }
736}
737
738/// Image format enumeration for typed image responses
739#[derive(Debug, Clone, Copy)]
740pub enum ImageFormat {
741    Png,
742    Jpeg,
743    Gif,
744    WebP,
745    Svg,
746}
747
748/// Response transformation helpers
749impl ElifResponse {
750    /// Transform response body with a closure
751    pub fn transform_body<F>(mut self, transform: F) -> Self
752    where
753        F: FnOnce(ResponseBody) -> ResponseBody,
754    {
755        self.body = transform(self.body);
756        self
757    }
758
759    /// Add multiple headers at once
760    pub fn with_headers<I, K, V>(mut self, headers: I) -> HttpResult<Self>
761    where
762        I: IntoIterator<Item = (K, V)>,
763        K: AsRef<str>,
764        V: AsRef<str>,
765    {
766        for (key, value) in headers {
767            self = self.header(key, value)?;
768        }
769        Ok(self)
770    }
771
772    /// Create response and immediately build to Axum Response
773    pub fn build_axum(self) -> Response<axum::body::Body> {
774        match self.build() {
775            Ok(response) => response,
776            Err(e) => {
777                // Fallback error response
778                (
779                    ElifStatusCode::INTERNAL_SERVER_ERROR.to_axum(),
780                    format!("Response build failed: {}", e),
781                )
782                    .into_response()
783            }
784        }
785    }
786
787    /// Check if response is an error (4xx or 5xx status)
788    pub fn is_error(&self) -> bool {
789        self.status.as_u16() >= 400
790    }
791
792    /// Check if response is successful (2xx status)
793    pub fn is_success(&self) -> bool {
794        let status_code = self.status.as_u16();
795        (200..300).contains(&status_code)
796    }
797
798    /// Check if response is a redirect (3xx status)
799    pub fn is_redirect(&self) -> bool {
800        let status_code = self.status.as_u16();
801        (300..400).contains(&status_code)
802    }
803
804    /// Get response body size estimate
805    pub fn body_size_estimate(&self) -> usize {
806        match &self.body {
807            ResponseBody::Empty => 0,
808            ResponseBody::Text(text) => text.len(),
809            ResponseBody::Bytes(bytes) => bytes.len(),
810            ResponseBody::Json(value) => {
811                // Estimate JSON serialization size
812                serde_json::to_string(value).map(|s| s.len()).unwrap_or(0)
813            }
814        }
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821    use serde_json::json;
822
823    #[test]
824    fn test_basic_response_building() {
825        let response = ElifResponse::ok().text("Hello, World!");
826
827        assert_eq!(response.status, ElifStatusCode::OK);
828        match response.body {
829            ResponseBody::Text(text) => assert_eq!(text, "Hello, World!"),
830            _ => panic!("Expected text body"),
831        }
832    }
833
834    #[test]
835    fn test_json_response() {
836        let data = json!({
837            "name": "John Doe",
838            "age": 30
839        });
840
841        let response = ElifResponse::ok().json_value(data.clone());
842
843        match response.body {
844            ResponseBody::Json(value) => assert_eq!(value, data),
845            _ => panic!("Expected JSON body"),
846        }
847    }
848
849    #[test]
850    fn test_status_codes() {
851        assert_eq!(ElifResponse::created().status, ElifStatusCode::CREATED);
852        assert_eq!(ElifResponse::not_found().status, ElifStatusCode::NOT_FOUND);
853        assert_eq!(
854            ElifResponse::internal_server_error().status,
855            ElifStatusCode::INTERNAL_SERVER_ERROR
856        );
857    }
858
859    #[test]
860    fn test_headers() {
861        let response = ElifResponse::ok()
862            .header("x-custom-header", "test-value")
863            .unwrap();
864
865        let custom_header =
866            crate::response::headers::ElifHeaderName::from_str("x-custom-header").unwrap();
867        assert!(response.headers.contains_key(&custom_header));
868        assert_eq!(
869            response.headers.get(&custom_header).unwrap(),
870            &crate::response::headers::ElifHeaderValue::from_static("test-value")
871        );
872    }
873
874    #[test]
875    fn test_redirect_responses() {
876        let redirect = ElifResponse::redirect_permanent("/new-location").unwrap();
877        assert_eq!(redirect.status, ElifStatusCode::MOVED_PERMANENTLY);
878        assert!(redirect.headers.contains_key(
879            &crate::response::headers::ElifHeaderName::from_str("location").unwrap()
880        ));
881    }
882
883    #[test]
884    fn test_status_code_getter() {
885        let response = ElifResponse::created();
886        assert_eq!(response.status_code(), ElifStatusCode::CREATED);
887    }
888
889    #[test]
890    fn test_borrowing_api_headers() {
891        let mut response = ElifResponse::ok();
892
893        // Test borrowing header methods
894        response
895            .add_header("x-custom-header", "test-value")
896            .unwrap();
897        response.set_content_type("application/json").unwrap();
898
899        let built_response = response.build().unwrap();
900        let headers = built_response.headers();
901
902        assert!(headers.contains_key("x-custom-header"));
903        assert_eq!(headers.get("x-custom-header").unwrap(), "test-value");
904        assert_eq!(headers.get("content-type").unwrap(), "application/json");
905    }
906
907    #[test]
908    fn test_borrowing_api_body() {
909        let mut response = ElifResponse::ok();
910
911        // Test borrowing text body method
912        response.set_text("Hello World");
913        assert_eq!(response.status_code(), ElifStatusCode::OK);
914        match &response.body {
915            ResponseBody::Text(text) => assert_eq!(text, "Hello World"),
916            _ => panic!("Expected text body after calling set_text"),
917        }
918
919        // Test borrowing bytes body method
920        let bytes_data = Bytes::from("binary data");
921        response.set_bytes(bytes_data.clone());
922        match &response.body {
923            ResponseBody::Bytes(bytes) => assert_eq!(bytes, &bytes_data),
924            _ => panic!("Expected bytes body after calling set_bytes"),
925        }
926
927        // Test borrowing JSON body method
928        let data = json!({"message": "Hello"});
929        response.set_json_value(data.clone());
930
931        // Verify the body was set correctly
932        match &response.body {
933            ResponseBody::Json(value) => assert_eq!(*value, data),
934            _ => panic!("Expected JSON body after calling set_json_value"),
935        }
936    }
937
938    #[test]
939    fn test_borrowing_api_status() {
940        let mut response = ElifResponse::ok();
941
942        // Test borrowing status method
943        response.set_status(ElifStatusCode::CREATED);
944        assert_eq!(response.status_code(), ElifStatusCode::CREATED);
945
946        // Test multiple modifications
947        response.set_status(ElifStatusCode::ACCEPTED);
948        response.set_text("Updated");
949
950        assert_eq!(response.status_code(), ElifStatusCode::ACCEPTED);
951    }
952
953    #[test]
954    fn test_borrowing_api_middleware_pattern() {
955        // Test the pattern that caused issues in middleware v2
956        let mut response = ElifResponse::ok().text("Original");
957
958        // Simulate middleware adding headers iteratively
959        let headers = vec![
960            ("x-middleware-1", "executed"),
961            ("x-middleware-2", "processed"),
962            ("x-custom", "value"),
963        ];
964
965        for (name, value) in headers {
966            // This should work without ownership issues
967            response.add_header(name, value).unwrap();
968        }
969
970        let built = response.build().unwrap();
971        let response_headers = built.headers();
972
973        assert!(response_headers.contains_key("x-middleware-1"));
974        assert!(response_headers.contains_key("x-middleware-2"));
975        assert!(response_headers.contains_key("x-custom"));
976    }
977
978    #[test]
979    fn test_etag_generation_includes_body_content() {
980        // Test that ETag generation properly includes response body content
981
982        // Same status and headers, different text bodies
983        let response1 = ElifResponse::ok().with_text("Hello World");
984        let response2 = ElifResponse::ok().with_text("Different Content");
985
986        let etag1 = response1.generate_etag();
987        let etag2 = response2.generate_etag();
988
989        assert_ne!(
990            etag1, etag2,
991            "ETags should be different for different text content"
992        );
993
994        // Same status and headers, different JSON bodies
995        let json1 = serde_json::json!({"name": "Alice", "age": 30});
996        let json2 = serde_json::json!({"name": "Bob", "age": 25});
997
998        let response3 = ElifResponse::ok().with_json(&json1);
999        let response4 = ElifResponse::ok().with_json(&json2);
1000
1001        let etag3 = response3.generate_etag();
1002        let etag4 = response4.generate_etag();
1003
1004        assert_ne!(
1005            etag3, etag4,
1006            "ETags should be different for different JSON content"
1007        );
1008
1009        // Same content should produce same ETag
1010        let response5 = ElifResponse::ok().with_text("Hello World");
1011        let etag5 = response5.generate_etag();
1012
1013        assert_eq!(
1014            etag1, etag5,
1015            "ETags should be identical for identical content"
1016        );
1017
1018        // Different response types with same logical content should be different
1019        let response6 = ElifResponse::ok().with_json(&serde_json::json!("Hello World"));
1020        let etag6 = response6.generate_etag();
1021
1022        assert_ne!(
1023            etag1, etag6,
1024            "ETags should be different for different body types even with same content"
1025        );
1026    }
1027
1028    #[test]
1029    fn test_etag_generation_different_body_types() {
1030        // Test ETag generation for all body types
1031
1032        let empty_response = ElifResponse::ok();
1033        let text_response = ElifResponse::ok().with_text("test content");
1034        let bytes_response = ElifResponse::ok().bytes(Bytes::from("test content"));
1035        let json_response = ElifResponse::ok().with_json(&serde_json::json!({"key": "value"}));
1036
1037        let empty_etag = empty_response.generate_etag();
1038        let text_etag = text_response.generate_etag();
1039        let bytes_etag = bytes_response.generate_etag();
1040        let json_etag = json_response.generate_etag();
1041
1042        // All ETags should be different
1043        let etags = vec![&empty_etag, &text_etag, &bytes_etag, &json_etag];
1044        for i in 0..etags.len() {
1045            for j in (i + 1)..etags.len() {
1046                assert_ne!(
1047                    etags[i], etags[j],
1048                    "ETags should be unique for different body types: {} vs {}",
1049                    etags[i], etags[j]
1050                );
1051            }
1052        }
1053
1054        // ETags should be consistent for same content
1055        let text_response2 = ElifResponse::ok().with_text("test content");
1056        let text_etag2 = text_response2.generate_etag();
1057        assert_eq!(
1058            text_etag, text_etag2,
1059            "ETags should be consistent for identical text content"
1060        );
1061    }
1062
1063    #[test]
1064    fn test_etag_generation_with_status_and_headers() {
1065        // Verify that status codes and headers still affect ETag generation
1066
1067        let base_content = "same content";
1068
1069        // Different status codes should produce different ETags
1070        let response_200 = ElifResponse::ok().with_text(base_content);
1071        let response_201 = ElifResponse::created().with_text(base_content);
1072
1073        let etag_200 = response_200.generate_etag();
1074        let etag_201 = response_201.generate_etag();
1075
1076        assert_ne!(
1077            etag_200, etag_201,
1078            "ETags should be different for different status codes"
1079        );
1080
1081        // Different headers should produce different ETags
1082        let response_no_header = ElifResponse::ok().with_text(base_content);
1083        let response_with_header = ElifResponse::ok()
1084            .with_text(base_content)
1085            .with_header("x-custom", "value");
1086
1087        let etag_no_header = response_no_header.generate_etag();
1088        let etag_with_header = response_with_header.generate_etag();
1089
1090        assert_ne!(
1091            etag_no_header, etag_with_header,
1092            "ETags should be different when headers differ"
1093        );
1094    }
1095
1096    #[test]
1097    fn test_etag_conditional_response() {
1098        let response = ElifResponse::ok().with_text("Test content");
1099        let etag = response.generate_etag();
1100        let etag_quoted = format!("\"{}\"", etag);
1101
1102        // Test If-None-Match with matching ETag (should return 304)
1103        let conditional_response = response.conditional(Some(&etag_quoted));
1104        assert_eq!(
1105            conditional_response.status_code(),
1106            ElifStatusCode::NOT_MODIFIED
1107        );
1108
1109        // Test If-None-Match with non-matching ETag (should return original response)
1110        let response2 = ElifResponse::ok().with_text("Test content");
1111        let different_etag = "\"different_etag_value\"";
1112        let conditional_response2 = response2.conditional(Some(different_etag));
1113        assert_eq!(conditional_response2.status_code(), ElifStatusCode::OK);
1114
1115        // Test wildcard match
1116        let wildcard_response = ElifResponse::ok()
1117            .with_text("Test content")
1118            .conditional(Some("*"));
1119        assert_eq!(
1120            wildcard_response.status_code(),
1121            ElifStatusCode::NOT_MODIFIED
1122        );
1123    }
1124}