ignitia/response/mod.rs
1//! # HTTP Response Generation Module
2//!
3//! This module provides comprehensive HTTP response generation capabilities for the Ignitia web framework.
4//! It includes response building, content type handling, status code management, and error response generation
5//! with efficient serialization and flexible customization options.
6//!
7//! ## Features
8//!
9//! - **Multiple Response Formats**: JSON, HTML, text, and binary response generation
10//! - **Status Code Management**: Easy status code setting with validation
11//! - **Header Management**: Flexible header manipulation and content type handling
12//! - **Error Response Generation**: Automatic error-to-response conversion with structured formats
13//! - **Builder Pattern Support**: Fluent response building with method chaining
14//! - **Performance Optimized**: Efficient serialization and memory usage
15//!
16//! ## Response Types
17//!
18//! ### JSON Responses
19//! - Automatic serialization with proper content-type headers
20//! - Support for any type implementing `Serialize`
21//! - Comprehensive error handling for serialization failures
22//!
23//! ### HTML Responses
24//! - Proper content-type headers with charset specification
25//! - Template integration support
26//! - XSS protection considerations
27//!
28//! ### Text Responses
29//! - UTF-8 encoded plain text with proper headers
30//! - Support for various text formats
31//!
32//! ### Binary Responses
33//! - Efficient handling of binary data
34//! - Flexible content-type specification
35//! - Support for file downloads and streaming
36//!
37//! ## Usage Examples
38//!
39//! ### Basic Response Creation
40//! ```
41//! use ignitia::{Response, Result};
42//! use http::StatusCode;
43//!
44//! // Simple text response
45//! async fn hello_handler() -> Result<Response> {
46//! Ok(Response::text("Hello, World!"))
47//! }
48//!
49//! // JSON response
50//! async fn json_handler() -> Result<Response> {
51//! let data = serde_json::json!({
52//! "message": "Hello, World!",
53//! "timestamp": chrono::Utc::now()
54//! });
55//! Response::json(data)
56//! }
57//!
58//! // HTML response
59//! async fn html_handler() -> Result<Response> {
60//! let html = r#"
61//! <!DOCTYPE html>
62//! <html>
63//! <head><title>Hello</title></head>
64//! <body><h1>Hello, World!</h1></body>
65//! </html>
66//! "#;
67//! Ok(Response::html(html))
68//! }
69//! ```
70//!
71//! ### Custom Status Codes
72//! ```
73//! use ignitia::Response;
74//! use http::StatusCode;
75//!
76//! async fn custom_status_handler() -> ignitia::Result<Response> {
77//! Ok(Response::text("Created successfully")
78//! .with_status(StatusCode::CREATED))
79//! }
80//!
81//! async fn not_found_handler() -> ignitia::Result<Response> {
82//! Ok(Response::text("Resource not found")
83//! .with_status_code(404))
84//! }
85//! ```
86//!
87//! ### Working with Headers
88//! ```
89//! use ignitia::Response;
90//! use http::{HeaderMap, HeaderName, HeaderValue};
91//!
92//! async fn custom_headers_handler() -> ignitia::Result<Response> {
93//! let mut response = Response::text("Custom headers example");
94//!
95//! // Add custom headers
96//! response.headers.insert(
97//! HeaderName::from_static("x-custom-header"),
98//! HeaderValue::from_static("custom-value")
99//! );
100//!
101//! response.headers.insert(
102//! HeaderName::from_static("cache-control"),
103//! HeaderValue::from_static("no-cache, no-store, must-revalidate")
104//! );
105//!
106//! Ok(response)
107//! }
108//! ```
109//!
110//! ## Advanced Usage
111//!
112//! ### API Response Patterns
113//! ```
114//! use ignitia::{Response, Result};
115//! use serde::{Deserialize, Serialize};
116//! use http::StatusCode;
117//!
118//! #[derive(Serialize)]
119//! struct ApiResponse<T> {
120//! success: bool,
121//! data: Option<T>,
122//! message: String,
123//! timestamp: String,
124//! }
125//!
126//! #[derive(Serialize)]
127//! struct User {
128//! id: u32,
129//! name: String,
130//! email: String,
131//! }
132//!
133//! async fn get_user_handler() -> Result<Response> {
134//! let user = User {
135//! id: 1,
136//! name: "Alice".to_string(),
137//! email: "alice@example.com".to_string(),
138//! };
139//!
140//! let api_response = ApiResponse {
141//! success: true,
142//! data: Some(user),
143//! message: "User retrieved successfully".to_string(),
144//! timestamp: chrono::Utc::now().to_rfc3339(),
145//! };
146//!
147//! Response::json(api_response)
148//! }
149//!
150//! async fn user_not_found_handler() -> Result<Response> {
151//! let api_response = ApiResponse::<()> {
152//! success: false,
153//! data: None,
154//! message: "User not found".to_string(),
155//! timestamp: chrono::Utc::now().to_rfc3339(),
156//! };
157//!
158//! let mut response = Response::json(api_response)?;
159//! response.status = StatusCode::NOT_FOUND;
160//! Ok(response)
161//! }
162//! ```
163//!
164//! ### File Download Responses
165//! ```
166//! use ignitia::Response;
167//! use bytes::Bytes;
168//! use http::{HeaderName, HeaderValue};
169//!
170//! async fn download_handler() -> ignitia::Result<Response> {
171//! // Simulate file content
172//! let file_content = b"Hello, this is a downloadable file!";
173//! let filename = "example.txt";
174//!
175//! let mut response = Response::new(http::StatusCode::OK);
176//! response.body = Bytes::from(&file_content[..]);
177//!
178//! // Set appropriate headers for file download
179//! response.headers.insert(
180//! HeaderName::from_static("content-type"),
181//! HeaderValue::from_static("application/octet-stream")
182//! );
183//!
184//! response.headers.insert(
185//! HeaderName::from_static("content-disposition"),
186//! HeaderValue::from_str(&format!("attachment; filename=\"{}\"", filename))
187//! .map_err(|_| ignitia::Error::Internal("Invalid filename".into()))?
188//! );
189//!
190//! response.headers.insert(
191//! HeaderName::from_static("content-length"),
192//! HeaderValue::from_str(&file_content.len().to_string())
193//! .map_err(|_| ignitia::Error::Internal("Invalid content length".into()))?
194//! );
195//!
196//! Ok(response)
197//! }
198//! ```
199//!
200//! ### Streaming Responses
201//! ```
202//! use ignitia::Response;
203//! use bytes::Bytes;
204//! use http::{HeaderName, HeaderValue};
205//!
206//! async fn streaming_handler() -> ignitia::Result<Response> {
207//! // Server-Sent Events example
208//! let event_data = "data: Hello from server!\n\n";
209//!
210//! let mut response = Response::new(http::StatusCode::OK);
211//! response.body = Bytes::from(event_data);
212//!
213//! response.headers.insert(
214//! HeaderName::from_static("content-type"),
215//! HeaderValue::from_static("text/event-stream")
216//! );
217//!
218//! response.headers.insert(
219//! HeaderName::from_static("cache-control"),
220//! HeaderValue::from_static("no-cache")
221//! );
222//!
223//! response.headers.insert(
224//! HeaderName::from_static("connection"),
225//! HeaderValue::from_static("keep-alive")
226//! );
227//!
228//! Ok(response)
229//! }
230//! ```
231//!
232//! ## Error Response Handling
233//!
234//! ### Automatic Error Conversion
235//! ```
236//! use ignitia::{Response, Error, Result};
237//! use http::StatusCode;
238//!
239//! async fn error_example_handler() -> Result<Response> {
240//! // This will automatically be converted to an error response
241//! Err(Error::NotFound("User not found".to_string()))
242//! }
243//!
244//! // The framework automatically converts errors to responses:
245//! // {
246//! // "error": "Not Found",
247//! // "message": "User not found",
248//! // "status": 404,
249//! // "error_type": "not_found",
250//! // "timestamp": "2023-01-01T12:00:00Z"
251//! // }
252//! ```
253//!
254//! ### Custom Error Responses
255//! ```
256//! use ignitia::{Response, Error, Result};
257//! use serde_json::json;
258//!
259//! async fn custom_error_handler() -> Result<Response> {
260//! let error_messages = vec![
261//! "Name is required".to_string(),
262//! "Email format is invalid".to_string(),
263//! ];
264//!
265//! Response::validation_error(error_messages)
266//! }
267//!
268//! async fn api_error_handler() -> Result<Response> {
269//! let error_response = json!({
270//! "error": {
271//! "code": "RATE_LIMITED",
272//! "message": "Too many requests",
273//! "retry_after": 60
274//! }
275//! });
276//!
277//! let mut response = Response::json(error_response)?;
278//! response.status = http::StatusCode::TOO_MANY_REQUESTS;
279//! Ok(response)
280//! }
281//! ```
282//!
283//! ## Performance Considerations
284//!
285//! ### Memory Efficiency
286//! - Uses `bytes::Bytes` for zero-copy operations
287//! - Efficient serialization with pre-allocated buffers
288//! - Minimal header allocation overhead
289//!
290//! ### Serialization Performance
291//! ```
292//! use ignitia::Response;
293//! use serde::Serialize;
294//!
295//! #[derive(Serialize)]
296//! struct LargeData {
297//! items: Vec<String>,
298//! }
299//!
300//! async fn optimized_json_handler() -> ignitia::Result<Response> {
301//! let data = LargeData {
302//! items: (0..1000).map(|i| format!("Item {}", i)).collect(),
303//! };
304//!
305//! // Efficient JSON serialization with error handling
306//! match Response::json(data) {
307//! Ok(response) => Ok(response),
308//! Err(e) => {
309//! tracing::error!("JSON serialization failed: {}", e);
310//! Ok(Response::text("Internal server error")
311//! .with_status(http::StatusCode::INTERNAL_SERVER_ERROR))
312//! }
313//! }
314//! }
315//! ```
316//!
317//! ## Security Considerations
318//!
319//! ### Content Type Security
320//! - Always set appropriate content-type headers
321//! - Validate content before setting as HTML
322//! - Use proper encoding for text responses
323//!
324//! ### XSS Prevention
325//! ```
326//! use ignitia::Response;
327//!
328//! async fn safe_html_handler(user_input: &str) -> ignitia::Result<Response> {
329//! // Escape user input to prevent XSS
330//! let escaped_input = html_escape::encode_text(user_input);
331//!
332//! let safe_html = format!(
333//! r#"<!DOCTYPE html>
334//! <html>
335//! <head><title>Safe Output</title></head>
336//! <body><h1>Hello, {}</h1></body>
337//! </html>"#,
338//! escaped_input
339//! );
340//!
341//! Ok(Response::html(safe_html))
342//! }
343//! ```
344//!
345//! ### Security Headers
346//! ```
347//! use ignitia::Response;
348//! use http::{HeaderName, HeaderValue};
349//!
350//! async fn secure_response_handler() -> ignitia::Result<Response> {
351//! let mut response = Response::html("<h1>Secure Page</h1>");
352//!
353//! // Add security headers
354//! response.headers.insert(
355//! HeaderName::from_static("x-content-type-options"),
356//! HeaderValue::from_static("nosniff")
357//! );
358//!
359//! response.headers.insert(
360//! HeaderName::from_static("x-frame-options"),
361//! HeaderValue::from_static("DENY")
362//! );
363//!
364//! response.headers.insert(
365//! HeaderName::from_static("x-xss-protection"),
366//! HeaderValue::from_static("1; mode=block")
367//! );
368//!
369//! response.headers.insert(
370//! HeaderName::from_static("content-security-policy"),
371//! HeaderValue::from_static("default-src 'self'")
372//! );
373//!
374//! Ok(response)
375//! }
376//! ```
377//!
378//! ## Testing Response Generation
379//!
380//! ### Unit Testing
381//! ```
382//! #[cfg(test)]
383//! mod tests {
384//! use super::*;
385//! use http::StatusCode;
386//!
387//! #[tokio::test]
388//! async fn test_text_response() {
389//! let response = Response::text("Hello, World!");
390//!
391//! assert_eq!(response.status, StatusCode::OK);
392//! assert_eq!(
393//! response.headers.get("content-type").unwrap(),
394//! "text/plain; charset=utf-8"
395//! );
396//! assert_eq!(response.body, "Hello, World!");
397//! }
398//!
399//! #[tokio::test]
400//! async fn test_json_response() {
401//! let data = serde_json::json!({"message": "test"});
402//! let response = Response::json(data).unwrap();
403//!
404//! assert_eq!(response.status, StatusCode::OK);
405//! assert_eq!(
406//! response.headers.get("content-type").unwrap(),
407//! "application/json"
408//! );
409//! }
410//!
411//! #[tokio::test]
412//! async fn test_error_conversion() {
413//! let error = ignitia::Error::NotFound("test".to_string());
414//! let response = Response::from(error);
415//!
416//! assert_eq!(response.status, StatusCode::NOT_FOUND);
417//! }
418//! }
419//! ```
420
421pub mod builder;
422pub mod status;
423
424pub mod into_response;
425
426// Re-export IntoResponse
427pub use into_response::{Html, IntoResponse};
428
429use bytes::Bytes;
430use http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode};
431use serde::Serialize;
432
433/// HTTP response representation containing status, headers, and body.
434///
435/// The `Response` struct encapsulates all data needed to send an HTTP response,
436/// including the status code, headers, and body content. It provides convenient
437/// methods for creating responses with different content types and formats.
438///
439/// # Structure
440/// - **status**: HTTP status code (200, 404, 500, etc.)
441/// - **headers**: HTTP headers as a HeaderMap
442/// - **body**: Response body as bytes
443///
444/// # Examples
445///
446/// ## Basic Response Creation
447/// ```
448/// use ignitia::Response;
449/// use http::StatusCode;
450///
451/// // Create a simple text response
452/// let response = Response::text("Hello, World!");
453/// assert_eq!(response.status, StatusCode::OK);
454///
455/// // Create a response with custom status
456/// let response = Response::new(StatusCode::CREATED);
457/// ```
458///
459/// ## JSON Responses
460/// ```
461/// use ignitia::Response;
462/// use serde_json::json;
463///
464/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
465/// let data = json!({
466/// "message": "Success",
467/// "data": {
468/// "id": 123,
469/// "name": "Example"
470/// }
471/// });
472///
473/// let response = Response::json(data)?;
474/// # Ok(())
475/// # }
476/// ```
477///
478/// ## HTML Responses
479/// ```
480/// use ignitia::Response;
481///
482/// let html = r#"
483/// <!DOCTYPE html>
484/// <html>
485/// <head><title>Hello</title></head>
486/// <body><h1>Hello, World!</h1></body>
487/// </html>
488/// "#;
489///
490/// let response = Response::html(html);
491/// ```
492#[derive(Debug, Clone)]
493pub struct Response {
494 /// HTTP status code
495 pub status: StatusCode,
496 /// HTTP response headers
497 pub headers: HeaderMap,
498 /// Response body as bytes
499 pub body: Bytes,
500 /// Cache control information
501 pub cache_control: Option<CacheControl>, // Add this field
502}
503
504/// HTTP cache control configuration for optimizing response caching.
505///
506/// The `CacheControl` struct encapsulates caching metadata used to optimize
507/// HTTP response delivery through strategic cache management. It provides
508/// fine-grained control over cache behavior including cache duration and
509/// cache key generation for efficient content delivery.
510///
511/// # Purpose
512///
513/// This struct enables:
514/// - **Cache Duration Control**: Setting appropriate cache lifetimes via `max_age`
515/// - **Cache Key Management**: Generating unique identifiers for cached content
516/// - **Performance Optimization**: Reducing server load through intelligent caching
517/// - **CDN Integration**: Supporting Content Delivery Network caching strategies
518///
519/// # Cache Strategy
520///
521/// The cache control system implements a dual-approach strategy:
522/// 1. **Time-based Expiration**: Uses `max_age` for cache lifetime management
523/// 2. **Content-based Invalidation**: Uses `key` for cache versioning and invalidation
524///
525/// # Integration with Response
526///
527/// When attached to a `Response`, the `CacheControl` struct automatically:
528/// - Sets appropriate HTTP cache headers (`Cache-Control`, `ETag`, etc.)
529/// - Generates cache keys for storage systems
530/// - Enables conditional requests (304 Not Modified responses)
531/// - Supports cache invalidation strategies
532///
533/// # Examples
534///
535/// ## Basic Cache Control
536/// ```
537/// use ignitia::{Response, CacheControl};
538///
539/// let cache_control = CacheControl {
540/// max_age: 3600, // 1 hour
541/// key: "user_profile_123".to_string(),
542/// };
543///
544/// let response = Response::json(user_data)?
545/// .with_cache_control(cache_control);
546/// ```
547///
548/// ## Static Asset Caching
549/// ```
550/// // Long-term caching for static assets
551/// let static_cache = CacheControl {
552/// max_age: 31536000, // 1 year
553/// key: format!("static_{}_{}", filename, version_hash),
554/// };
555/// ```
556///
557/// ## API Response Caching
558/// ```
559/// // Short-term caching for API responses
560/// let api_cache = CacheControl {
561/// max_age: 300, // 5 minutes
562/// key: format!("api_{}_{}_{}", endpoint, user_id, timestamp),
563/// };
564/// ```
565///
566/// ## Dynamic Content Caching
567/// ```
568/// // User-specific content with medium cache duration
569/// let user_cache = CacheControl {
570/// max_age: 1800, // 30 minutes
571/// key: format!("content_{}_{}", content_id, last_modified),
572/// };
573/// ```
574#[derive(Debug, Clone)]
575pub struct CacheControl {
576 /// Maximum age for cached content in seconds.
577 ///
578 /// This field determines how long the content should be considered fresh
579 /// by browsers, CDNs, and intermediate caches. The value directly maps
580 /// to the HTTP `Cache-Control: max-age=` directive.
581 ///
582 /// # Common Values
583 /// - **0**: No caching (always revalidate)
584 /// - **300**: 5 minutes (dynamic API responses)
585 /// - **3600**: 1 hour (semi-static content)
586 /// - **86400**: 24 hours (daily updated content)
587 /// - **31536000**: 1 year (static assets with versioning)
588 ///
589 /// # Performance Considerations
590 /// - Longer cache times reduce server load but may serve stale content
591 /// - Shorter cache times ensure freshness but increase server requests
592 /// - Consider content update frequency when setting values
593 ///
594 /// # Examples
595 /// ```
596 /// // No caching for sensitive data
597 /// let sensitive = CacheControl { max_age: 0, key: "...".to_string() };
598 ///
599 /// // Medium caching for API responses
600 /// let api = CacheControl { max_age: 600, key: "...".to_string() };
601 ///
602 /// // Long caching for static assets
603 /// let static_content = CacheControl { max_age: 2592000, key: "...".to_string() };
604 /// ```
605 pub max_age: u64,
606
607 /// Unique identifier for cache entry management and invalidation.
608 ///
609 /// The cache key serves multiple purposes in the caching infrastructure:
610 /// - **Uniqueness**: Ensures different content versions are cached separately
611 /// - **Invalidation**: Enables targeted cache clearing when content changes
612 /// - **Versioning**: Supports content versioning through key changes
613 /// - **Debugging**: Provides identifiable cache entries for troubleshooting
614 ///
615 /// # Key Generation Strategies
616 ///
617 /// ## Content-Based Keys
618 /// Include content identifiers that change when content changes:
619 /// ```
620 /// let key = format!("article_{}_{}", article_id, last_modified_timestamp);
621 /// ```
622 ///
623 /// ## User-Specific Keys
624 /// Include user context for personalized content:
625 /// ```
626 /// let key = format!("dashboard_{}_{}_{}", user_id, role, preferences_hash);
627 /// ```
628 ///
629 /// ## Version-Based Keys
630 /// Include application or content version for cache busting:
631 /// ```
632 /// let key = format!("api_response_{}_v{}", endpoint, api_version);
633 /// ```
634 ///
635 /// ## Hierarchical Keys
636 /// Use hierarchical structure for organized cache management:
637 /// ```
638 /// let key = format!("app:{}:user:{}:page:{}", app_version, user_id, page_id);
639 /// ```
640 ///
641 /// # Best Practices
642 /// - Include all relevant context that affects content
643 /// - Use consistent naming conventions across the application
644 /// - Include version or timestamp information for automatic invalidation
645 /// - Keep keys reasonably short while maintaining uniqueness
646 /// - Avoid sensitive information in cache keys
647 ///
648 /// # Performance Notes
649 /// - Shorter keys reduce memory overhead in cache systems
650 /// - Consistent key patterns improve cache hit rates
651 /// - Include enough context to avoid cache collisions
652 /// - Consider key distribution for cache sharding strategies
653 pub key: String,
654}
655
656impl Response {
657 /// Creates a new Response with the specified status code.
658 ///
659 /// This is the basic constructor for Response instances. It creates a response
660 /// with the given status code, empty headers, and an empty body.
661 ///
662 /// # Parameters
663 /// - `status`: The HTTP status code for the response
664 ///
665 /// # Returns
666 /// A new Response instance
667 ///
668 /// # Examples
669 /// ```
670 /// use ignitia::Response;
671 /// use http::StatusCode;
672 ///
673 /// let response = Response::new(StatusCode::OK);
674 /// assert_eq!(response.status, StatusCode::OK);
675 /// assert!(response.body.is_empty());
676 ///
677 /// let error_response = Response::new(StatusCode::INTERNAL_SERVER_ERROR);
678 /// assert_eq!(error_response.status, StatusCode::INTERNAL_SERVER_ERROR);
679 /// ```
680 #[inline]
681 pub fn new(status: StatusCode) -> Self {
682 Self {
683 status,
684 headers: HeaderMap::new(),
685 body: Bytes::new(),
686 cache_control: None,
687 }
688 }
689
690 /// Sets the status code of the response (builder pattern).
691 ///
692 /// This method consumes the response and returns it with the new status code,
693 /// enabling fluent method chaining.
694 ///
695 /// # Parameters
696 /// - `status`: The new status code to set
697 ///
698 /// # Returns
699 /// The response with the updated status code
700 ///
701 /// # Examples
702 /// ```
703 /// use ignitia::Response;
704 /// use http::StatusCode;
705 ///
706 /// let response = Response::text("Created successfully")
707 /// .with_status(StatusCode::CREATED);
708 /// assert_eq!(response.status, StatusCode::CREATED);
709 /// ```
710 #[inline]
711 pub fn with_status(mut self, status: StatusCode) -> Self {
712 self.status = status;
713 self
714 }
715
716 /// Sets the status code using a numeric value (builder pattern).
717 ///
718 /// This is a convenience method that accepts a u16 status code and converts
719 /// it to a StatusCode. Invalid status codes are ignored.
720 ///
721 /// # Parameters
722 /// - `status_code`: The numeric status code (e.g., 200, 404, 500)
723 ///
724 /// # Returns
725 /// The response with the updated status code (if valid)
726 ///
727 /// # Examples
728 /// ```
729 /// use ignitia::Response;
730 ///
731 /// let response = Response::text("Not Found")
732 /// .with_status_code(404);
733 /// assert_eq!(response.status.as_u16(), 404);
734 ///
735 /// // Invalid status codes are ignored
736 /// let response = Response::text("Test")
737 /// .with_status_code(9999); // Invalid, ignored
738 /// ```
739 #[inline]
740 pub fn with_status_code(mut self, status_code: u16) -> Self {
741 if let Ok(status) = StatusCode::from_u16(status_code) {
742 self.status = status;
743 }
744 self
745 }
746
747 /// Sets the response body (builder pattern).
748 ///
749 /// This method accepts any type that can be converted to `Bytes` and sets
750 /// it as the response body.
751 ///
752 /// # Parameters
753 /// - `body`: The body content (String, &str, Vec<u8>, Bytes, etc.)
754 ///
755 /// # Returns
756 /// The response with the updated body
757 ///
758 /// # Examples
759 /// ```
760 /// use ignitia::Response;
761 /// use bytes::Bytes;
762 ///
763 /// // From string
764 /// let response = Response::new(http::StatusCode::OK)
765 /// .with_body("Hello, World!");
766 ///
767 /// // From bytes
768 /// let data = Bytes::from("Binary data");
769 /// let response = Response::new(http::StatusCode::OK)
770 /// .with_body(data);
771 /// ```
772 #[inline]
773 pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
774 self.body = body.into();
775 self
776 }
777
778 /// Returns a shared reference to the response body.
779 ///
780 /// This method returns a shared reference to the response body, allowing
781 /// multiple parts of the application to access the body without cloning it.
782 ///
783 /// # Returns
784 /// A shared reference to the response body
785 ///
786 /// # Examples
787 /// ```
788 /// use ignitia::Response;
789 /// use bytes::Bytes;
790 ///
791 /// let response = Response::new(http::StatusCode::OK)
792 /// .with_body("Hello, World!");
793 ///
794 /// let body = response.body_shared();
795 /// assert_eq!(body.as_ref(), b"Hello, World!");
796 /// ```
797 #[inline]
798 pub fn body_shared(&self) -> &Bytes {
799 &self.body
800 }
801
802 /// Creates a successful response (200 OK).
803 ///
804 /// This is a convenience method for creating responses with a 200 OK status.
805 ///
806 /// # Returns
807 /// A new response with status 200 OK
808 ///
809 /// # Examples
810 /// ```
811 /// use ignitia::Response;
812 /// use http::StatusCode;
813 ///
814 /// let response = Response::ok();
815 /// assert_eq!(response.status, StatusCode::OK);
816 /// ```
817 #[inline]
818 pub fn ok() -> Self {
819 Self::new(StatusCode::OK)
820 }
821
822 /// Creates a not found response (404 Not Found).
823 ///
824 /// This is a convenience method for creating 404 responses.
825 ///
826 /// # Returns
827 /// A new response with status 404 Not Found
828 ///
829 /// # Examples
830 /// ```
831 /// use ignitia::Response;
832 /// use http::StatusCode;
833 ///
834 /// let response = Response::not_found();
835 /// assert_eq!(response.status, StatusCode::NOT_FOUND);
836 /// ```
837 #[inline]
838 pub fn not_found() -> Self {
839 Self::new(StatusCode::NOT_FOUND)
840 }
841
842 /// Creates an internal server error response (500 Internal Server Error).
843 ///
844 /// This is a convenience method for creating 500 error responses.
845 ///
846 /// # Returns
847 /// A new response with status 500 Internal Server Error
848 ///
849 /// # Examples
850 /// ```
851 /// use ignitia::Response;
852 /// use http::StatusCode;
853 ///
854 /// let response = Response::internal_error();
855 /// assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
856 /// ```
857 #[inline]
858 pub fn internal_error() -> Self {
859 Self::new(StatusCode::INTERNAL_SERVER_ERROR)
860 }
861
862 /// Creates a JSON response with automatic serialization.
863 ///
864 /// This method serializes the provided data to JSON and creates a response
865 /// with the appropriate content-type header. It returns a Result because
866 /// serialization can fail.
867 ///
868 /// # Type Parameters
869 /// - `T`: The type to serialize (must implement `Serialize`)
870 ///
871 /// # Parameters
872 /// - `data`: The data to serialize as JSON
873 ///
874 /// # Returns
875 /// - `Ok(Response)`: Successfully created JSON response
876 /// - `Err(Error)`: JSON serialization error
877 ///
878 /// # Examples
879 /// ```
880 /// use ignitia::Response;
881 /// use serde::Serialize;
882 /// use serde_json::json;
883 ///
884 /// #[derive(Serialize)]
885 /// struct ApiResponse {
886 /// success: bool,
887 /// message: String,
888 /// }
889 ///
890 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
891 /// // With custom struct
892 /// let data = ApiResponse {
893 /// success: true,
894 /// message: "Operation completed".to_string(),
895 /// };
896 /// let response = Response::json(data)?;
897 ///
898 /// // With serde_json::Value
899 /// let data = json!({
900 /// "users": [
901 /// {"id": 1, "name": "Alice"},
902 /// {"id": 2, "name": "Bob"}
903 /// ],
904 /// "total": 2
905 /// });
906 /// let response = Response::json(data)?;
907 /// # Ok(())
908 /// # }
909 /// ```
910 ///
911 /// ## Error Handling
912 /// ```
913 /// use ignitia::Response;
914 /// use serde_json::json;
915 ///
916 /// async fn safe_json_handler() -> ignitia::Result<Response> {
917 /// let data = json!({"key": "value"});
918 ///
919 /// match Response::json(data) {
920 /// Ok(response) => Ok(response),
921 /// Err(e) => {
922 /// tracing::error!("JSON serialization failed: {}", e);
923 /// Ok(Response::text("Internal server error")
924 /// .with_status_code(500))
925 /// }
926 /// }
927 /// }
928 /// ```
929 pub fn json<T: Serialize>(data: T) -> Self {
930 let body = match serde_json::to_vec(&data) {
931 Ok(body) => body,
932 Err(e) => {
933 tracing::error!("JSON serialization failed: {}", e);
934 return Response::text("JSON serialization failed").with_status_code(500);
935 }
936 };
937 let mut response = Self::new(StatusCode::OK);
938 response.headers.insert(
939 HeaderName::from_static("content-type"),
940 HeaderValue::from_static("application/json"),
941 );
942 response.body = Bytes::from(body);
943 response
944 }
945
946 /// Creates a plain text response.
947 ///
948 /// This method creates a response with UTF-8 encoded text and sets the
949 /// appropriate content-type header.
950 ///
951 /// # Parameters
952 /// - `text`: The text content (String, &str, or anything that converts to String)
953 ///
954 /// # Returns
955 /// A new response with the text content
956 ///
957 /// # Examples
958 /// ```
959 /// use ignitia::Response;
960 ///
961 /// let response = Response::text("Hello, World!");
962 /// assert_eq!(
963 /// response.headers.get("content-type").unwrap(),
964 /// "text/plain; charset=utf-8"
965 /// );
966 ///
967 /// let response = Response::text(format!("User ID: {}", 123));
968 /// ```
969 ///
970 /// ## Multi-line Text
971 /// ```
972 /// use ignitia::Response;
973 ///
974 /// let text = r#"
975 /// Line 1
976 /// Line 2
977 /// Line 3
978 /// "#;
979 /// let response = Response::text(text);
980 /// ```
981 #[inline]
982 pub fn text(text: impl Into<String>) -> Self {
983 let mut response = Self::new(StatusCode::OK);
984 response.headers.insert(
985 HeaderName::from_static("content-type"),
986 HeaderValue::from_static("text/plain; charset=utf-8"),
987 );
988 response.body = Bytes::from(text.into());
989 response
990 }
991
992 /// Creates an HTML response.
993 ///
994 /// This method creates a response with HTML content and sets the appropriate
995 /// content-type header with UTF-8 charset.
996 ///
997 /// # Parameters
998 /// - `html`: The HTML content (String, &str, or anything that converts to String)
999 ///
1000 /// # Returns
1001 /// A new response with the HTML content
1002 ///
1003 /// # Examples
1004 /// ```
1005 /// use ignitia::Response;
1006 ///
1007 /// let html = r#"
1008 /// <!DOCTYPE html>
1009 /// <html>
1010 /// <head>
1011 /// <title>Hello Page</title>
1012 /// <meta charset="UTF-8">
1013 /// </head>
1014 /// <body>
1015 /// <h1>Hello, World!</h1>
1016 /// <p>Welcome to our website!</p>
1017 /// </body>
1018 /// </html>
1019 /// "#;
1020 ///
1021 /// let response = Response::html(html);
1022 /// assert_eq!(
1023 /// response.headers.get("content-type").unwrap(),
1024 /// "text/html; charset=utf-8"
1025 /// );
1026 /// ```
1027 ///
1028 /// ## Dynamic HTML Generation
1029 /// ```
1030 /// use ignitia::Response;
1031 ///
1032 /// fn generate_user_page(username: &str, email: &str) -> Response {
1033 /// let html = format!(r#"
1034 /// <!DOCTYPE html>
1035 /// <html>
1036 /// <head><title>User Profile</title></head>
1037 /// <body>
1038 /// <h1>User Profile</h1>
1039 /// <p><strong>Username:</strong> {}</p>
1040 /// <p><strong>Email:</strong> {}</p>
1041 /// </body>
1042 /// </html>
1043 /// "#, username, email);
1044 ///
1045 /// Response::html(html)
1046 /// }
1047 /// ```
1048 ///
1049 /// ## Security Note
1050 /// When generating HTML with user input, always escape the input to prevent XSS:
1051 /// ```
1052 /// use ignitia::Response;
1053 ///
1054 /// fn safe_html_response(user_input: &str) -> Response {
1055 /// // Escape user input (you would use a proper HTML escape function)
1056 /// let escaped_input = user_input
1057 /// .replace('&', "&")
1058 /// .replace('<', "<")
1059 /// .replace('>', ">")
1060 /// .replace('"', """)
1061 /// .replace('\'', "'");
1062 ///
1063 /// let html = format!("<p>Hello, {}</p>", escaped_input);
1064 /// Response::html(html)
1065 /// }
1066 /// ```
1067 #[inline]
1068 pub fn html(html: impl Into<String>) -> Self {
1069 let mut response = Self::new(StatusCode::OK);
1070 response.headers.insert(
1071 HeaderName::from_static("content-type"),
1072 HeaderValue::from_static("text/html; charset=utf-8"),
1073 );
1074 response.body = Bytes::from(html.into());
1075 response
1076 }
1077
1078 /// Creates a temporary redirect response (HTTP 302 Found).
1079 ///
1080 /// This is the most commonly used redirect method. The client will make a new request
1081 /// to the provided location, but the original URL should be used for future requests.
1082 /// The HTTP method may change to GET for the redirected request.
1083 ///
1084 /// # Arguments
1085 ///
1086 /// * `location` - The URL to redirect to
1087 ///
1088 /// # Examples
1089 ///
1090 /// ## Basic Usage
1091 /// ```
1092 /// use ignitia::Response;
1093 ///
1094 /// let response = Response::redirect("/dashboard");
1095 /// assert_eq!(response.status, ignitia::StatusCode::FOUND);
1096 /// assert_eq!(
1097 /// response.headers.get("location").unwrap(),
1098 /// "/dashboard"
1099 /// );
1100 /// ```
1101 ///
1102 /// ## Redirect After Login
1103 /// ```
1104 /// use ignitia::Response;
1105 ///
1106 /// async fn login_handler() -> ignitia::Result<Response> {
1107 /// // Authenticate user...
1108 /// Ok(Response::redirect("/dashboard"))
1109 /// }
1110 /// ```
1111 ///
1112 /// ## Conditional Redirect
1113 /// ```
1114 /// use ignitia::Response;
1115 ///
1116 /// fn redirect_based_on_role(user_role: &str) -> Response {
1117 /// match user_role {
1118 /// "admin" => Response::redirect("/admin/dashboard"),
1119 /// "user" => Response::redirect("/user/profile"),
1120 /// _ => Response::redirect("/login"),
1121 /// }
1122 /// }
1123 /// ```
1124 #[inline]
1125 pub fn redirect(location: impl Into<String>) -> Self {
1126 Self::redirect_with_status(StatusCode::FOUND, location)
1127 }
1128
1129 /// Creates a permanent redirect response (HTTP 301 Moved Permanently).
1130 ///
1131 /// Use this when a resource has permanently moved to a new location. Search engines
1132 /// and browsers will update their records to use the new URL. The HTTP method may
1133 /// change to GET for the redirected request.
1134 ///
1135 /// # Arguments
1136 ///
1137 /// * `location` - The new permanent URL location
1138 ///
1139 /// # Examples
1140 ///
1141 /// ## Basic Permanent Redirect
1142 /// ```
1143 /// use ignitia::Response;
1144 ///
1145 /// let response = Response::permanent_redirect("/new-location");
1146 /// assert_eq!(response.status, ignitia::StatusCode::MOVED_PERMANENTLY);
1147 /// ```
1148 ///
1149 /// ## Redirecting Old URLs
1150 /// ```
1151 /// use ignitia::Response;
1152 ///
1153 /// async fn handle_old_blog_url() -> ignitia::Result<Response> {
1154 /// // Old blog structure: /blog/2023/article-title
1155 /// // New blog structure: /articles/article-title
1156 /// Ok(Response::permanent_redirect("/articles/migrating-to-new-blog"))
1157 /// }
1158 /// ```
1159 ///
1160 /// ## SEO-Friendly Redirects
1161 /// ```
1162 /// use ignitia::Response;
1163 ///
1164 /// fn redirect_old_product_page(old_id: u32, new_slug: &str) -> Response {
1165 /// // Permanently redirect old product IDs to new slug-based URLs
1166 /// Response::permanent_redirect(&format!("/products/{}", new_slug))
1167 /// }
1168 /// ```
1169 #[inline]
1170 pub fn permanent_redirect(location: impl Into<String>) -> Self {
1171 Self::redirect_with_status(StatusCode::MOVED_PERMANENTLY, location)
1172 }
1173
1174 /// Creates a redirect response with a custom HTTP status code.
1175 ///
1176 /// This method allows you to specify any redirect status code (3xx series).
1177 /// The response includes an HTML fallback page for browsers that don't
1178 /// automatically follow redirects.
1179 ///
1180 /// # Arguments
1181 ///
1182 /// * `status` - The HTTP status code for the redirect (typically 3xx)
1183 /// * `location` - The URL to redirect to
1184 ///
1185 /// # Examples
1186 ///
1187 /// ## Custom Status Redirect
1188 /// ```
1189 /// use ignitia::{Response, StatusCode};
1190 ///
1191 /// let response = Response::redirect_with_status(
1192 /// StatusCode::FOUND,
1193 /// "/custom-redirect"
1194 /// );
1195 /// ```
1196 ///
1197 /// ## Multiple Choice Redirect
1198 /// ```
1199 /// use ignitia::{Response, StatusCode};
1200 ///
1201 /// fn handle_ambiguous_request() -> Response {
1202 /// Response::redirect_with_status(
1203 /// StatusCode::MULTIPLE_CHOICES,
1204 /// "/choose-option"
1205 /// )
1206 /// }
1207 /// ```
1208 ///
1209 /// ## Temporary Maintenance Redirect
1210 /// ```
1211 /// use ignitia::{Response, StatusCode};
1212 ///
1213 /// fn maintenance_redirect() -> Response {
1214 /// Response::redirect_with_status(
1215 /// StatusCode::TEMPORARY_REDIRECT,
1216 /// "/maintenance"
1217 /// )
1218 /// }
1219 /// ```
1220 pub fn redirect_with_status(status: StatusCode, location: impl Into<String>) -> Self {
1221 let location_str = location.into();
1222
1223 let mut response = Self::new(status);
1224 response.headers.insert(
1225 header::LOCATION,
1226 HeaderValue::from_str(&location_str).unwrap_or_else(|_| HeaderValue::from_static("/")),
1227 );
1228
1229 // Add a simple HTML body for browsers that don't handle redirects automatically
1230 let html_body = format!(
1231 r#"<!DOCTYPE html>
1232 <html>
1233 <head>
1234 <title>Redirect</title>
1235 <meta http-equiv="refresh" content="0; url={}">
1236 </head>
1237 <body>
1238 <p>Redirecting to <a href="{}">{}</a></p>
1239 </body>
1240 </html>"#,
1241 html_escape(&location_str),
1242 html_escape(&location_str),
1243 html_escape(&location_str)
1244 );
1245
1246 response.headers.insert(
1247 header::CONTENT_TYPE,
1248 HeaderValue::from_static("text/html; charset=utf-8"),
1249 );
1250 response.body = bytes::Bytes::from(html_body);
1251
1252 response
1253 }
1254
1255 /// Creates a "See Other" redirect response (HTTP 303 See Other).
1256 ///
1257 /// This redirect is ideal for the POST-redirect-GET pattern, where after
1258 /// processing a POST request, you redirect the client to a GET endpoint.
1259 /// This prevents duplicate form submissions if the user refreshes the page.
1260 ///
1261 /// # Arguments
1262 ///
1263 /// * `location` - The URL to redirect to (typically a GET endpoint)
1264 ///
1265 /// # Examples
1266 ///
1267 /// ## POST-Redirect-GET Pattern
1268 /// ```
1269 /// use ignitia::Response;
1270 ///
1271 /// async fn process_form() -> ignitia::Result<Response> {
1272 /// // Process form data...
1273 /// // Save to database...
1274 ///
1275 /// // Redirect to success page to prevent duplicate submissions
1276 /// Ok(Response::see_other("/form-success"))
1277 /// }
1278 /// ```
1279 ///
1280 /// ## After User Registration
1281 /// ```
1282 /// use ignitia::Response;
1283 ///
1284 /// async fn register_user() -> ignitia::Result<Response> {
1285 /// // Create new user account...
1286 ///
1287 /// // Redirect to welcome page
1288 /// Ok(Response::see_other("/welcome"))
1289 /// }
1290 /// ```
1291 ///
1292 /// ## Shopping Cart Checkout
1293 /// ```
1294 /// use ignitia::Response;
1295 ///
1296 /// async fn checkout_handler() -> ignitia::Result<Response> {
1297 /// // Process payment...
1298 /// // Update inventory...
1299 ///
1300 /// // Redirect to order confirmation
1301 /// Ok(Response::see_other("/order-confirmation"))
1302 /// }
1303 /// ```
1304 #[inline]
1305 pub fn see_other(location: impl Into<String>) -> Self {
1306 Self::redirect_with_status(StatusCode::SEE_OTHER, location)
1307 }
1308
1309 /// Creates a temporary redirect that preserves the HTTP method (HTTP 307 Temporary Redirect).
1310 ///
1311 /// Unlike 302 redirects, this guarantees that the client will use the same HTTP method
1312 /// when making the redirected request. Use this when the method preservation is important.
1313 ///
1314 /// # Arguments
1315 ///
1316 /// * `location` - The temporary URL to redirect to
1317 ///
1318 /// # Examples
1319 ///
1320 /// ## Method-Preserving Redirect
1321 /// ```
1322 /// use ignitia::Response;
1323 ///
1324 /// async fn api_endpoint() -> ignitia::Result<Response> {
1325 /// // Temporarily redirect POST to another server
1326 /// Ok(Response::temporary_redirect("/api/v2/endpoint"))
1327 /// }
1328 /// ```
1329 ///
1330 /// ## Load Balancing Redirect
1331 /// ```
1332 /// use ignitia::Response;
1333 ///
1334 /// fn balance_load() -> Response {
1335 /// // Redirect to less busy server while preserving method
1336 /// Response::temporary_redirect("https://server2.example.com/api")
1337 /// }
1338 /// ```
1339 #[inline]
1340 pub fn temporary_redirect(location: impl Into<String>) -> Self {
1341 Self::redirect_with_status(StatusCode::TEMPORARY_REDIRECT, location)
1342 }
1343
1344 /// Creates a permanent redirect that preserves the HTTP method (HTTP 308 Permanent Redirect).
1345 ///
1346 /// This is like 301 but guarantees the client will use the same HTTP method.
1347 /// Use this for permanent moves where method preservation is crucial.
1348 ///
1349 /// # Arguments
1350 ///
1351 /// * `location` - The new permanent URL
1352 ///
1353 /// # Examples
1354 ///
1355 /// ## API Version Migration
1356 /// ```
1357 /// use ignitia::Response;
1358 ///
1359 /// async fn old_api_endpoint() -> ignitia::Result<Response> {
1360 /// // Permanently moved to new API version, preserve HTTP method
1361 /// Ok(Response::permanent_redirect_308("/api/v2/users"))
1362 /// }
1363 /// ```
1364 #[inline]
1365 pub fn permanent_redirect_308(location: impl Into<String>) -> Self {
1366 Self::redirect_with_status(StatusCode::PERMANENT_REDIRECT, location)
1367 }
1368
1369 /// Creates a redirect response without an HTML body (useful for APIs).
1370 ///
1371 /// This method creates a minimal redirect response with only the necessary headers,
1372 /// making it ideal for REST APIs or situations where you don't want the HTML fallback.
1373 ///
1374 /// # Arguments
1375 ///
1376 /// * `status` - The HTTP status code for the redirect
1377 /// * `location` - The URL to redirect to
1378 ///
1379 /// # Examples
1380 ///
1381 /// ## API Redirect
1382 /// ```
1383 /// use ignitia::{Response, StatusCode};
1384 ///
1385 /// async fn api_redirect() -> ignitia::Result<Response> {
1386 /// Ok(Response::redirect_empty(
1387 /// StatusCode::FOUND,
1388 /// "https://api.example.com/v2/endpoint"
1389 /// ))
1390 /// }
1391 /// ```
1392 ///
1393 /// ## Minimal Redirect
1394 /// ```
1395 /// use ignitia::{Response, StatusCode};
1396 ///
1397 /// fn lightweight_redirect() -> Response {
1398 /// Response::redirect_empty(
1399 /// StatusCode::MOVED_PERMANENTLY,
1400 /// "/new-location"
1401 /// )
1402 /// }
1403 /// ```
1404 pub fn redirect_empty(status: StatusCode, location: impl Into<String>) -> Self {
1405 let location_str = location.into();
1406
1407 let mut response = Self::new(status);
1408 response.headers.insert(
1409 header::LOCATION,
1410 HeaderValue::from_str(&location_str).unwrap_or_else(|_| HeaderValue::from_static("/")),
1411 );
1412
1413 response
1414 }
1415
1416 /// Convenience method for redirecting to a login page.
1417 ///
1418 /// Creates a temporary redirect (302) to the specified login path.
1419 /// This is a commonly used pattern in web applications for authentication flows.
1420 ///
1421 /// # Arguments
1422 ///
1423 /// * `login_path` - The path to the login page
1424 ///
1425 /// # Examples
1426 ///
1427 /// ## Basic Login Redirect
1428 /// ```
1429 /// use ignitia::Response;
1430 ///
1431 /// async fn protected_handler() -> ignitia::Result<Response> {
1432 /// // Check authentication...
1433 /// if !user_authenticated {
1434 /// return Ok(Response::redirect_to_login("/auth/login"));
1435 /// }
1436 ///
1437 /// // Continue with protected logic...
1438 /// Ok(Response::text("Welcome to protected area"))
1439 /// }
1440 /// ```
1441 ///
1442 /// ## With Return URL
1443 /// ```
1444 /// use ignitia::Response;
1445 ///
1446 /// fn redirect_with_return_url(current_path: &str) -> Response {
1447 /// let login_url = format!("/login?return_to={}",
1448 /// urlencoding::encode(current_path));
1449 /// Response::redirect_to_login(login_url)
1450 /// }
1451 /// ```
1452 #[inline]
1453 pub fn redirect_to_login(login_path: impl Into<String>) -> Self {
1454 Self::redirect(login_path)
1455 }
1456
1457 /// Convenience method for redirecting after a successful POST request.
1458 ///
1459 /// Uses HTTP 303 (See Other) to implement the POST-redirect-GET pattern,
1460 /// preventing duplicate form submissions when users refresh the page.
1461 ///
1462 /// # Arguments
1463 ///
1464 /// * `location` - The success page URL to redirect to
1465 ///
1466 /// # Examples
1467 ///
1468 /// ## Form Submission Success
1469 /// ```
1470 /// use ignitia::Response;
1471 ///
1472 /// async fn contact_form_handler() -> ignitia::Result<Response> {
1473 /// // Process contact form...
1474 /// // Send email...
1475 /// // Save to database...
1476 ///
1477 /// Ok(Response::redirect_after_post("/contact/thank-you"))
1478 /// }
1479 /// ```
1480 ///
1481 /// ## E-commerce Order
1482 /// ```
1483 /// use ignitia::Response;
1484 ///
1485 /// async fn place_order() -> ignitia::Result<Response> {
1486 /// // Process order...
1487 /// // Charge payment...
1488 /// // Update inventory...
1489 ///
1490 /// Ok(Response::redirect_after_post("/orders/success"))
1491 /// }
1492 /// ```
1493 #[inline]
1494 pub fn redirect_after_post(location: impl Into<String>) -> Self {
1495 Self::see_other(location)
1496 }
1497
1498 /// Convenience method for redirecting moved content.
1499 ///
1500 /// Creates a permanent redirect (301) for content that has been permanently moved.
1501 /// This is ideal for SEO as search engines will update their indexes.
1502 ///
1503 /// # Arguments
1504 ///
1505 /// * `new_location` - The new permanent location of the content
1506 ///
1507 /// # Examples
1508 ///
1509 /// ## Content Migration
1510 /// ```
1511 /// use ignitia::Response;
1512 ///
1513 /// async fn old_article_handler() -> ignitia::Result<Response> {
1514 /// // Article has moved to new URL structure
1515 /// Ok(Response::redirect_moved("/articles/2023/new-article-slug"))
1516 /// }
1517 /// ```
1518 ///
1519 /// ## Domain Migration
1520 /// ```
1521 /// use ignitia::Response;
1522 ///
1523 /// fn redirect_to_new_domain() -> Response {
1524 /// Response::redirect_moved("https://newdomain.com/same-path")
1525 /// }
1526 /// ```
1527 #[inline]
1528 pub fn redirect_moved(new_location: impl Into<String>) -> Self {
1529 Self::permanent_redirect(new_location)
1530 }
1531
1532 /// Sets cache control header with specified max-age value.
1533 ///
1534 /// This method adds a `Cache-Control` header to the response with the specified
1535 /// maximum age in seconds. The cache control header instructs browsers, CDNs,
1536 /// and other caching systems how long to cache this response before considering
1537 /// it stale and requiring revalidation.
1538 ///
1539 /// # Parameters
1540 /// - `max_age`: Cache lifetime in seconds (0 = no caching)
1541 ///
1542 /// # HTTP Header Generated
1543 /// Creates: `Cache-Control: max-age={max_age}`
1544 ///
1545 /// # Common Cache Durations
1546 /// - **0**: No caching (immediate expiration)
1547 /// - **300**: 5 minutes (dynamic API responses)
1548 /// - **3600**: 1 hour (semi-static content)
1549 /// - **86400**: 24 hours (daily updated content)
1550 /// - **2592000**: 30 days (monthly content)
1551 /// - **31536000**: 1 year (static assets with versioning)
1552 ///
1553 /// # Examples
1554 ///
1555 /// ## API Response Caching
1556 /// ```
1557 /// use ignitia::Response;
1558 /// use serde_json::json;
1559 ///
1560 /// async fn user_profile() -> ignitia::Result<Response> {
1561 /// let profile = get_user_profile().await?;
1562 ///
1563 /// Response::json(profile)?
1564 /// .with_cache_control(1800) // Cache for 30 minutes
1565 /// }
1566 /// ```
1567 ///
1568 /// ## Static Asset Caching
1569 /// ```
1570 /// async fn serve_css() -> Response {
1571 /// Response::text_static(include_str!("styles.css"))
1572 /// .with_cache_control(31536000) // Cache for 1 year
1573 /// }
1574 /// ```
1575 ///
1576 /// ## No-Cache for Sensitive Data
1577 /// ```
1578 /// async fn user_balance() -> ignitia::Result<Response> {
1579 /// let balance = get_current_balance().await?;
1580 ///
1581 /// Response::json(balance)?
1582 /// .with_cache_control(0) // Never cache sensitive financial data
1583 /// }
1584 /// ```
1585 #[inline]
1586 pub fn with_cache_control(mut self, max_age: u64) -> Self {
1587 self.headers.insert(
1588 http::header::CACHE_CONTROL,
1589 HeaderValue::from_str(&format!("max-age={}", max_age)).unwrap(),
1590 );
1591 self
1592 }
1593
1594 /// Generates a unique cache key for this response based on request URI and ETag.
1595 ///
1596 /// Creates a cache key that uniquely identifies this response for storage in
1597 /// cache systems like Redis, Memcached, or CDN edge caches. The key combines
1598 /// the request URI with the response's ETag header (if present) to ensure
1599 /// cache invalidation when content changes.
1600 ///
1601 /// # Key Format
1602 /// `cache_{request_uri}_{etag_or_default}`
1603 ///
1604 /// # Parameters
1605 /// - `request_uri`: The URI path of the original request
1606 ///
1607 /// # Cache Key Strategy
1608 /// - Uses request URI as the primary identifier
1609 /// - Includes ETag header value for content versioning
1610 /// - Falls back to "default" if no ETag is present
1611 /// - Ensures unique keys for different content versions
1612 ///
1613 /// # Use Cases
1614 /// - **CDN Cache Keys**: Identifying cached responses in CDN systems
1615 /// - **Application Cache**: Storing responses in Redis/Memcached
1616 /// - **Cache Invalidation**: Targeting specific cached entries for removal
1617 /// - **Analytics**: Tracking cache hit/miss ratios per endpoint
1618 ///
1619 /// # Examples
1620 ///
1621 /// ## Basic Cache Key Generation
1622 /// ```
1623 /// use ignitia::Response;
1624 ///
1625 /// let response = Response::json(user_data)?
1626 /// .with_header("etag", "\"abc123\"");
1627 ///
1628 /// let cache_key = response.cache_key("/api/users/123");
1629 /// // Returns: "cache_/api/users/123_abc123"
1630 /// ```
1631 ///
1632 /// ## Cache Storage Integration
1633 /// ```
1634 /// async fn cached_response(uri: &str) -> ignitia::Result<Response> {
1635 /// let response = Response::json(expensive_computation().await)?;
1636 /// let cache_key = response.cache_key(uri);
1637 ///
1638 /// // Store in Redis for future requests
1639 /// redis_client.set(&cache_key, &response.body).await?;
1640 ///
1641 /// Ok(response)
1642 /// }
1643 /// ```
1644 ///
1645 /// ## Cache Invalidation
1646 /// ```
1647 /// async fn invalidate_user_cache(user_id: u64) {
1648 /// let pattern = format!("cache_/api/users/{}_*", user_id);
1649 /// redis_client.delete_pattern(&pattern).await;
1650 /// }
1651 /// ```
1652 pub fn cache_key(&self, request_uri: &str) -> String {
1653 format!(
1654 "cache_{}_{}",
1655 request_uri,
1656 self.headers
1657 .get("etag")
1658 .and_then(|v| v.to_str().ok())
1659 .unwrap_or("default")
1660 )
1661 }
1662
1663 /// Extracts the max-age value from the Cache-Control header.
1664 ///
1665 /// Parses the `Cache-Control` header to extract the `max-age` directive value,
1666 /// which indicates how many seconds the response should be cached. Returns 0
1667 /// if no valid max-age directive is found or if the header is malformed.
1668 ///
1669 /// # Parsing Logic
1670 /// 1. Retrieves `Cache-Control` header value
1671 /// 2. Searches for `max-age=` directive
1672 /// 3. Extracts numeric value after the equals sign
1673 /// 4. Handles comma-separated directives correctly
1674 /// 5. Returns 0 for invalid or missing values
1675 ///
1676 /// # Return Value
1677 /// - **> 0**: Cache lifetime in seconds
1678 /// - **0**: No caching or invalid header
1679 ///
1680 /// # Examples
1681 ///
1682 /// ## Reading Cache Duration
1683 /// ```
1684 /// use ignitia::Response;
1685 ///
1686 /// let response = Response::json(data)?
1687 /// .with_cache_control(3600);
1688 ///
1689 /// assert_eq!(response.cache_max_age(), 3600);
1690 /// ```
1691 ///
1692 /// ## Conditional Processing Based on Cache Duration
1693 /// ```
1694 /// async fn process_response(response: &Response) {
1695 /// let cache_duration = response.cache_max_age();
1696 ///
1697 /// match cache_duration {
1698 /// 0 => {
1699 /// // No caching - always fetch fresh
1700 /// log::info!("Response not cacheable");
1701 /// }
1702 /// 1..=300 => {
1703 /// // Short-term cache - good for dynamic content
1704 /// log::info!("Short-term cache: {} seconds", cache_duration);
1705 /// }
1706 /// 301..=3600 => {
1707 /// // Medium-term cache - semi-static content
1708 /// log::info!("Medium-term cache: {} seconds", cache_duration);
1709 /// }
1710 /// _ => {
1711 /// // Long-term cache - static assets
1712 /// log::info!("Long-term cache: {} seconds", cache_duration);
1713 /// }
1714 /// }
1715 /// }
1716 /// ```
1717 ///
1718 /// ## Cache Validation Logic
1719 /// ```
1720 /// fn should_serve_from_cache(response: &Response, cached_at: SystemTime) -> bool {
1721 /// let max_age = response.cache_max_age();
1722 /// if max_age == 0 {
1723 /// return false; // Never cache
1724 /// }
1725 ///
1726 /// let elapsed = cached_at.elapsed().unwrap_or_default();
1727 /// elapsed.as_secs() < max_age
1728 /// }
1729 /// ```
1730 pub fn cache_max_age(&self) -> u64 {
1731 self.headers
1732 .get("cache-control")
1733 .and_then(|v| v.to_str().ok())
1734 .and_then(|v| v.split("max-age=").nth(1))
1735 .and_then(|v| v.split(',').next())
1736 .and_then(|v| v.trim().parse().ok())
1737 .unwrap_or(0)
1738 }
1739
1740 /// Determines if this response is cacheable based on status and headers.
1741 ///
1742 /// Analyzes the response to determine if it should be cached by browsers,
1743 /// CDNs, and other caching systems. A response is considered cacheable if:
1744 /// 1. The HTTP status indicates success (2xx range)
1745 /// 2. The Cache-Control header contains a valid `max-age` directive
1746 /// 3. The max-age value is greater than 0
1747 ///
1748 /// # Cacheability Rules
1749 /// - **Success Status Required**: Only 2xx status codes are cacheable
1750 /// - **Valid Cache-Control**: Must have `max-age=` directive
1751 /// - **Non-Zero Duration**: `max-age=0` indicates no caching
1752 /// - **Header Presence**: Missing Cache-Control header = not cacheable
1753 ///
1754 /// # Returns
1755 /// - `true`: Response should be cached by clients and intermediaries
1756 /// - `false`: Response should not be cached (fetch fresh each time)
1757 ///
1758 /// # Examples
1759 ///
1760 /// ## Conditional Cache Storage
1761 /// ```
1762 /// use ignitia::Response;
1763 ///
1764 /// async fn handle_api_request() -> ignitia::Result<Response> {
1765 /// let response = Response::json(get_data().await)?
1766 /// .with_cache_control(1800);
1767 ///
1768 /// if response.is_cacheable() {
1769 /// // Store in application cache
1770 /// cache_service.store(&response).await?;
1771 /// log::info!("Response cached for {} seconds", response.cache_max_age());
1772 /// } else {
1773 /// log::info!("Response not cacheable - serving fresh");
1774 /// }
1775 ///
1776 /// Ok(response)
1777 /// }
1778 /// ```
1779 ///
1780 /// ## CDN Integration
1781 /// ```
1782 /// fn configure_cdn_headers(mut response: Response) -> Response {
1783 /// if response.is_cacheable() {
1784 /// // Add CDN-specific headers for cacheable responses
1785 /// response.headers.insert("x-cdn-cache", HeaderValue::from_static("HIT"));
1786 /// response.headers.insert("x-cache-duration",
1787 /// HeaderValue::from_str(&response.cache_max_age().to_string()).unwrap());
1788 /// } else {
1789 /// // Ensure CDN doesn't cache non-cacheable responses
1790 /// response.headers.insert("x-cdn-cache", HeaderValue::from_static("BYPASS"));
1791 /// }
1792 /// response
1793 /// }
1794 /// ```
1795 ///
1796 /// ## Performance Monitoring
1797 /// ```
1798 /// fn log_cache_metrics(response: &Response, endpoint: &str) {
1799 /// if response.is_cacheable() {
1800 /// metrics::increment_counter("cacheable_responses", &[("endpoint", endpoint)]);
1801 /// metrics::histogram("cache_duration_seconds", response.cache_max_age() as f64);
1802 /// } else {
1803 /// metrics::increment_counter("non_cacheable_responses", &[("endpoint", endpoint)]);
1804 /// }
1805 /// }
1806 /// ```
1807 ///
1808 /// ## Error Response Handling
1809 /// ```
1810 /// fn create_error_response(error: &AppError) -> Response {
1811 /// let response = Response::json(error.to_json())
1812 /// .unwrap_or_else(|_| Response::server_error());
1813 ///
1814 /// // Error responses are typically not cacheable
1815 /// assert!(!response.is_cacheable());
1816 ///
1817 /// response
1818 /// }
1819 /// ```
1820 pub fn is_cacheable(&self) -> bool {
1821 self.status.is_success()
1822 && self
1823 .headers
1824 .get("cache-control")
1825 .and_then(|v| v.to_str().ok())
1826 .map(|v| v.contains("max-age=") && !v.contains("max-age=0"))
1827 .unwrap_or(false)
1828 }
1829}
1830
1831pub use builder::ResponseBuilder;
1832
1833use crate::error::{Error, ErrorResponse};
1834
1835impl From<Error> for Response {
1836 /// Converts an Error into an HTTP Response.
1837 ///
1838 /// This implementation automatically converts framework errors into proper
1839 /// HTTP responses with JSON formatting and appropriate status codes.
1840 ///
1841 /// # Parameters
1842 /// - `err`: The error to convert
1843 ///
1844 /// # Returns
1845 /// An HTTP response representing the error
1846 ///
1847 /// # Error Response Format
1848 /// The generated response contains:
1849 /// - Appropriate HTTP status code
1850 /// - JSON body with error details
1851 /// - Proper content-type headers
1852 /// - Timestamp information
1853 ///
1854 /// # Examples
1855 /// ```
1856 /// use ignitia::{Error, Response};
1857 /// use http::StatusCode;
1858 ///
1859 /// // Convert a NotFound error
1860 /// let error = Error::NotFound("User not found".to_string());
1861 /// let response = Response::from(error);
1862 /// assert_eq!(response.status, StatusCode::NOT_FOUND);
1863 ///
1864 /// // The response body will be JSON:
1865 /// // {
1866 /// // "error": "Not Found",
1867 /// // "message": "User not found",
1868 /// // "status": 404,
1869 /// // "error_type": "not_found",
1870 /// // "timestamp": "2023-01-01T12:00:00Z"
1871 /// // }
1872 /// ```
1873 fn from(err: Error) -> Self {
1874 let status = err.status_code();
1875 let error_response = err.to_response(true);
1876
1877 // Try JSON first, fallback to plain text
1878 match serde_json::to_vec(&error_response) {
1879 Ok(json_body) => {
1880 let mut response = Response::new(status);
1881 response.headers.insert(
1882 http::header::CONTENT_TYPE,
1883 http::HeaderValue::from_static("application/json"),
1884 );
1885 response.body = bytes::Bytes::from(json_body);
1886 response
1887 }
1888 Err(_) => {
1889 let mut response = Response::new(status);
1890 response.headers.insert(
1891 http::header::CONTENT_TYPE,
1892 http::HeaderValue::from_static("text/plain; charset=utf-8"),
1893 );
1894 response.body = bytes::Bytes::from(err.to_string());
1895 response
1896 }
1897 }
1898 }
1899}
1900
1901// Enhanced response methods
1902impl Response {
1903 /// Creates a JSON error response from an error.
1904 ///
1905 /// This method provides more control over error response generation than
1906 /// the automatic From implementation. It always returns JSON and provides
1907 /// better error handling for serialization failures.
1908 ///
1909 /// # Type Parameters
1910 /// - `E`: Error type that can be converted to framework Error
1911 ///
1912 /// # Parameters
1913 /// - `error`: The error to convert to a response
1914 ///
1915 /// # Returns
1916 /// - `Ok(Response)`: Successfully created error response
1917 /// - `Err(Error)`: JSON serialization error
1918 ///
1919 /// # Examples
1920 /// ```
1921 /// use ignitia::{Response, Error};
1922 ///
1923 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1924 /// let error = Error::Validation("Email format is invalid".to_string());
1925 /// let response = Response::error_json(error)?;
1926 /// # Ok(())
1927 /// # }
1928 /// ```
1929 pub fn error_json<E: Into<Error>>(error: E) -> crate::Result<Self> {
1930 let err = error.into();
1931 let status = err.status_code();
1932 let error_response = err.to_response(true);
1933
1934 let mut response = Self::new(status);
1935 response.headers.insert(
1936 http::header::CONTENT_TYPE,
1937 http::HeaderValue::from_static("application/json"),
1938 );
1939 response.body = bytes::Bytes::from(serde_json::to_vec(&error_response)?);
1940 Ok(response)
1941 }
1942
1943 /// Creates a validation error response with multiple error messages.
1944 ///
1945 /// This method creates a structured validation error response that can
1946 /// contain multiple validation failure messages.
1947 ///
1948 /// # Parameters
1949 /// - `messages`: Vector of validation error messages
1950 ///
1951 /// # Returns
1952 /// - `Ok(Response)`: Successfully created validation error response
1953 /// - `Err(Error)`: JSON serialization error
1954 ///
1955 /// # Examples
1956 /// ```
1957 /// use ignitia::Response;
1958 ///
1959 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1960 /// let validation_errors = vec![
1961 /// "Name is required".to_string(),
1962 /// "Email format is invalid".to_string(),
1963 /// "Password must be at least 8 characters".to_string(),
1964 /// ];
1965 ///
1966 /// let response = Response::validation_error(validation_errors)?;
1967 /// # Ok(())
1968 /// # }
1969 /// ```
1970 ///
1971 /// ## Response Format
1972 /// The generated response has this structure:
1973 /// ```
1974 /// {
1975 /// "error": "Validation Failed",
1976 /// "message": "Name is required, Email format is invalid, Password must be at least 8 characters",
1977 /// "status": 400,
1978 /// "error_type": "validation_error",
1979 /// "error_code": "VALIDATION_FAILED",
1980 /// "metadata": {
1981 /// "validation_errors": ["Name is required", "Email format is invalid", "Password must be at least 8 characters"]
1982 /// },
1983 /// "timestamp": "2023-01-01T12:00:00Z"
1984 /// }
1985 /// ```
1986 ///
1987 /// ## Usage in Form Validation
1988 /// ```
1989 /// use ignitia::{Response, Request, Result};
1990 /// use serde::Deserialize;
1991 ///
1992 /// #[derive(Deserialize)]
1993 /// struct UserForm {
1994 /// name: String,
1995 /// email: String,
1996 /// password: String,
1997 /// }
1998 ///
1999 /// async fn validate_user_form(req: Request) -> Result<Response> {
2000 /// let form: UserForm = req.json()?;
2001 /// let mut errors = Vec::new();
2002 ///
2003 /// if form.name.trim().is_empty() {
2004 /// errors.push("Name is required".to_string());
2005 /// }
2006 ///
2007 /// if !form.email.contains('@') {
2008 /// errors.push("Invalid email format".to_string());
2009 /// }
2010 ///
2011 /// if form.password.len() < 8 {
2012 /// errors.push("Password must be at least 8 characters".to_string());
2013 /// }
2014 ///
2015 /// if !errors.is_empty() {
2016 /// return Response::validation_error(errors);
2017 /// }
2018 ///
2019 /// Ok(Response::json(serde_json::json!({
2020 /// "message": "User created successfully"
2021 /// }))?)
2022 /// }
2023 /// ```
2024 pub fn validation_error(messages: Vec<String>) -> crate::Result<Self> {
2025 let error_response = ErrorResponse {
2026 error: "Validation Failed".to_string(),
2027 message: messages.join(", "),
2028 status: 400,
2029 error_type: Some("validation_error".to_string()),
2030 error_code: Some("VALIDATION_FAILED".to_string()),
2031 metadata: Some(serde_json::json!({
2032 "validation_errors": messages
2033 })),
2034 timestamp: Some(chrono::Utc::now().to_rfc3339()),
2035 };
2036
2037 let mut response = Self::new(StatusCode::BAD_REQUEST);
2038 response.headers.insert(
2039 http::header::CONTENT_TYPE,
2040 http::HeaderValue::from_static("application/json"),
2041 );
2042 response.body = bytes::Bytes::from(serde_json::to_vec(&error_response)?);
2043 Ok(response)
2044 }
2045}
2046
2047// Helper function to escape HTML entities
2048fn html_escape(input: &str) -> String {
2049 input
2050 .replace('&', "&")
2051 .replace('<', "<")
2052 .replace('>', ">")
2053 .replace('"', """)
2054 .replace('\'', "'")
2055}