elif_http/response/
builder.rs

1//! Laravel-style Response Builder
2//! 
3//! Provides a fluent builder pattern for creating HTTP responses with intuitive chaining.
4//! 
5//! # Examples
6//! 
7//! ```rust,no_run
8//! use elif_http::response::response;
9//! use elif_http::{HttpResult, ElifResponse};
10//! 
11//! // Clean Laravel-style syntax with terminal methods
12//! async fn list_users() -> HttpResult<ElifResponse> {
13//!     let users = vec!["Alice", "Bob"];
14//!     response().json(users).send()
15//! }
16//! 
17//! async fn create_user() -> HttpResult<ElifResponse> {
18//!     let user = serde_json::json!({"id": 1, "name": "Alice"});
19//!     response().json(user).created().location("/users/1").send()
20//! }
21//! 
22//! async fn redirect_user() -> HttpResult<ElifResponse> {
23//!     response().redirect("/login").permanent().send()
24//! }
25//! 
26//! // Alternative: using .finish() or traditional Ok(.into())
27//! async fn other_examples() -> HttpResult<ElifResponse> {
28//!     // Using finish()
29//!     response().json("data").finish()
30//!     
31//!     // Traditional approach still works
32//!     // Ok(response().json("data").into())
33//! }
34//! ```
35
36use crate::response::{ElifResponse, ElifStatusCode, ResponseBody};
37use crate::errors::HttpResult;
38use serde::Serialize;
39use axum::body::Bytes;
40use tracing;
41
42/// Response builder for fluent API construction
43/// 
44/// This struct provides a Laravel-inspired builder pattern for creating HTTP responses.
45/// All methods return Self for chaining, and the builder converts to ElifResponse automatically.
46#[derive(Debug)]
47pub struct ResponseBuilder {
48    status: Option<ElifStatusCode>,
49    headers: Vec<(String, String)>,
50    body: Option<ResponseBody>,
51}
52
53impl ResponseBuilder {
54    /// Create new response builder
55    pub fn new() -> Self {
56        Self {
57            status: None,
58            headers: Vec::new(),
59            body: None,
60        }
61    }
62
63    // Status Code Helpers
64
65    /// Set status to 200 OK
66    pub fn ok(mut self) -> Self {
67        self.status = Some(ElifStatusCode::OK);
68        self
69    }
70
71    /// Set status to 201 Created
72    pub fn created(mut self) -> Self {
73        self.status = Some(ElifStatusCode::CREATED);
74        self
75    }
76
77    /// Set status to 202 Accepted  
78    pub fn accepted(mut self) -> Self {
79        self.status = Some(ElifStatusCode::ACCEPTED);
80        self
81    }
82
83    /// Set status to 204 No Content
84    pub fn no_content(mut self) -> Self {
85        self.status = Some(ElifStatusCode::NO_CONTENT);
86        self
87    }
88
89    /// Set status to 400 Bad Request
90    pub fn bad_request(mut self) -> Self {
91        self.status = Some(ElifStatusCode::BAD_REQUEST);
92        self
93    }
94
95    /// Set status to 401 Unauthorized
96    pub fn unauthorized(mut self) -> Self {
97        self.status = Some(ElifStatusCode::UNAUTHORIZED);
98        self
99    }
100
101    /// Set status to 403 Forbidden
102    pub fn forbidden(mut self) -> Self {
103        self.status = Some(ElifStatusCode::FORBIDDEN);
104        self
105    }
106
107    /// Set status to 404 Not Found
108    pub fn not_found(mut self) -> Self {
109        self.status = Some(ElifStatusCode::NOT_FOUND);
110        self
111    }
112
113    /// Set status to 422 Unprocessable Entity
114    pub fn unprocessable_entity(mut self) -> Self {
115        self.status = Some(ElifStatusCode::UNPROCESSABLE_ENTITY);
116        self
117    }
118
119    /// Set status to 500 Internal Server Error
120    pub fn internal_server_error(mut self) -> Self {
121        self.status = Some(ElifStatusCode::INTERNAL_SERVER_ERROR);
122        self
123    }
124
125    /// Set custom status code
126    pub fn status(mut self, status: ElifStatusCode) -> Self {
127        self.status = Some(status);
128        self
129    }
130
131    // Content Helpers
132
133    /// Set JSON body with automatic content-type
134    pub fn json<T: Serialize>(mut self, data: T) -> Self {
135        match serde_json::to_value(&data) {
136            Ok(value) => {
137                self.body = Some(ResponseBody::Json(value));
138                self.headers.push(("content-type".to_string(), "application/json".to_string()));
139                self
140            }
141            Err(err) => {
142                // Log the serialization error for easier debugging
143                tracing::error!("JSON serialization failed: {}", err);
144                // Fallback to error response
145                self.status = Some(ElifStatusCode::INTERNAL_SERVER_ERROR);
146                self.body = Some(ResponseBody::Text(format!("JSON serialization failed: {}", err)));
147                self
148            }
149        }
150    }
151
152    /// Set text body
153    pub fn text<S: Into<String>>(mut self, text: S) -> Self {
154        self.body = Some(ResponseBody::Text(text.into()));
155        self.headers.push(("content-type".to_string(), "text/plain; charset=utf-8".to_string()));
156        self
157    }
158
159    /// Set HTML body
160    pub fn html<S: Into<String>>(mut self, html: S) -> Self {
161        self.body = Some(ResponseBody::Text(html.into()));
162        self.headers.push(("content-type".to_string(), "text/html; charset=utf-8".to_string()));
163        self
164    }
165
166    /// Set binary body
167    pub fn bytes(mut self, bytes: Bytes) -> Self {
168        self.body = Some(ResponseBody::Bytes(bytes));
169        self
170    }
171
172    // Redirect Helpers
173
174    /// Create redirect response
175    pub fn redirect<S: Into<String>>(mut self, location: S) -> Self {
176        self.headers.push(("location".to_string(), location.into()));
177        if self.status.is_none() {
178            self.status = Some(ElifStatusCode::FOUND);
179        }
180        self
181    }
182
183    /// Set redirect as permanent (301)
184    pub fn permanent(mut self) -> Self {
185        self.status = Some(ElifStatusCode::MOVED_PERMANENTLY);
186        self
187    }
188
189    /// Set redirect as temporary (302) - default
190    pub fn temporary(mut self) -> Self {
191        self.status = Some(ElifStatusCode::FOUND);
192        self
193    }
194
195    // Header Helpers
196
197    /// Add custom header
198    pub fn header<K, V>(mut self, key: K, value: V) -> Self
199    where
200        K: Into<String>,
201        V: Into<String>,
202    {
203        self.headers.push((key.into(), value.into()));
204        self
205    }
206
207    /// Set location header
208    pub fn location<S: Into<String>>(mut self, url: S) -> Self {
209        self.headers.push(("location".to_string(), url.into()));
210        self
211    }
212
213    /// Set cache-control header
214    pub fn cache_control<S: Into<String>>(mut self, value: S) -> Self {
215        self.headers.push(("cache-control".to_string(), value.into()));
216        self
217    }
218
219    /// Set content-type header
220    pub fn content_type<S: Into<String>>(mut self, content_type: S) -> Self {
221        self.headers.push(("content-type".to_string(), content_type.into()));
222        self
223    }
224
225    /// Add a cookie header (supports multiple cookies)
226    pub fn cookie<S: Into<String>>(mut self, cookie_value: S) -> Self {
227        self.headers.push(("set-cookie".to_string(), cookie_value.into()));
228        self
229    }
230
231    // Error Response Helpers
232
233    /// Create error response with message
234    pub fn error<S: Into<String>>(mut self, message: S) -> Self {
235        let error_data = serde_json::json!({
236            "error": {
237                "message": message.into(),
238                "timestamp": chrono::Utc::now().to_rfc3339()
239            }
240        });
241        
242        self.body = Some(ResponseBody::Json(error_data));
243        self.headers.push(("content-type".to_string(), "application/json".to_string()));
244        self
245    }
246
247    /// Create validation error response
248    pub fn validation_error<T: Serialize>(mut self, errors: T) -> Self {
249        let error_data = serde_json::json!({
250            "error": {
251                "type": "validation",
252                "details": errors,
253                "timestamp": chrono::Utc::now().to_rfc3339()
254            }
255        });
256        
257        self.body = Some(ResponseBody::Json(error_data));
258        self.headers.push(("content-type".to_string(), "application/json".to_string()));
259        if self.status.is_none() {
260            self.status = Some(ElifStatusCode::UNPROCESSABLE_ENTITY);
261        }
262        self
263    }
264
265    /// Create not found error with custom message
266    pub fn not_found_with_message<S: Into<String>>(mut self, message: S) -> Self {
267        let error_data = serde_json::json!({
268            "error": {
269                "type": "not_found",
270                "message": message.into(),
271                "timestamp": chrono::Utc::now().to_rfc3339()
272            }
273        });
274        
275        self.body = Some(ResponseBody::Json(error_data));
276        self.headers.push(("content-type".to_string(), "application/json".to_string()));
277        self.status = Some(ElifStatusCode::NOT_FOUND);
278        self
279    }
280
281    // CORS Helpers
282
283    /// Add CORS headers
284    pub fn cors(mut self, origin: &str) -> Self {
285        self.headers.push(("access-control-allow-origin".to_string(), origin.to_string()));
286        self
287    }
288
289    /// Add CORS headers with credentials
290    pub fn cors_with_credentials(mut self, origin: &str) -> Self {
291        self.headers.push(("access-control-allow-origin".to_string(), origin.to_string()));
292        self.headers.push(("access-control-allow-credentials".to_string(), "true".to_string()));
293        self
294    }
295
296    // Security Helpers
297
298    /// Add security headers
299    pub fn with_security_headers(mut self) -> Self {
300        self.headers.extend([
301            ("x-content-type-options".to_string(), "nosniff".to_string()),
302            ("x-frame-options".to_string(), "DENY".to_string()),
303            ("x-xss-protection".to_string(), "1; mode=block".to_string()),
304            ("referrer-policy".to_string(), "strict-origin-when-cross-origin".to_string()),
305        ]);
306        self
307    }
308
309    // Terminal Methods (convert to Result)
310
311    /// Build and return the response wrapped in Ok()
312    /// 
313    /// This enables Laravel-style terminal chaining: response().json(data).send()
314    /// Alternative to: Ok(response().json(data).into())
315    pub fn send(self) -> HttpResult<ElifResponse> {
316        Ok(self.build())
317    }
318
319    /// Build and return the response wrapped in Ok() - alias for send()
320    /// 
321    /// This enables Laravel-style terminal chaining: response().json(data).finish()
322    pub fn finish(self) -> HttpResult<ElifResponse> {
323        Ok(self.build())
324    }
325
326    /// Build the final ElifResponse
327    pub fn build(self) -> ElifResponse {
328        let mut response = ElifResponse::new();
329
330        // Set status
331        if let Some(status) = self.status {
332            response = response.status(status);
333        }
334
335        // Check if we have body types that auto-set content-type
336        let body_sets_content_type = matches!(
337            self.body, 
338            Some(ResponseBody::Json(_)) | Some(ResponseBody::Text(_))
339        );
340
341        // Set body
342        if let Some(body) = self.body {
343            match body {
344                ResponseBody::Empty => {},
345                ResponseBody::Text(text) => {
346                    response = response.text(text);
347                }
348                ResponseBody::Bytes(bytes) => {
349                    response = response.bytes(bytes);
350                }
351                ResponseBody::Json(value) => {
352                    response = response.json_value(value);
353                }
354            }
355        }
356
357        // Add headers (skip content-type if already set by body methods)
358        let has_explicit_content_type = self.headers.iter()
359            .any(|(k, _)| k.to_lowercase() == "content-type");
360
361        for (key, value) in self.headers {
362            // Skip content-type headers added by json/text/html if body methods already set it
363            if key.to_lowercase() == "content-type" && 
364               body_sets_content_type &&
365               !has_explicit_content_type {
366                continue;
367            }
368            
369            if let (Ok(name), Ok(val)) = (
370                crate::response::ElifHeaderName::from_str(&key),
371                crate::response::ElifHeaderValue::from_str(&value)
372            ) {
373                // Use append instead of insert to support multi-value headers like Set-Cookie
374                response.headers_mut().append(name, val);
375            } else {
376                return ElifResponse::internal_server_error();
377            }
378        }
379
380        response
381    }
382}
383
384impl Default for ResponseBuilder {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390/// Convert ResponseBuilder to ElifResponse
391impl From<ResponseBuilder> for ElifResponse {
392    fn from(builder: ResponseBuilder) -> Self {
393        builder.build()
394    }
395}
396
397/// Global response helper function
398/// 
399/// Creates a new ResponseBuilder for fluent response construction.
400/// This is the main entry point for the Laravel-style response API.
401/// 
402/// # Examples
403/// 
404/// ```rust,no_run
405/// use elif_http::response::response;
406/// use serde_json::json;
407/// 
408/// // Basic usage
409/// let users = vec!["Alice", "Bob"];
410/// let resp = response().json(users);
411/// let resp = response().text("Hello World").ok();
412/// let resp = response().redirect("/login");
413/// 
414/// // Complex chaining
415/// let user_data = json!({"id": 1, "name": "Alice"});
416/// let resp = response()
417///     .json(user_data)
418///     .created()
419///     .location("/users/123")
420///     .cache_control("no-cache");
421/// ```
422pub fn response() -> ResponseBuilder {
423    ResponseBuilder::new()
424}
425
426/// Global JSON response helper
427/// 
428/// Creates a ResponseBuilder with JSON data already set.
429pub fn json_response<T: Serialize>(data: T) -> ResponseBuilder {
430    response().json(data)
431}
432
433/// Global text response helper
434/// 
435/// Creates a ResponseBuilder with text content already set.
436pub fn text_response<S: Into<String>>(content: S) -> ResponseBuilder {
437    response().text(content)
438}
439
440/// Global redirect response helper
441/// 
442/// Creates a ResponseBuilder with redirect location already set.
443pub fn redirect_response<S: Into<String>>(location: S) -> ResponseBuilder {
444    response().redirect(location)
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use serde_json::json;
451
452    #[test]
453    fn test_basic_response_builder() {
454        let resp: ElifResponse = response().text("Hello World").ok().into();
455        assert_eq!(resp.status_code(), ElifStatusCode::OK);
456    }
457
458    #[test]
459    fn test_json_response() {
460        let data = json!({"name": "Alice", "age": 30});
461        let resp: ElifResponse = response().json(data).into();
462        assert_eq!(resp.status_code(), ElifStatusCode::OK);
463    }
464
465    #[test]
466    fn test_status_helpers() {
467        let resp: ElifResponse = response().text("Created").created().into();
468        assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
469
470        let resp: ElifResponse = response().text("Not Found").not_found().into();
471        assert_eq!(resp.status_code(), ElifStatusCode::NOT_FOUND);
472    }
473
474    #[test]
475    fn test_redirect_helpers() {
476        let resp: ElifResponse = response().redirect("/login").into();
477        assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
478
479        let resp: ElifResponse = response().redirect("/users").permanent().into();
480        assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
481    }
482
483    #[test]
484    fn test_redirect_method_call_order_independence() {
485        // Test that permanent() works regardless of call order
486        
487        // Order 1: redirect first, then permanent
488        let resp1: ElifResponse = response().redirect("/test").permanent().into();
489        assert_eq!(resp1.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
490        assert!(resp1.has_header("location"));
491        
492        // Order 2: permanent first, then redirect  
493        let resp2: ElifResponse = response().permanent().redirect("/test").into();
494        assert_eq!(resp2.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
495        assert!(resp2.has_header("location"));
496        
497        // Both should have the same final status
498        assert_eq!(resp1.status_code(), resp2.status_code());
499    }
500
501    #[test]
502    fn test_temporary_method_call_order_independence() {
503        // Test that temporary() works regardless of call order
504        
505        // Order 1: redirect first, then temporary
506        let resp1: ElifResponse = response().redirect("/test").temporary().into();
507        assert_eq!(resp1.status_code(), ElifStatusCode::FOUND);
508        assert!(resp1.has_header("location"));
509        
510        // Order 2: temporary first, then redirect
511        let resp2: ElifResponse = response().temporary().redirect("/test").into();
512        assert_eq!(resp2.status_code(), ElifStatusCode::FOUND);
513        assert!(resp2.has_header("location"));
514        
515        // Both should have the same final status
516        assert_eq!(resp1.status_code(), resp2.status_code());
517    }
518
519    #[test]
520    fn test_redirect_status_override_behavior() {
521        // Test that redirect() respects pre-set status codes
522        
523        // Default redirect (should be 302 FOUND)
524        let resp: ElifResponse = response().redirect("/default").into();
525        assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
526        
527        // Pre-set permanent status should be preserved
528        let resp: ElifResponse = response().permanent().redirect("/perm").into();  
529        assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
530        
531        // Pre-set temporary status should be preserved
532        let resp: ElifResponse = response().temporary().redirect("/temp").into();
533        assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
534        
535        // Last status wins (permanent overrides default)
536        let resp: ElifResponse = response().redirect("/test").permanent().into();
537        assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
538        
539        // Last status wins (temporary overrides permanent) 
540        let resp: ElifResponse = response().redirect("/test").permanent().temporary().into();
541        assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
542    }
543
544    #[test]
545    fn test_header_chaining() {
546        let resp: ElifResponse = response()
547            .text("Hello")
548            .header("x-custom", "value")
549            .cache_control("no-cache")
550            .into();
551        
552        assert!(resp.has_header("x-custom"));
553        assert!(resp.has_header("cache-control"));
554    }
555
556    #[test]
557    fn test_complex_chaining() {
558        let user_data = json!({"id": 1, "name": "Alice"});
559        let resp: ElifResponse = response()
560            .json(user_data)
561            .created()
562            .location("/users/1")
563            .cache_control("no-cache")
564            .header("x-custom", "test")
565            .into();
566
567        assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
568        assert!(resp.has_header("location"));
569        assert!(resp.has_header("cache-control"));
570        assert!(resp.has_header("x-custom"));
571    }
572
573    #[test]
574    fn test_error_responses() {
575        let resp: ElifResponse = response().error("Something went wrong").internal_server_error().into();
576        assert_eq!(resp.status_code(), ElifStatusCode::INTERNAL_SERVER_ERROR);
577
578        let validation_errors = json!({"email": ["Email is required"]});
579        let resp: ElifResponse = response().validation_error(validation_errors).into();
580        assert_eq!(resp.status_code(), ElifStatusCode::UNPROCESSABLE_ENTITY);
581    }
582
583    #[test]
584    fn test_global_helpers() {
585        let data = json!({"message": "Hello"});
586        let resp: ElifResponse = json_response(data).ok().into();
587        assert_eq!(resp.status_code(), ElifStatusCode::OK);
588
589        let resp: ElifResponse = text_response("Hello World").into();
590        assert_eq!(resp.status_code(), ElifStatusCode::OK);
591
592        let resp: ElifResponse = redirect_response("/home").into();
593        assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
594    }
595
596    #[test]
597    fn test_cors_helpers() {
598        let resp: ElifResponse = response()
599            .json(json!({"data": "test"}))
600            .cors("*")
601            .into();
602        
603        assert!(resp.has_header("access-control-allow-origin"));
604    }
605
606    #[test]
607    fn test_security_headers() {
608        let resp: ElifResponse = response()
609            .text("Secure content")
610            .with_security_headers()
611            .into();
612        
613        assert!(resp.has_header("x-content-type-options"));
614        assert!(resp.has_header("x-frame-options"));
615        assert!(resp.has_header("x-xss-protection"));
616        assert!(resp.has_header("referrer-policy"));
617    }
618
619    #[test]
620    fn test_multi_value_headers() {
621        // Test that multiple headers with the same name are properly appended
622        let resp: ElifResponse = response()
623            .text("Hello")
624            .header("set-cookie", "session=abc123; Path=/")
625            .header("set-cookie", "theme=dark; Path=/")
626            .header("set-cookie", "lang=en; Path=/")
627            .into();
628        
629        // All Set-Cookie headers should be present (append behavior)
630        assert!(resp.has_header("set-cookie"));
631        
632        // Test that we can build the response without errors
633        assert_eq!(resp.status_code(), ElifStatusCode::OK);
634    }
635
636    #[test]
637    fn test_cookie_helper_method() {
638        // Test the convenience cookie method
639        let resp: ElifResponse = response()
640            .json(json!({"user": "alice"}))
641            .cookie("session=12345; HttpOnly; Secure")
642            .cookie("csrf=token123; SameSite=Strict")
643            .cookie("theme=dark; Path=/")
644            .created()
645            .into();
646        
647        assert!(resp.has_header("set-cookie"));
648        assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
649    }
650
651    #[test]
652    fn test_terminal_methods() {
653        // Test .send() terminal method
654        let result: HttpResult<ElifResponse> = response()
655            .json(json!({"data": "test"}))
656            .created()
657            .send();
658        
659        assert!(result.is_ok());
660        let resp = result.unwrap();
661        assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
662
663        // Test .finish() terminal method
664        let result: HttpResult<ElifResponse> = response()
665            .text("Hello World")
666            .cache_control("no-cache")
667            .finish();
668        
669        assert!(result.is_ok());
670        let resp = result.unwrap();
671        assert_eq!(resp.status_code(), ElifStatusCode::OK);
672        assert!(resp.has_header("cache-control"));
673    }
674
675    #[test]
676    fn test_laravel_style_chaining() {
677        // Test the complete Laravel-style chain without Ok() wrapper
678        let result: HttpResult<ElifResponse> = response()
679            .json(json!({"user_id": 123}))
680            .created()
681            .location("/users/123") 
682            .cookie("session=abc123; HttpOnly")
683            .header("x-custom", "value")
684            .send();
685
686        assert!(result.is_ok());
687        let resp = result.unwrap();
688        assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
689        assert!(resp.has_header("location"));
690        assert!(resp.has_header("set-cookie"));
691        assert!(resp.has_header("x-custom"));
692    }
693
694    #[test] 
695    fn test_json_serialization_error_handling() {
696        use std::collections::HashMap;
697        
698        // Test that JSON serialization errors are properly logged and handled
699        // We'll use a structure that can potentially fail serialization
700        
701        // Test with valid data first
702        let valid_data = HashMap::from([("key", "value")]);
703        let resp: ElifResponse = response()
704            .json(valid_data)
705            .into();
706        
707        assert_eq!(resp.status_code(), ElifStatusCode::OK);
708        
709        // For actual serialization errors (which are rare with standard types),
710        // the enhanced error handling now provides:
711        // 1. tracing::error! log with full error context
712        // 2. 500 status code 
713        // 3. Descriptive error message in response body including the actual error
714        // 4. Better debugging experience for developers
715        
716        // The key improvement is that errors are no longer silently ignored
717        // and developers get actionable error information in both logs and response
718    }
719
720    #[test]
721    fn test_header_append_vs_insert_behavior() {
722        // Verify that multiple headers with same name are preserved
723        let resp: ElifResponse = response()
724            .json(json!({"test": "data"}))
725            .header("x-custom", "value1")
726            .header("x-custom", "value2")
727            .header("x-custom", "value3")
728            .into();
729        
730        assert!(resp.has_header("x-custom"));
731        assert_eq!(resp.status_code(), ElifStatusCode::OK);
732        
733        // The response should build successfully with all custom headers
734        // (The exact behavior depends on how we want to handle duplicate non-cookie headers)
735    }
736}