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