Skip to main content

fastapi_core/
response.rs

1//! HTTP response types.
2
3use serde::Serialize;
4use std::fmt;
5use std::pin::Pin;
6
7use asupersync::stream::Stream;
8#[cfg(test)]
9use asupersync::types::PanicPayload;
10use asupersync::types::{CancelKind, CancelReason, Outcome};
11
12/// HTTP status code.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct StatusCode(u16);
15
16impl StatusCode {
17    // Informational
18    /// 100 Continue
19    pub const CONTINUE: Self = Self(100);
20    /// 101 Switching Protocols
21    pub const SWITCHING_PROTOCOLS: Self = Self(101);
22
23    // Success
24    /// 200 OK
25    pub const OK: Self = Self(200);
26    /// 201 Created
27    pub const CREATED: Self = Self(201);
28    /// 202 Accepted
29    pub const ACCEPTED: Self = Self(202);
30    /// 204 No Content
31    pub const NO_CONTENT: Self = Self(204);
32    /// 206 Partial Content
33    pub const PARTIAL_CONTENT: Self = Self(206);
34
35    // Redirection
36    /// 301 Moved Permanently
37    pub const MOVED_PERMANENTLY: Self = Self(301);
38    /// 302 Found
39    pub const FOUND: Self = Self(302);
40    /// 303 See Other
41    pub const SEE_OTHER: Self = Self(303);
42    /// 304 Not Modified
43    pub const NOT_MODIFIED: Self = Self(304);
44    /// 307 Temporary Redirect
45    pub const TEMPORARY_REDIRECT: Self = Self(307);
46    /// 308 Permanent Redirect
47    pub const PERMANENT_REDIRECT: Self = Self(308);
48
49    // Client Error
50    /// 400 Bad Request
51    pub const BAD_REQUEST: Self = Self(400);
52    /// 401 Unauthorized
53    pub const UNAUTHORIZED: Self = Self(401);
54    /// 403 Forbidden
55    pub const FORBIDDEN: Self = Self(403);
56    /// 404 Not Found
57    pub const NOT_FOUND: Self = Self(404);
58    /// 405 Method Not Allowed
59    pub const METHOD_NOT_ALLOWED: Self = Self(405);
60    /// 406 Not Acceptable
61    pub const NOT_ACCEPTABLE: Self = Self(406);
62    /// 412 Precondition Failed
63    pub const PRECONDITION_FAILED: Self = Self(412);
64    /// 413 Payload Too Large
65    pub const PAYLOAD_TOO_LARGE: Self = Self(413);
66    /// 415 Unsupported Media Type
67    pub const UNSUPPORTED_MEDIA_TYPE: Self = Self(415);
68    /// 416 Range Not Satisfiable
69    pub const RANGE_NOT_SATISFIABLE: Self = Self(416);
70    /// 422 Unprocessable Entity
71    pub const UNPROCESSABLE_ENTITY: Self = Self(422);
72    /// 429 Too Many Requests
73    pub const TOO_MANY_REQUESTS: Self = Self(429);
74    /// 499 Client Closed Request
75    pub const CLIENT_CLOSED_REQUEST: Self = Self(499);
76
77    // Server Error
78    /// 500 Internal Server Error
79    pub const INTERNAL_SERVER_ERROR: Self = Self(500);
80    /// 503 Service Unavailable
81    pub const SERVICE_UNAVAILABLE: Self = Self(503);
82    /// 504 Gateway Timeout
83    pub const GATEWAY_TIMEOUT: Self = Self(504);
84
85    /// Create a status code from a u16.
86    #[must_use]
87    pub const fn from_u16(code: u16) -> Self {
88        Self(code)
89    }
90
91    /// Get the numeric value.
92    #[must_use]
93    pub const fn as_u16(self) -> u16 {
94        self.0
95    }
96
97    /// Check if status code allows a body.
98    #[must_use]
99    pub const fn allows_body(self) -> bool {
100        !matches!(self.0, 100..=103 | 204 | 304)
101    }
102
103    /// Get the canonical reason phrase.
104    #[must_use]
105    pub const fn canonical_reason(self) -> &'static str {
106        match self.0 {
107            100 => "Continue",
108            101 => "Switching Protocols",
109            200 => "OK",
110            201 => "Created",
111            202 => "Accepted",
112            204 => "No Content",
113            206 => "Partial Content",
114            301 => "Moved Permanently",
115            302 => "Found",
116            303 => "See Other",
117            304 => "Not Modified",
118            307 => "Temporary Redirect",
119            308 => "Permanent Redirect",
120            400 => "Bad Request",
121            401 => "Unauthorized",
122            403 => "Forbidden",
123            404 => "Not Found",
124            405 => "Method Not Allowed",
125            406 => "Not Acceptable",
126            412 => "Precondition Failed",
127            413 => "Payload Too Large",
128            415 => "Unsupported Media Type",
129            416 => "Range Not Satisfiable",
130            422 => "Unprocessable Entity",
131            429 => "Too Many Requests",
132            499 => "Client Closed Request",
133            500 => "Internal Server Error",
134            503 => "Service Unavailable",
135            504 => "Gateway Timeout",
136            _ => "Unknown",
137        }
138    }
139}
140
141/// Streamed response body type.
142pub type BodyStream = Pin<Box<dyn Stream<Item = Vec<u8>> + Send>>;
143
144/// Response body.
145pub enum ResponseBody {
146    /// Empty body.
147    Empty,
148    /// Bytes body.
149    Bytes(Vec<u8>),
150    /// Streaming body.
151    Stream(BodyStream),
152}
153
154impl ResponseBody {
155    /// Create a streaming response body.
156    #[must_use]
157    pub fn stream<S>(stream: S) -> Self
158    where
159        S: Stream<Item = Vec<u8>> + Send + 'static,
160    {
161        Self::Stream(Box::pin(stream))
162    }
163
164    /// Check if body is empty.
165    #[must_use]
166    pub fn is_empty(&self) -> bool {
167        matches!(self, Self::Empty) || matches!(self, Self::Bytes(b) if b.is_empty())
168    }
169
170    /// Get body length.
171    #[must_use]
172    pub fn len(&self) -> usize {
173        match self {
174            Self::Empty => 0,
175            Self::Bytes(b) => b.len(),
176            Self::Stream(_) => 0,
177        }
178    }
179}
180
181impl fmt::Debug for ResponseBody {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        match self {
184            Self::Empty => f.debug_tuple("Empty").finish(),
185            Self::Bytes(bytes) => f.debug_tuple("Bytes").field(bytes).finish(),
186            Self::Stream(_) => f.debug_tuple("Stream").finish(),
187        }
188    }
189}
190
191// ============================================================================
192// Header Validation (CRLF Injection Prevention)
193// ============================================================================
194
195/// Check if a header name contains only valid HTTP token characters.
196///
197/// Valid token characters per RFC 7230:
198/// `!#$%&'*+-.0-9A-Z^_`a-z|~`
199fn is_valid_header_name(name: &str) -> bool {
200    !name.is_empty()
201        && name.bytes().all(|b| {
202            matches!(b,
203                b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
204                b'0'..=b'9' | b'A'..=b'Z' | b'^' | b'_' | b'`' | b'a'..=b'z' | b'|' | b'~'
205            )
206        })
207}
208
209/// Sanitize a header value to prevent CRLF injection attacks.
210///
211/// Removes CR (\r) and LF (\n) characters which could be used to inject
212/// additional headers. Also removes null bytes.
213fn sanitize_header_value(value: Vec<u8>) -> Vec<u8> {
214    value
215        .into_iter()
216        .filter(|&b| b != b'\r' && b != b'\n' && b != 0)
217        .collect()
218}
219
220// ============================================================================
221// Set-Cookie Builder
222// ============================================================================
223
224/// SameSite cookie attribute.
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum SameSite {
227    /// Strict SameSite policy.
228    Strict,
229    /// Lax SameSite policy.
230    Lax,
231    /// None SameSite policy.
232    None,
233}
234
235impl SameSite {
236    #[must_use]
237    pub const fn as_str(self) -> &'static str {
238        match self {
239            Self::Strict => "Strict",
240            Self::Lax => "Lax",
241            Self::None => "None",
242        }
243    }
244}
245
246/// Response cookie builder (serialized into a `Set-Cookie` header).
247#[derive(Debug, Clone)]
248pub struct SetCookie {
249    name: String,
250    value: String,
251    path: Option<String>,
252    domain: Option<String>,
253    max_age: Option<i64>,
254    http_only: bool,
255    secure: bool,
256    same_site: Option<SameSite>,
257}
258
259impl SetCookie {
260    /// Create a new cookie with `name=value`.
261    #[must_use]
262    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
263        Self {
264            name: name.into(),
265            value: value.into(),
266            path: Some("/".to_string()),
267            domain: None,
268            max_age: None,
269            http_only: false,
270            secure: false,
271            same_site: None,
272        }
273    }
274
275    /// Set the cookie path.
276    #[must_use]
277    pub fn path(mut self, path: impl Into<String>) -> Self {
278        self.path = Some(path.into());
279        self
280    }
281
282    /// Set the cookie domain.
283    #[must_use]
284    pub fn domain(mut self, domain: impl Into<String>) -> Self {
285        self.domain = Some(domain.into());
286        self
287    }
288
289    /// Set Max-Age (in seconds). Use `0` to delete the cookie.
290    #[must_use]
291    pub fn max_age(mut self, seconds: i64) -> Self {
292        self.max_age = Some(seconds);
293        self
294    }
295
296    /// Set HttpOnly flag.
297    #[must_use]
298    pub fn http_only(mut self, on: bool) -> Self {
299        self.http_only = on;
300        self
301    }
302
303    /// Set Secure flag.
304    #[must_use]
305    pub fn secure(mut self, on: bool) -> Self {
306        self.secure = on;
307        self
308    }
309
310    /// Set SameSite attribute.
311    #[must_use]
312    pub fn same_site(mut self, same_site: SameSite) -> Self {
313        self.same_site = Some(same_site);
314        self
315    }
316
317    /// Serialize into a `Set-Cookie` header value.
318    #[must_use]
319    pub fn to_header_value(&self) -> String {
320        // RFC6265-compatible formatting.
321        //
322        // Note: cookie name and value must use restricted character sets. We validate and
323        // omit invalid optional attributes rather than producing broken Set-Cookie headers.
324        fn is_valid_cookie_name(name: &str) -> bool {
325            // cookie-name is an HTTP token in RFC6265. Reuse our token validator.
326            is_valid_header_name(name)
327        }
328
329        fn is_valid_cookie_value(value: &str) -> bool {
330            // cookie-value = *cookie-octet
331            // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
332            value.is_empty()
333                || value.bytes().all(|b| {
334                    matches!(
335                        b,
336                        0x21
337                            | 0x23..=0x2B
338                            | 0x2D..=0x3A
339                            | 0x3C..=0x5B
340                            | 0x5D..=0x7E
341                    )
342                })
343        }
344
345        fn is_valid_attr_value(value: &str) -> bool {
346            // Keep this conservative: allow visible ASCII excluding ';' and ','.
347            value
348                .bytes()
349                .all(|b| (0x21..=0x7E).contains(&b) && b != b';' && b != b',')
350        }
351
352        if !is_valid_cookie_name(&self.name) || !is_valid_cookie_value(&self.value) {
353            // An invalid cookie name/value generates a broken header; return empty so
354            // callers can choose to drop the header.
355            return String::new();
356        }
357
358        let mut out = String::new();
359        out.push_str(&self.name);
360        out.push('=');
361        out.push_str(&self.value);
362
363        if let Some(ref path) = self.path {
364            if is_valid_attr_value(path) {
365                out.push_str("; Path=");
366                out.push_str(path);
367            }
368        }
369        if let Some(ref domain) = self.domain {
370            if is_valid_attr_value(domain) {
371                out.push_str("; Domain=");
372                out.push_str(domain);
373            }
374        }
375        if let Some(max_age) = self.max_age {
376            out.push_str("; Max-Age=");
377            out.push_str(&max_age.to_string());
378        }
379        if let Some(same_site) = self.same_site {
380            out.push_str("; SameSite=");
381            out.push_str(same_site.as_str());
382        }
383        if self.http_only {
384            out.push_str("; HttpOnly");
385        }
386        if self.secure {
387            out.push_str("; Secure");
388        }
389
390        out
391    }
392}
393
394/// HTTP response.
395#[derive(Debug)]
396pub struct Response {
397    status: StatusCode,
398    headers: Vec<(String, Vec<u8>)>,
399    body: ResponseBody,
400}
401
402impl Response {
403    /// Create a response with the given status.
404    #[must_use]
405    pub fn with_status(status: StatusCode) -> Self {
406        Self {
407            status,
408            headers: Vec::new(),
409            body: ResponseBody::Empty,
410        }
411    }
412
413    /// Create a 200 OK response.
414    #[must_use]
415    pub fn ok() -> Self {
416        Self::with_status(StatusCode::OK)
417    }
418
419    /// Create a 201 Created response.
420    #[must_use]
421    pub fn created() -> Self {
422        Self::with_status(StatusCode::CREATED)
423    }
424
425    /// Create a 204 No Content response.
426    #[must_use]
427    pub fn no_content() -> Self {
428        Self::with_status(StatusCode::NO_CONTENT)
429    }
430
431    /// Create a 500 Internal Server Error response.
432    #[must_use]
433    pub fn internal_error() -> Self {
434        Self::with_status(StatusCode::INTERNAL_SERVER_ERROR)
435    }
436
437    /// Create a 206 Partial Content response.
438    ///
439    /// Used for range requests. You should also set the `Content-Range` header.
440    ///
441    /// # Example
442    ///
443    /// ```ignore
444    /// use fastapi_core::{Response, ResponseBody};
445    ///
446    /// let response = Response::partial_content()
447    ///     .header("Content-Range", b"bytes 0-499/1000".to_vec())
448    ///     .header("Accept-Ranges", b"bytes".to_vec())
449    ///     .body(ResponseBody::Bytes(partial_data));
450    /// ```
451    #[must_use]
452    pub fn partial_content() -> Self {
453        Self::with_status(StatusCode::PARTIAL_CONTENT)
454    }
455
456    /// Create a 416 Range Not Satisfiable response.
457    ///
458    /// Used when a Range header specifies a range that cannot be satisfied.
459    /// You should also set the `Content-Range` header with the resource size.
460    ///
461    /// # Example
462    ///
463    /// ```ignore
464    /// use fastapi_core::Response;
465    ///
466    /// let response = Response::range_not_satisfiable()
467    ///     .header("Content-Range", b"bytes */1000".to_vec());
468    /// ```
469    #[must_use]
470    pub fn range_not_satisfiable() -> Self {
471        Self::with_status(StatusCode::RANGE_NOT_SATISFIABLE)
472    }
473
474    /// Create a 304 Not Modified response.
475    ///
476    /// Used for conditional requests where the resource has not changed.
477    /// The response body is empty per HTTP spec.
478    #[must_use]
479    pub fn not_modified() -> Self {
480        Self::with_status(StatusCode::NOT_MODIFIED)
481    }
482
483    /// Create a 412 Precondition Failed response.
484    ///
485    /// Used when a conditional request's precondition (e.g., `If-Match`) fails.
486    #[must_use]
487    pub fn precondition_failed() -> Self {
488        Self::with_status(StatusCode::PRECONDITION_FAILED)
489    }
490
491    /// Set the ETag header on this response.
492    ///
493    /// # Example
494    ///
495    /// ```ignore
496    /// let response = Response::ok()
497    ///     .with_etag("\"abc123\"")
498    ///     .body(b"content".to_vec());
499    /// ```
500    #[must_use]
501    pub fn with_etag(self, etag: impl Into<String>) -> Self {
502        self.header("ETag", etag.into().into_bytes())
503    }
504
505    /// Set a weak ETag header on this response.
506    ///
507    /// Automatically prefixes with `W/` if not already present.
508    #[must_use]
509    pub fn with_weak_etag(self, etag: impl Into<String>) -> Self {
510        let etag = etag.into();
511        let value = if etag.starts_with("W/") {
512            etag
513        } else {
514            format!("W/{}", etag)
515        };
516        self.header("ETag", value.into_bytes())
517    }
518
519    /// Add a header.
520    ///
521    /// # Security
522    ///
523    /// Header names are validated to contain only valid token characters.
524    /// Header values are sanitized to prevent CRLF injection attacks.
525    /// Invalid characters in names will cause the header to be silently dropped.
526    #[must_use]
527    pub fn header(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
528        let name = name.into();
529        let value = value.into();
530
531        // Validate header name (must be valid HTTP token)
532        if !is_valid_header_name(&name) {
533            // Silently drop invalid headers to prevent injection
534            return self;
535        }
536
537        // Sanitize header value (remove CRLF to prevent injection)
538        let sanitized_value = sanitize_header_value(value);
539
540        self.headers.push((name, sanitized_value));
541        self
542    }
543
544    /// Remove all headers matching `name` (case-insensitive).
545    ///
546    /// This is useful for middleware that needs to suppress or replace headers
547    /// produced by handlers or other middleware.
548    #[must_use]
549    pub fn remove_header(mut self, name: &str) -> Self {
550        self.headers.retain(|(n, _)| !n.eq_ignore_ascii_case(name));
551        self
552    }
553
554    /// Set the body.
555    #[must_use]
556    pub fn body(mut self, body: ResponseBody) -> Self {
557        self.body = body;
558        self
559    }
560
561    /// Set a cookie on the response.
562    ///
563    /// Adds a `Set-Cookie` header with the serialized cookie value.
564    /// Multiple cookies can be set by calling this method multiple times.
565    ///
566    /// # Example
567    ///
568    /// ```
569    /// use fastapi_core::{Response, SameSite, SetCookie};
570    ///
571    /// let response = Response::ok()
572    ///     .set_cookie(SetCookie::new("session", "abc123").http_only(true))
573    ///     .set_cookie(SetCookie::new("prefs", "dark").same_site(SameSite::Lax));
574    /// ```
575    #[must_use]
576    pub fn set_cookie(self, cookie: SetCookie) -> Self {
577        let v = cookie.to_header_value();
578        if v.is_empty() {
579            return self;
580        }
581        self.header("set-cookie", v.into_bytes())
582    }
583
584    /// Delete a cookie by setting it to expire immediately.
585    ///
586    /// This sets the cookie with an empty value and `Max-Age=0`, which tells
587    /// the browser to remove the cookie.
588    ///
589    /// # Example
590    ///
591    /// ```
592    /// use fastapi_core::Response;
593    ///
594    /// let response = Response::ok()
595    ///     .delete_cookie("session");
596    /// ```
597    #[must_use]
598    pub fn delete_cookie(self, name: &str) -> Self {
599        // Create an expired cookie to delete it
600        let cookie = SetCookie::new(name, "").max_age(0);
601        self.set_cookie(cookie)
602    }
603
604    /// Create a JSON response.
605    ///
606    /// # Errors
607    ///
608    /// Returns an error if serialization fails.
609    pub fn json<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> {
610        let bytes = serde_json::to_vec(value)?;
611        Ok(Self::ok()
612            .header("content-type", b"application/json".to_vec())
613            .body(ResponseBody::Bytes(bytes)))
614    }
615
616    /// Get the status code.
617    #[must_use]
618    pub fn status(&self) -> StatusCode {
619        self.status
620    }
621
622    /// Get the headers.
623    #[must_use]
624    pub fn headers(&self) -> &[(String, Vec<u8>)] {
625        &self.headers
626    }
627
628    /// Get the body.
629    #[must_use]
630    pub fn body_ref(&self) -> &ResponseBody {
631        &self.body
632    }
633
634    /// Decompose this response into its parts.
635    #[must_use]
636    pub fn into_parts(self) -> (StatusCode, Vec<(String, Vec<u8>)>, ResponseBody) {
637        (self.status, self.headers, self.body)
638    }
639
640    /// Rebuilds this response with the given headers, preserving status and body.
641    ///
642    /// This is useful for middleware that needs to modify the response
643    /// but preserve original headers.
644    ///
645    /// # Example
646    ///
647    /// ```ignore
648    /// let (status, headers, body) = response.into_parts();
649    /// // ... modify headers ...
650    /// let new_response = Response::with_status(status)
651    ///     .body(body)
652    ///     .rebuild_with_headers(headers);
653    /// ```
654    #[must_use]
655    pub fn rebuild_with_headers(mut self, headers: Vec<(String, Vec<u8>)>) -> Self {
656        for (name, value) in headers {
657            self = self.header(name, value);
658        }
659        self
660    }
661}
662
663/// Trait for types that can be converted into a response.
664pub trait IntoResponse {
665    /// Convert into a response.
666    fn into_response(self) -> Response;
667}
668
669impl IntoResponse for Response {
670    fn into_response(self) -> Response {
671        self
672    }
673}
674
675impl IntoResponse for () {
676    fn into_response(self) -> Response {
677        Response::no_content()
678    }
679}
680
681impl IntoResponse for &'static str {
682    fn into_response(self) -> Response {
683        Response::ok()
684            .header("content-type", b"text/plain; charset=utf-8".to_vec())
685            .body(ResponseBody::Bytes(self.as_bytes().to_vec()))
686    }
687}
688
689impl IntoResponse for String {
690    fn into_response(self) -> Response {
691        Response::ok()
692            .header("content-type", b"text/plain; charset=utf-8".to_vec())
693            .body(ResponseBody::Bytes(self.into_bytes()))
694    }
695}
696
697impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
698    fn into_response(self) -> Response {
699        match self {
700            Ok(v) => v.into_response(),
701            Err(e) => e.into_response(),
702        }
703    }
704}
705
706impl IntoResponse for std::convert::Infallible {
707    fn into_response(self) -> Response {
708        match self {}
709    }
710}
711
712// =============================================================================
713// Response Type Checking (OpenAPI)
714// =============================================================================
715
716/// Marker trait for compile-time response type verification.
717///
718/// This trait is used by the route macros to verify at compile time that
719/// a handler's return type can produce the declared OpenAPI response schema.
720///
721/// # How It Works
722///
723/// When you declare `#[get("/users", response(200, User))]`, the macro
724/// generates a compile-time assertion that checks if the handler's return
725/// type implements `ResponseProduces<User>`.
726///
727/// The implementation uses a simple blanket implementation: any type `T`
728/// that implements `IntoResponse` trivially produces itself as a schema.
729///
730/// For wrapper types like `Json<T>`, they implement `ResponseProduces<T>`
731/// to indicate they produce the inner type's schema.
732///
733/// # Example
734///
735/// ```ignore
736/// // This compiles because User produces User schema
737/// #[get("/user/{id}", response(200, User))]
738/// async fn get_user(Path(id): Path<i64>) -> User {
739///     User { id, name: "Alice".into() }
740/// }
741///
742/// // This also compiles because Json<User> produces User schema
743/// #[get("/user/{id}", response(200, User))]
744/// async fn get_user(Path(id): Path<i64>) -> Json<User> {
745///     Json(User { id, name: "Alice".into() })
746/// }
747/// ```
748pub trait ResponseProduces<T> {}
749
750// A type trivially produces itself
751impl<T> ResponseProduces<T> for T {}
752
753// Json<T> produces T schema (in addition to producing Json<T>)
754impl<T: serde::Serialize + 'static> ResponseProduces<T> for crate::extract::Json<T> {}
755
756// =============================================================================
757// Specialized Response Types
758// =============================================================================
759
760/// HTTP redirect response.
761///
762/// Creates responses with appropriate redirect status codes and Location header.
763///
764/// # Examples
765///
766/// ```
767/// use fastapi_core::Redirect;
768///
769/// // Temporary redirect (307)
770/// let response = Redirect::temporary("/new-location");
771///
772/// // Permanent redirect (308)
773/// let response = Redirect::permanent("/moved-permanently");
774///
775/// // See Other (303) - for POST/redirect/GET pattern
776/// let response = Redirect::see_other("/result");
777/// ```
778#[derive(Debug, Clone)]
779pub struct Redirect {
780    status: StatusCode,
781    location: String,
782}
783
784impl Redirect {
785    /// Create a 307 Temporary Redirect.
786    ///
787    /// The request method and body should be preserved when following the redirect.
788    #[must_use]
789    pub fn temporary(location: impl Into<String>) -> Self {
790        Self {
791            status: StatusCode::TEMPORARY_REDIRECT,
792            location: location.into(),
793        }
794    }
795
796    /// Create a 308 Permanent Redirect.
797    ///
798    /// The request method and body should be preserved when following the redirect.
799    /// This indicates the resource has permanently moved.
800    #[must_use]
801    pub fn permanent(location: impl Into<String>) -> Self {
802        Self {
803            status: StatusCode::PERMANENT_REDIRECT,
804            location: location.into(),
805        }
806    }
807
808    /// Create a 303 See Other redirect.
809    ///
810    /// The client should use GET to fetch the redirected resource.
811    /// Commonly used for POST/redirect/GET pattern.
812    #[must_use]
813    pub fn see_other(location: impl Into<String>) -> Self {
814        Self {
815            status: StatusCode::SEE_OTHER,
816            location: location.into(),
817        }
818    }
819
820    /// Create a 301 Moved Permanently redirect.
821    ///
822    /// Note: Browsers may change POST to GET. Use 308 for method preservation.
823    #[must_use]
824    pub fn moved_permanently(location: impl Into<String>) -> Self {
825        Self {
826            status: StatusCode::MOVED_PERMANENTLY,
827            location: location.into(),
828        }
829    }
830
831    /// Create a 302 Found redirect.
832    ///
833    /// Note: Browsers may change POST to GET. Use 307 for method preservation.
834    #[must_use]
835    pub fn found(location: impl Into<String>) -> Self {
836        Self {
837            status: StatusCode::FOUND,
838            location: location.into(),
839        }
840    }
841
842    /// Get the redirect location.
843    #[must_use]
844    pub fn location(&self) -> &str {
845        &self.location
846    }
847
848    /// Get the status code.
849    #[must_use]
850    pub fn status(&self) -> StatusCode {
851        self.status
852    }
853}
854
855impl IntoResponse for Redirect {
856    fn into_response(self) -> Response {
857        Response::with_status(self.status).header("location", self.location.into_bytes())
858    }
859}
860
861/// HTML response with proper content-type.
862///
863/// # Examples
864///
865/// ```
866/// use fastapi_core::Html;
867///
868/// let response = Html::new("<html><body>Hello</body></html>");
869/// ```
870#[derive(Debug, Clone)]
871pub struct Html(String);
872
873impl Html {
874    /// Create a new HTML response from trusted content.
875    ///
876    /// # Safety Note
877    ///
878    /// This method does NOT escape the content. Only use with trusted HTML.
879    /// For user-provided content, use [`Html::escaped`] instead to prevent XSS.
880    #[must_use]
881    pub fn new(content: impl Into<String>) -> Self {
882        Self(content.into())
883    }
884
885    /// Create an HTML response with the content escaped to prevent XSS.
886    ///
887    /// Use this method when including any user-provided content in HTML.
888    /// Characters `& < > " '` are escaped to their HTML entities.
889    #[must_use]
890    pub fn escaped(content: impl AsRef<str>) -> Self {
891        Self(escape_html(content.as_ref()))
892    }
893
894    /// Get the HTML content.
895    #[must_use]
896    pub fn content(&self) -> &str {
897        &self.0
898    }
899}
900
901/// Escape HTML special characters to prevent XSS attacks.
902fn escape_html(s: &str) -> String {
903    let mut out = String::with_capacity(s.len());
904    for c in s.chars() {
905        match c {
906            '&' => out.push_str("&amp;"),
907            '<' => out.push_str("&lt;"),
908            '>' => out.push_str("&gt;"),
909            '"' => out.push_str("&quot;"),
910            '\'' => out.push_str("&#x27;"),
911            _ => out.push(c),
912        }
913    }
914    out
915}
916
917impl IntoResponse for Html {
918    fn into_response(self) -> Response {
919        Response::ok()
920            .header("content-type", b"text/html; charset=utf-8".to_vec())
921            .body(ResponseBody::Bytes(self.0.into_bytes()))
922    }
923}
924
925impl<S: Into<String>> From<S> for Html {
926    fn from(s: S) -> Self {
927        Self::new(s)
928    }
929}
930
931/// Plain text response with proper content-type.
932///
933/// While `String` and `&str` already implement `IntoResponse` as plain text,
934/// this type provides an explicit way to indicate text content.
935///
936/// # Examples
937///
938/// ```
939/// use fastapi_core::Text;
940///
941/// let response = Text::new("Hello, World!");
942/// ```
943#[derive(Debug, Clone)]
944pub struct Text(String);
945
946impl Text {
947    /// Create a new plain text response.
948    #[must_use]
949    pub fn new(content: impl Into<String>) -> Self {
950        Self(content.into())
951    }
952
953    /// Get the text content.
954    #[must_use]
955    pub fn content(&self) -> &str {
956        &self.0
957    }
958}
959
960impl IntoResponse for Text {
961    fn into_response(self) -> Response {
962        Response::ok()
963            .header("content-type", b"text/plain; charset=utf-8".to_vec())
964            .body(ResponseBody::Bytes(self.0.into_bytes()))
965    }
966}
967
968impl<S: Into<String>> From<S> for Text {
969    fn from(s: S) -> Self {
970        Self::new(s)
971    }
972}
973
974/// No Content (204) response.
975///
976/// Used for successful operations that don't return a body,
977/// such as DELETE operations.
978///
979/// # Examples
980///
981/// ```
982/// use fastapi_core::NoContent;
983///
984/// // After a successful DELETE
985/// let response = NoContent;
986/// ```
987#[derive(Debug, Clone, Copy, Default)]
988pub struct NoContent;
989
990impl IntoResponse for NoContent {
991    fn into_response(self) -> Response {
992        Response::no_content()
993    }
994}
995
996/// Binary response with `application/octet-stream` content type.
997///
998/// Use this for raw binary data that doesn't have a specific MIME type.
999///
1000/// # Examples
1001///
1002/// ```
1003/// use fastapi_core::Binary;
1004///
1005/// let data = vec![0x00, 0x01, 0x02, 0x03];
1006/// let response = Binary::new(data);
1007/// ```
1008#[derive(Debug, Clone)]
1009pub struct Binary(Vec<u8>);
1010
1011impl Binary {
1012    /// Create a new binary response.
1013    #[must_use]
1014    pub fn new(data: impl Into<Vec<u8>>) -> Self {
1015        Self(data.into())
1016    }
1017
1018    /// Get the binary data.
1019    #[must_use]
1020    pub fn data(&self) -> &[u8] {
1021        &self.0
1022    }
1023
1024    /// Create with a specific content type override.
1025    #[must_use]
1026    pub fn with_content_type(self, content_type: &str) -> BinaryWithType {
1027        BinaryWithType {
1028            data: self.0,
1029            content_type: content_type.to_string(),
1030        }
1031    }
1032}
1033
1034impl IntoResponse for Binary {
1035    fn into_response(self) -> Response {
1036        Response::ok()
1037            .header("content-type", b"application/octet-stream".to_vec())
1038            .body(ResponseBody::Bytes(self.0))
1039    }
1040}
1041
1042impl From<Vec<u8>> for Binary {
1043    fn from(data: Vec<u8>) -> Self {
1044        Self::new(data)
1045    }
1046}
1047
1048impl From<&[u8]> for Binary {
1049    fn from(data: &[u8]) -> Self {
1050        Self::new(data.to_vec())
1051    }
1052}
1053
1054/// Binary response with a custom content type.
1055///
1056/// # Examples
1057///
1058/// ```
1059/// use fastapi_core::Binary;
1060///
1061/// let pdf_data = vec![0x25, 0x50, 0x44, 0x46]; // PDF magic bytes
1062/// let response = Binary::new(pdf_data).with_content_type("application/pdf");
1063/// ```
1064#[derive(Debug, Clone)]
1065pub struct BinaryWithType {
1066    data: Vec<u8>,
1067    content_type: String,
1068}
1069
1070impl BinaryWithType {
1071    /// Get a reference to the underlying data.
1072    pub fn data(&self) -> &[u8] {
1073        &self.data
1074    }
1075
1076    /// Get the content type.
1077    pub fn content_type(&self) -> &str {
1078        &self.content_type
1079    }
1080}
1081
1082impl IntoResponse for BinaryWithType {
1083    fn into_response(self) -> Response {
1084        Response::ok()
1085            .header("content-type", self.content_type.into_bytes())
1086            .body(ResponseBody::Bytes(self.data))
1087    }
1088}
1089
1090/// File response for serving files.
1091///
1092/// Supports:
1093/// - Automatic content-type inference from file extension
1094/// - Optional Content-Disposition for downloads
1095/// - Streaming for large files
1096///
1097/// # Examples
1098///
1099/// ```ignore
1100/// use fastapi_core::response::FileResponse;
1101/// use std::path::Path;
1102///
1103/// // Inline display (images, PDFs in browser)
1104/// let response = FileResponse::new(Path::new("image.png"));
1105///
1106/// // Force download with custom filename
1107/// let response = FileResponse::new(Path::new("data.csv"))
1108///     .download_as("report.csv");
1109/// ```
1110#[derive(Debug)]
1111pub struct FileResponse {
1112    path: std::path::PathBuf,
1113    content_type: Option<String>,
1114    download_name: Option<String>,
1115    inline: bool,
1116}
1117
1118impl FileResponse {
1119    /// Create a new file response.
1120    ///
1121    /// The content-type will be inferred from the file extension.
1122    #[must_use]
1123    pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
1124        Self {
1125            path: path.into(),
1126            content_type: None,
1127            download_name: None,
1128            inline: true,
1129        }
1130    }
1131
1132    /// Override the content-type.
1133    #[must_use]
1134    pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
1135        self.content_type = Some(content_type.into());
1136        self
1137    }
1138
1139    /// Set as download with the specified filename.
1140    ///
1141    /// Sets Content-Disposition: attachment; filename="..."
1142    #[must_use]
1143    pub fn download_as(mut self, filename: impl Into<String>) -> Self {
1144        self.download_name = Some(filename.into());
1145        self.inline = false;
1146        self
1147    }
1148
1149    /// Set as inline content (default).
1150    ///
1151    /// Sets Content-Disposition: inline
1152    #[must_use]
1153    pub fn inline(mut self) -> Self {
1154        self.inline = true;
1155        self.download_name = None;
1156        self
1157    }
1158
1159    /// Get the file path.
1160    #[must_use]
1161    pub fn path(&self) -> &std::path::Path {
1162        &self.path
1163    }
1164
1165    /// Infer content-type from file extension.
1166    fn infer_content_type(&self) -> &'static str {
1167        self.path
1168            .extension()
1169            .and_then(|ext| ext.to_str())
1170            .map(|ext| mime_type_for_extension(ext))
1171            .unwrap_or("application/octet-stream")
1172    }
1173
1174    /// Build the Content-Disposition header value.
1175    fn content_disposition(&self) -> String {
1176        if self.inline {
1177            "inline".to_string()
1178        } else if let Some(ref name) = self.download_name {
1179            // RFC 6266: filename should be quoted and special chars escaped
1180            format!("attachment; filename=\"{}\"", name.replace('"', "\\\""))
1181        } else {
1182            // Use the actual filename from path
1183            let filename = self
1184                .path
1185                .file_name()
1186                .and_then(|n| n.to_str())
1187                .unwrap_or("download");
1188            format!("attachment; filename=\"{}\"", filename.replace('"', "\\\""))
1189        }
1190    }
1191
1192    /// Read file and create response.
1193    ///
1194    /// # Errors
1195    ///
1196    /// Returns an error response if the file cannot be read.
1197    #[must_use]
1198    pub fn into_response_sync(self) -> Response {
1199        match std::fs::read(&self.path) {
1200            Ok(contents) => {
1201                let content_type = self
1202                    .content_type
1203                    .as_deref()
1204                    .unwrap_or_else(|| self.infer_content_type());
1205
1206                Response::ok()
1207                    .header("content-type", content_type.as_bytes().to_vec())
1208                    .header(
1209                        "content-disposition",
1210                        self.content_disposition().into_bytes(),
1211                    )
1212                    .header("accept-ranges", b"bytes".to_vec())
1213                    .body(ResponseBody::Bytes(contents))
1214            }
1215            Err(_) => Response::with_status(StatusCode::NOT_FOUND),
1216        }
1217    }
1218}
1219
1220impl IntoResponse for FileResponse {
1221    fn into_response(self) -> Response {
1222        self.into_response_sync()
1223    }
1224}
1225
1226/// Get MIME type for a file extension.
1227///
1228/// Returns a reasonable MIME type for common file extensions.
1229/// Falls back to "application/octet-stream" for unknown types.
1230#[must_use]
1231pub fn mime_type_for_extension(ext: &str) -> &'static str {
1232    match ext.to_ascii_lowercase().as_str() {
1233        // Text
1234        "html" | "htm" => "text/html; charset=utf-8",
1235        "css" => "text/css; charset=utf-8",
1236        "js" | "mjs" => "text/javascript; charset=utf-8",
1237        "json" | "map" => "application/json",
1238        "xml" => "application/xml",
1239        "txt" => "text/plain; charset=utf-8",
1240        "csv" => "text/csv; charset=utf-8",
1241        "md" => "text/markdown; charset=utf-8",
1242
1243        // Images
1244        "png" => "image/png",
1245        "jpg" | "jpeg" => "image/jpeg",
1246        "gif" => "image/gif",
1247        "webp" => "image/webp",
1248        "svg" => "image/svg+xml",
1249        "ico" => "image/x-icon",
1250        "bmp" => "image/bmp",
1251        "avif" => "image/avif",
1252
1253        // Fonts
1254        "woff" => "font/woff",
1255        "woff2" => "font/woff2",
1256        "ttf" => "font/ttf",
1257        "otf" => "font/otf",
1258        "eot" => "application/vnd.ms-fontobject",
1259
1260        // Audio
1261        "mp3" => "audio/mpeg",
1262        "wav" => "audio/wav",
1263        "ogg" => "audio/ogg",
1264        "flac" => "audio/flac",
1265        "aac" => "audio/aac",
1266        "m4a" => "audio/mp4",
1267
1268        // Video
1269        "mp4" => "video/mp4",
1270        "webm" => "video/webm",
1271        "avi" => "video/x-msvideo",
1272        "mov" => "video/quicktime",
1273        "mkv" => "video/x-matroska",
1274
1275        // Documents
1276        "pdf" => "application/pdf",
1277        "doc" => "application/msword",
1278        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1279        "xls" => "application/vnd.ms-excel",
1280        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1281        "ppt" => "application/vnd.ms-powerpoint",
1282        "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1283
1284        // Archives
1285        "zip" => "application/zip",
1286        "gz" | "gzip" => "application/gzip",
1287        "tar" => "application/x-tar",
1288        "rar" => "application/vnd.rar",
1289        "7z" => "application/x-7z-compressed",
1290
1291        // Other
1292        "wasm" => "application/wasm",
1293
1294        _ => "application/octet-stream",
1295    }
1296}
1297
1298/// Convert an asupersync Outcome into an HTTP response.
1299///
1300/// This is the canonical mapping used by the framework when handlers return
1301/// Outcome values. It preserves normal success/error responses while
1302/// translating cancellations and panics into appropriate HTTP status codes.
1303#[must_use]
1304#[allow(dead_code)] // Will be used when TCP server is wired up
1305pub fn outcome_to_response<T, E>(outcome: Outcome<T, E>) -> Response
1306where
1307    T: IntoResponse,
1308    E: IntoResponse,
1309{
1310    match outcome {
1311        Outcome::Ok(value) => value.into_response(),
1312        Outcome::Err(err) => err.into_response(),
1313        Outcome::Cancelled(reason) => cancelled_to_response(&reason),
1314        Outcome::Panicked(_payload) => Response::with_status(StatusCode::INTERNAL_SERVER_ERROR),
1315    }
1316}
1317
1318#[allow(dead_code)] // Will be used when TCP server is wired up
1319fn cancelled_to_response(reason: &CancelReason) -> Response {
1320    let status = match reason.kind() {
1321        CancelKind::Timeout => StatusCode::GATEWAY_TIMEOUT,
1322        CancelKind::Shutdown => StatusCode::SERVICE_UNAVAILABLE,
1323        _ => StatusCode::CLIENT_CLOSED_REQUEST,
1324    };
1325    Response::with_status(status)
1326}
1327
1328// ============================================================================
1329// Response Model Configuration
1330// ============================================================================
1331
1332/// Configuration for response model serialization.
1333///
1334/// This provides FastAPI-compatible options for controlling how response
1335/// data is serialized and validated before sending to clients.
1336///
1337/// # Examples
1338///
1339/// ```
1340/// use fastapi_core::ResponseModelConfig;
1341/// use std::collections::HashSet;
1342///
1343/// // Only include specific fields
1344/// let config = ResponseModelConfig::new()
1345///     .include(["id", "name", "email"].into_iter().map(String::from).collect());
1346///
1347/// // Exclude sensitive fields
1348/// let config = ResponseModelConfig::new()
1349///     .exclude(["password", "internal_notes"].into_iter().map(String::from).collect());
1350///
1351/// // Use field aliases in output
1352/// let config = ResponseModelConfig::new()
1353///     .by_alias(true);
1354/// ```
1355#[derive(Debug, Clone, Default)]
1356#[allow(clippy::struct_excessive_bools)] // Mirrors FastAPI's response_model options
1357pub struct ResponseModelConfig {
1358    /// Only include these fields in the response.
1359    /// If None, all fields are included (subject to exclude).
1360    pub include: Option<std::collections::HashSet<String>>,
1361
1362    /// Exclude these fields from the response.
1363    pub exclude: Option<std::collections::HashSet<String>>,
1364
1365    /// Use serde aliases in output field names.
1366    pub by_alias: bool,
1367
1368    /// Exclude fields that were not explicitly set.
1369    /// Requires the type to track which fields were set.
1370    pub exclude_unset: bool,
1371
1372    /// Exclude fields that have their default values.
1373    pub exclude_defaults: bool,
1374
1375    /// Exclude fields with None values.
1376    pub exclude_none: bool,
1377
1378    /// Optional alias metadata for `by_alias` transformations.
1379    ///
1380    /// In Rust/serde, field rename behavior is compile-time. To support FastAPI-style
1381    /// `response_model_by_alias` at runtime, callers must provide an explicit mapping
1382    /// from canonical field names to alias field names.
1383    ///
1384    /// The recommended way is to derive it:
1385    /// `#[derive(fastapi_macros::ResponseModelAliases)]` and then use
1386    /// `ResponseModelConfig::with_aliases_from::<T>()`.
1387    aliases: Option<&'static [(&'static str, &'static str)]>,
1388
1389    /// Optional provider for a default JSON value for the model.
1390    ///
1391    /// This is required to implement FastAPI-style `response_model_exclude_defaults`
1392    /// without reflection. Provide it with `ResponseModelConfig::with_defaults_from::<T>()`
1393    /// where `T: Default + Serialize`.
1394    defaults_json: Option<fn() -> Result<serde_json::Value, String>>,
1395
1396    /// Optional set of canonical field names explicitly set for this response instance.
1397    ///
1398    /// This is required to implement FastAPI-style `response_model_exclude_unset`.
1399    set_fields: Option<std::collections::HashSet<String>>,
1400}
1401
1402/// Compile-time response model metadata: canonical field names to alias field names.
1403///
1404/// Implement this with `#[derive(fastapi_macros::ResponseModelAliases)]`.
1405pub trait ResponseModelAliases {
1406    /// Returns a mapping of `(canonical_name, alias_name)` pairs.
1407    ///
1408    /// Entries with identical names are allowed, but derive implementations typically
1409    /// only include pairs where the alias differs from the canonical name.
1410    fn response_model_aliases() -> &'static [(&'static str, &'static str)];
1411}
1412
1413impl ResponseModelConfig {
1414    /// Create a new configuration with defaults.
1415    #[must_use]
1416    pub fn new() -> Self {
1417        Self::default()
1418    }
1419
1420    /// Set fields to include (whitelist).
1421    #[must_use]
1422    pub fn include(mut self, fields: std::collections::HashSet<String>) -> Self {
1423        self.include = Some(fields);
1424        self
1425    }
1426
1427    /// Set fields to exclude (blacklist).
1428    #[must_use]
1429    pub fn exclude(mut self, fields: std::collections::HashSet<String>) -> Self {
1430        self.exclude = Some(fields);
1431        self
1432    }
1433
1434    /// Use serde aliases in output.
1435    ///
1436    /// To make this meaningful, supply alias metadata using
1437    /// `with_aliases(...)` or `with_aliases_from::<T>()`.
1438    #[must_use]
1439    pub fn by_alias(mut self, value: bool) -> Self {
1440        self.by_alias = value;
1441        self
1442    }
1443
1444    /// Exclude unset fields.
1445    ///
1446    /// To make this meaningful, supply the per-response set field list using
1447    /// `with_set_fields(...)`.
1448    #[must_use]
1449    pub fn exclude_unset(mut self, value: bool) -> Self {
1450        self.exclude_unset = value;
1451        self
1452    }
1453
1454    /// Exclude fields with default values.
1455    ///
1456    /// To make this meaningful, supply default JSON via
1457    /// `with_defaults_from::<T>()` where `T: Default + Serialize`, or
1458    /// `with_defaults_json_provider(...)`.
1459    #[must_use]
1460    pub fn exclude_defaults(mut self, value: bool) -> Self {
1461        self.exclude_defaults = value;
1462        self
1463    }
1464
1465    /// Exclude fields with None values.
1466    #[must_use]
1467    pub fn exclude_none(mut self, value: bool) -> Self {
1468        self.exclude_none = value;
1469        self
1470    }
1471
1472    /// Provide alias metadata explicitly.
1473    #[must_use]
1474    pub fn with_aliases(mut self, aliases: &'static [(&'static str, &'static str)]) -> Self {
1475        self.aliases = Some(aliases);
1476        self
1477    }
1478
1479    /// Provide alias metadata from a type-level provider.
1480    #[must_use]
1481    pub fn with_aliases_from<T: ResponseModelAliases>(mut self) -> Self {
1482        self.aliases = Some(T::response_model_aliases());
1483        self
1484    }
1485
1486    /// Provide a default JSON provider explicitly.
1487    #[must_use]
1488    pub fn with_defaults_json_provider(
1489        mut self,
1490        provider: fn() -> Result<serde_json::Value, String>,
1491    ) -> Self {
1492        self.defaults_json = Some(provider);
1493        self
1494    }
1495
1496    fn defaults_json_for<T: Default + Serialize>() -> Result<serde_json::Value, String> {
1497        serde_json::to_value(T::default()).map_err(|e| e.to_string())
1498    }
1499
1500    /// Provide default JSON for the model via `T::default()`.
1501    #[must_use]
1502    pub fn with_defaults_from<T: Default + Serialize>(mut self) -> Self {
1503        self.defaults_json = Some(Self::defaults_json_for::<T>);
1504        self
1505    }
1506
1507    /// Provide the set of canonical field names that were explicitly set for this response.
1508    #[must_use]
1509    pub fn with_set_fields(mut self, fields: std::collections::HashSet<String>) -> Self {
1510        self.set_fields = Some(fields);
1511        self
1512    }
1513
1514    /// Check if any filtering is configured.
1515    #[must_use]
1516    pub fn has_filtering(&self) -> bool {
1517        self.include.is_some()
1518            || self.exclude.is_some()
1519            || self.exclude_none
1520            || self.exclude_unset
1521            || self.exclude_defaults
1522            || self.by_alias
1523    }
1524
1525    /// Apply filtering to a JSON value.
1526    ///
1527    /// This filters the JSON according to the configuration:
1528    /// - Applies include whitelist
1529    /// - Applies exclude blacklist
1530    /// - Removes None values if exclude_none is set
1531    #[allow(clippy::result_large_err)]
1532    pub fn filter_json(
1533        &self,
1534        value: serde_json::Value,
1535    ) -> Result<serde_json::Value, crate::error::ResponseValidationError> {
1536        let serde_json::Value::Object(mut map) = value else {
1537            return Ok(value);
1538        };
1539
1540        // Normalize to canonical field names first so include/exclude/set_fields operate
1541        // on stable names even when serde serialization uses aliases.
1542        if let Some(aliases) = self.aliases {
1543            normalize_to_canonical(&mut map, aliases)?;
1544        }
1545
1546        // Exclude unset fields (requires per-response set field list).
1547        if self.exclude_unset {
1548            let set_fields = self.set_fields.as_ref().ok_or_else(|| {
1549                crate::error::ResponseValidationError::serialization_failed(
1550                    "response_model_exclude_unset requires set-fields metadata \
1551                     (use ResponseModelConfig::with_set_fields)",
1552                )
1553            })?;
1554            map.retain(|k, _| set_fields.contains(k));
1555        }
1556
1557        // Apply include whitelist
1558        if let Some(ref include_set) = self.include {
1559            map.retain(|key, _| include_set.contains(key));
1560        }
1561
1562        // Apply exclude blacklist
1563        if let Some(ref exclude_set) = self.exclude {
1564            map.retain(|key, _| !exclude_set.contains(key));
1565        }
1566
1567        // Remove None values if configured
1568        if self.exclude_none {
1569            map.retain(|_, v| !v.is_null());
1570        }
1571
1572        // Exclude defaults (requires default JSON provider).
1573        if self.exclude_defaults {
1574            let provider = self.defaults_json.ok_or_else(|| {
1575                crate::error::ResponseValidationError::serialization_failed(
1576                    "response_model_exclude_defaults requires defaults metadata \
1577                     (use ResponseModelConfig::with_defaults_from::<T>() or \
1578                      ResponseModelConfig::with_defaults_json_provider)",
1579                )
1580            })?;
1581            let defaults =
1582                provider().map_err(crate::error::ResponseValidationError::serialization_failed)?;
1583            let serde_json::Value::Object(defaults_map) = defaults else {
1584                return Err(crate::error::ResponseValidationError::serialization_failed(
1585                    "defaults provider did not return a JSON object",
1586                ));
1587            };
1588
1589            // Remove keys that match the default value exactly.
1590            for (k, default_v) in defaults_map {
1591                if map.get(&k).is_some_and(|v| v == &default_v) {
1592                    map.remove(&k);
1593                }
1594            }
1595        }
1596
1597        // Apply aliases for output (requires alias metadata).
1598        if self.by_alias {
1599            let aliases = self.aliases.ok_or_else(|| {
1600                crate::error::ResponseValidationError::serialization_failed(
1601                    "response_model_by_alias requires alias metadata \
1602                     (use ResponseModelConfig::with_aliases(...) or \
1603                      ResponseModelConfig::with_aliases_from::<T>())",
1604                )
1605            })?;
1606            apply_aliases(&mut map, aliases)?;
1607        }
1608
1609        Ok(serde_json::Value::Object(map))
1610    }
1611}
1612
1613#[allow(clippy::result_large_err)]
1614fn normalize_to_canonical(
1615    map: &mut serde_json::Map<String, serde_json::Value>,
1616    aliases: &[(&'static str, &'static str)],
1617) -> Result<(), crate::error::ResponseValidationError> {
1618    for (canonical, alias) in aliases {
1619        if canonical == alias {
1620            continue;
1621        }
1622        let canonical = *canonical;
1623        let alias = *alias;
1624
1625        if map.contains_key(canonical) && map.contains_key(alias) {
1626            // Ambiguous: both names exist.
1627            return Err(crate::error::ResponseValidationError::serialization_failed(
1628                format!(
1629                    "response model contains both canonical field '{canonical}' and alias '{alias}'"
1630                ),
1631            ));
1632        }
1633
1634        if let Some(v) = map.remove(alias) {
1635            map.insert(canonical.to_string(), v);
1636        }
1637    }
1638    Ok(())
1639}
1640
1641#[allow(clippy::result_large_err)]
1642fn apply_aliases(
1643    map: &mut serde_json::Map<String, serde_json::Value>,
1644    aliases: &[(&'static str, &'static str)],
1645) -> Result<(), crate::error::ResponseValidationError> {
1646    for (canonical, alias) in aliases {
1647        if canonical == alias {
1648            continue;
1649        }
1650        let canonical = *canonical;
1651        let alias = *alias;
1652
1653        if map.contains_key(canonical) && map.contains_key(alias) {
1654            return Err(crate::error::ResponseValidationError::serialization_failed(
1655                format!(
1656                    "response model contains both canonical field '{canonical}' and alias '{alias}'"
1657                ),
1658            ));
1659        }
1660
1661        if let Some(v) = map.remove(canonical) {
1662            map.insert(alias.to_string(), v);
1663        }
1664    }
1665    Ok(())
1666}
1667
1668/// Trait for types that can be validated as response models.
1669///
1670/// This allows custom validation logic to be applied before serialization.
1671/// Types implementing this trait can verify that the response data is valid
1672/// according to the declared response model.
1673pub trait ResponseModel: Serialize {
1674    /// Validate the response model before serialization.
1675    ///
1676    /// Returns Ok(()) if valid, or a validation error if invalid.
1677    #[allow(clippy::result_large_err)] // Error provides detailed validation context
1678    fn validate(&self) -> Result<(), crate::error::ResponseValidationError> {
1679        // Default implementation: no validation
1680        Ok(())
1681    }
1682
1683    /// Get the model name for error messages.
1684    fn model_name() -> &'static str {
1685        std::any::type_name::<Self>()
1686    }
1687}
1688
1689// Blanket implementation for all Serialize types
1690impl<T: Serialize> ResponseModel for T {}
1691
1692/// A validated response with its configuration.
1693///
1694/// This wraps a response value with its model configuration, ensuring
1695/// the response is validated and filtered before sending.
1696///
1697/// # Examples
1698///
1699/// ```
1700/// use fastapi_core::{ValidatedResponse, ResponseModelConfig};
1701/// use serde::Serialize;
1702///
1703/// #[derive(Serialize)]
1704/// struct User {
1705///     id: i64,
1706///     name: String,
1707///     email: String,
1708///     password_hash: String,
1709/// }
1710///
1711/// let user = User {
1712///     id: 1,
1713///     name: "Alice".to_string(),
1714///     email: "alice@example.com".to_string(),
1715///     password_hash: "secret123".to_string(),
1716/// };
1717///
1718/// // Create a validated response that excludes the password
1719/// let response = ValidatedResponse::new(user)
1720///     .with_config(ResponseModelConfig::new()
1721///         .exclude(["password_hash"].into_iter().map(String::from).collect()));
1722/// ```
1723#[derive(Debug)]
1724pub struct ValidatedResponse<T> {
1725    /// The response value.
1726    pub value: T,
1727    /// The serialization configuration.
1728    pub config: ResponseModelConfig,
1729}
1730
1731impl<T> ValidatedResponse<T> {
1732    /// Create a new validated response.
1733    #[must_use]
1734    pub fn new(value: T) -> Self {
1735        Self {
1736            value,
1737            config: ResponseModelConfig::default(),
1738        }
1739    }
1740
1741    /// Set the serialization configuration.
1742    #[must_use]
1743    pub fn with_config(mut self, config: ResponseModelConfig) -> Self {
1744        self.config = config;
1745        self
1746    }
1747}
1748
1749impl<T: Serialize + ResponseModel> IntoResponse for ValidatedResponse<T> {
1750    fn into_response(self) -> Response {
1751        // First validate the response model
1752        if let Err(error) = self.value.validate() {
1753            return error.into_response();
1754        }
1755
1756        // Serialize to JSON
1757        let json_value = match serde_json::to_value(&self.value) {
1758            Ok(v) => v,
1759            Err(e) => {
1760                // Serialization failed - return 500
1761                let error =
1762                    crate::error::ResponseValidationError::serialization_failed(e.to_string());
1763                return error.into_response();
1764            }
1765        };
1766
1767        // Apply filtering
1768        let filtered = match self.config.filter_json(json_value) {
1769            Ok(v) => v,
1770            Err(e) => return e.into_response(),
1771        };
1772
1773        // Serialize the filtered value
1774        let bytes = match serde_json::to_vec(&filtered) {
1775            Ok(b) => b,
1776            Err(e) => {
1777                let error =
1778                    crate::error::ResponseValidationError::serialization_failed(e.to_string());
1779                return error.into_response();
1780            }
1781        };
1782
1783        Response::ok()
1784            .header("content-type", b"application/json".to_vec())
1785            .body(ResponseBody::Bytes(bytes))
1786    }
1787}
1788
1789/// Macro helper for creating validated responses with field exclusion.
1790///
1791/// This is a convenience wrapper that excludes specified fields from the response.
1792#[must_use]
1793pub fn exclude_fields<T: Serialize + ResponseModel>(
1794    value: T,
1795    fields: &[&str],
1796) -> ValidatedResponse<T> {
1797    ValidatedResponse::new(value).with_config(
1798        ResponseModelConfig::new().exclude(fields.iter().map(|s| (*s).to_string()).collect()),
1799    )
1800}
1801
1802/// Macro helper for creating validated responses with field inclusion.
1803///
1804/// This is a convenience wrapper that only includes specified fields in the response.
1805#[must_use]
1806pub fn include_fields<T: Serialize + ResponseModel>(
1807    value: T,
1808    fields: &[&str],
1809) -> ValidatedResponse<T> {
1810    ValidatedResponse::new(value).with_config(
1811        ResponseModelConfig::new().include(fields.iter().map(|s| (*s).to_string()).collect()),
1812    )
1813}
1814
1815// ============================================================================
1816// Conditional request (ETag / If-None-Match / If-Match) utilities
1817// ============================================================================
1818
1819/// Check an `If-None-Match` header value against an ETag.
1820///
1821/// Returns `true` if the condition is met (i.e., the resource HAS changed and the
1822/// full response should be sent). Returns `false` if a 304 Not Modified should be
1823/// returned instead.
1824///
1825/// Handles:
1826/// - `*` wildcard (matches any ETag)
1827/// - Multiple comma-separated ETags
1828/// - Weak ETag comparison (W/ prefix stripped for comparison)
1829///
1830/// # Example
1831///
1832/// ```ignore
1833/// use fastapi_core::response::check_if_none_match;
1834///
1835/// let current_etag = "\"abc123\"";
1836/// let if_none_match = "\"abc123\"";
1837/// // Returns false: ETag matches, so send 304
1838/// assert!(!check_if_none_match(if_none_match, current_etag));
1839/// ```
1840pub fn check_if_none_match(if_none_match: &str, current_etag: &str) -> bool {
1841    let if_none_match = if_none_match.trim();
1842
1843    // Wildcard matches everything
1844    if if_none_match == "*" {
1845        return false; // Resource exists, send 304
1846    }
1847
1848    let current_stripped = strip_weak_prefix(current_etag.trim());
1849
1850    // Check each ETag in the comma-separated list
1851    for candidate in if_none_match.split(',') {
1852        let candidate = strip_weak_prefix(candidate.trim());
1853        if candidate == current_stripped {
1854            return false; // Match found, send 304
1855        }
1856    }
1857
1858    true // No match, send full response
1859}
1860
1861/// Check an `If-Match` header value against an ETag.
1862///
1863/// Returns `true` if the precondition is met (the resource matches and the
1864/// request should proceed). Returns `false` if a 412 Precondition Failed
1865/// should be returned.
1866///
1867/// Uses strong comparison (W/ weak ETags never match).
1868pub fn check_if_match(if_match: &str, current_etag: &str) -> bool {
1869    let if_match = if_match.trim();
1870
1871    // Wildcard matches everything
1872    if if_match == "*" {
1873        return true;
1874    }
1875
1876    let current = current_etag.trim();
1877
1878    // Weak ETags never match for If-Match (strong comparison required)
1879    if current.starts_with("W/") {
1880        return false;
1881    }
1882
1883    for candidate in if_match.split(',') {
1884        let candidate = candidate.trim();
1885        // Weak ETags don't match in strong comparison
1886        if candidate.starts_with("W/") {
1887            continue;
1888        }
1889        if candidate == current {
1890            return true;
1891        }
1892    }
1893
1894    false
1895}
1896
1897/// Strip the `W/` weak ETag prefix for weak comparison.
1898fn strip_weak_prefix(etag: &str) -> &str {
1899    etag.strip_prefix("W/").unwrap_or(etag)
1900}
1901
1902/// Evaluate conditional request headers against a response and return the
1903/// appropriate response (304, 412, or the original).
1904///
1905/// This checks `If-None-Match` (for GET/HEAD) and `If-Match` (for PUT/PATCH/DELETE)
1906/// against the response's ETag header.
1907///
1908/// # Arguments
1909///
1910/// * `request_headers` - Iterator of (name, value) pairs from the request
1911/// * `method` - The HTTP method
1912/// * `response` - The prepared response (must have ETag header set)
1913///
1914/// # Returns
1915///
1916/// Either the original response or a 304/412 response as appropriate.
1917pub fn apply_conditional(
1918    request_headers: &[(String, Vec<u8>)],
1919    method: crate::request::Method,
1920    response: Response,
1921) -> Response {
1922    // Find the ETag from the response
1923    let response_etag = response
1924        .headers()
1925        .iter()
1926        .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
1927        .and_then(|(_, value)| std::str::from_utf8(value).ok())
1928        .map(String::from);
1929
1930    let Some(response_etag) = response_etag else {
1931        return response; // No ETag, can't do conditional
1932    };
1933
1934    // Check If-None-Match (for GET/HEAD - returns 304)
1935    if matches!(
1936        method,
1937        crate::request::Method::Get | crate::request::Method::Head
1938    ) {
1939        if let Some(if_none_match) = find_header(request_headers, "if-none-match") {
1940            if !check_if_none_match(&if_none_match, &response_etag) {
1941                return Response::not_modified().with_etag(response_etag);
1942            }
1943        }
1944    }
1945
1946    // Check If-Match (for unsafe methods - returns 412)
1947    if matches!(
1948        method,
1949        crate::request::Method::Put
1950            | crate::request::Method::Patch
1951            | crate::request::Method::Delete
1952    ) {
1953        if let Some(if_match) = find_header(request_headers, "if-match") {
1954            if !check_if_match(&if_match, &response_etag) {
1955                return Response::precondition_failed();
1956            }
1957        }
1958    }
1959
1960    response
1961}
1962
1963/// Find a header value by name (case-insensitive).
1964fn find_header(headers: &[(String, Vec<u8>)], name: &str) -> Option<String> {
1965    headers
1966        .iter()
1967        .find(|(n, _)| n.eq_ignore_ascii_case(name))
1968        .and_then(|(_, v)| std::str::from_utf8(v).ok())
1969        .map(String::from)
1970}
1971
1972// ============================================================================
1973// Link Header (RFC 8288)
1974// ============================================================================
1975
1976/// Link relation type per RFC 8288.
1977#[derive(Debug, Clone, PartialEq, Eq)]
1978pub enum LinkRel {
1979    /// The current resource.
1980    Self_,
1981    /// Next page in a paginated collection.
1982    Next,
1983    /// Previous page in a paginated collection.
1984    Prev,
1985    /// First page in a paginated collection.
1986    First,
1987    /// Last page in a paginated collection.
1988    Last,
1989    /// A related resource.
1990    Related,
1991    /// An alternate representation.
1992    Alternate,
1993    /// Custom relation type.
1994    Custom(String),
1995}
1996
1997impl fmt::Display for LinkRel {
1998    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1999        match self {
2000            Self::Self_ => write!(f, "self"),
2001            Self::Next => write!(f, "next"),
2002            Self::Prev => write!(f, "prev"),
2003            Self::First => write!(f, "first"),
2004            Self::Last => write!(f, "last"),
2005            Self::Related => write!(f, "related"),
2006            Self::Alternate => write!(f, "alternate"),
2007            Self::Custom(s) => write!(f, "{s}"),
2008        }
2009    }
2010}
2011
2012/// A single link entry in a Link header.
2013#[derive(Debug, Clone)]
2014pub struct Link {
2015    url: String,
2016    rel: LinkRel,
2017    title: Option<String>,
2018    media_type: Option<String>,
2019}
2020
2021impl Link {
2022    /// Create a new link with the given URL and relation.
2023    pub fn new(url: impl Into<String>, rel: LinkRel) -> Self {
2024        Self {
2025            url: url.into(),
2026            rel,
2027            title: None,
2028            media_type: None,
2029        }
2030    }
2031
2032    /// Set the title parameter.
2033    #[must_use]
2034    pub fn title(mut self, title: impl Into<String>) -> Self {
2035        self.title = Some(title.into());
2036        self
2037    }
2038
2039    /// Set the type parameter (media type).
2040    #[must_use]
2041    pub fn media_type(mut self, media_type: impl Into<String>) -> Self {
2042        self.media_type = Some(media_type.into());
2043        self
2044    }
2045}
2046
2047impl fmt::Display for Link {
2048    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2049        write!(f, "<{}>; rel=\"{}\"", self.url, self.rel)?;
2050        if let Some(ref title) = self.title {
2051            write!(f, "; title=\"{title}\"")?;
2052        }
2053        if let Some(ref mt) = self.media_type {
2054            write!(f, "; type=\"{mt}\"")?;
2055        }
2056        Ok(())
2057    }
2058}
2059
2060/// Builder for constructing RFC 8288 Link headers.
2061///
2062/// Supports multiple links in a single header value, pagination helpers,
2063/// and custom relation types.
2064///
2065/// # Example
2066///
2067/// ```
2068/// use fastapi_core::{LinkHeader, LinkRel};
2069///
2070/// let header = LinkHeader::new()
2071///     .link("https://api.example.com/users?page=2", LinkRel::Next)
2072///     .link("https://api.example.com/users?page=1", LinkRel::Prev)
2073///     .link("https://api.example.com/users?page=1", LinkRel::First)
2074///     .link("https://api.example.com/users?page=5", LinkRel::Last);
2075///
2076/// assert!(header.to_string().contains("rel=\"next\""));
2077/// ```
2078#[derive(Debug, Clone, Default)]
2079pub struct LinkHeader {
2080    links: Vec<Link>,
2081}
2082
2083impl LinkHeader {
2084    /// Create an empty link header builder.
2085    #[must_use]
2086    pub fn new() -> Self {
2087        Self::default()
2088    }
2089
2090    /// Add a link with the given URL and relation.
2091    #[must_use]
2092    pub fn link(mut self, url: impl Into<String>, rel: LinkRel) -> Self {
2093        self.links.push(Link::new(url, rel));
2094        self
2095    }
2096
2097    /// Add a fully configured [`Link`] entry.
2098    #[must_use]
2099    #[allow(clippy::should_implement_trait)]
2100    pub fn add(mut self, link: Link) -> Self {
2101        self.links.push(link);
2102        self
2103    }
2104
2105    /// Add pagination links from page/per_page/total parameters.
2106    ///
2107    /// Generates `first`, `last`, `next`, `prev`, and `self` links
2108    /// using the given base URL and query parameters.
2109    #[must_use]
2110    pub fn paginate(self, base_url: &str, page: u64, per_page: u64, total: u64) -> Self {
2111        let last_page = if total == 0 {
2112            1
2113        } else {
2114            total.div_ceil(per_page)
2115        };
2116        let sep = if base_url.contains('?') { '&' } else { '?' };
2117
2118        let mut h = self.link(
2119            format!("{base_url}{sep}page={page}&per_page={per_page}"),
2120            LinkRel::Self_,
2121        );
2122        h = h.link(
2123            format!("{base_url}{sep}page=1&per_page={per_page}"),
2124            LinkRel::First,
2125        );
2126        h = h.link(
2127            format!("{base_url}{sep}page={last_page}&per_page={per_page}"),
2128            LinkRel::Last,
2129        );
2130        if page > 1 {
2131            h = h.link(
2132                format!("{base_url}{sep}page={}&per_page={per_page}", page - 1),
2133                LinkRel::Prev,
2134            );
2135        }
2136        if page < last_page {
2137            h = h.link(
2138                format!("{base_url}{sep}page={}&per_page={per_page}", page + 1),
2139                LinkRel::Next,
2140            );
2141        }
2142        h
2143    }
2144
2145    /// Returns true if no links have been added.
2146    #[must_use]
2147    pub fn is_empty(&self) -> bool {
2148        self.links.is_empty()
2149    }
2150
2151    /// Returns the number of links.
2152    #[must_use]
2153    pub fn len(&self) -> usize {
2154        self.links.len()
2155    }
2156
2157    /// Convert to the header value string (RFC 8288 format).
2158    #[must_use]
2159    pub fn to_header_value(&self) -> String {
2160        self.to_string()
2161    }
2162
2163    /// Apply this Link header to a response.
2164    pub fn apply(self, response: Response) -> Response {
2165        if self.is_empty() {
2166            return response;
2167        }
2168        response.header("link", self.to_string().into_bytes())
2169    }
2170}
2171
2172impl fmt::Display for LinkHeader {
2173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2174        for (i, link) in self.links.iter().enumerate() {
2175            if i > 0 {
2176                write!(f, ", ")?;
2177            }
2178            write!(f, "{link}")?;
2179        }
2180        Ok(())
2181    }
2182}
2183
2184#[cfg(test)]
2185mod tests {
2186    use super::*;
2187    use crate::error::HttpError;
2188
2189    #[test]
2190    fn response_remove_header_removes_all_instances_case_insensitive() {
2191        let resp = Response::ok()
2192            .header("X-Test", b"1".to_vec())
2193            .header("x-test", b"2".to_vec())
2194            .header("Other", b"3".to_vec())
2195            .remove_header("X-Test");
2196
2197        assert!(
2198            resp.headers()
2199                .iter()
2200                .all(|(n, _)| !n.eq_ignore_ascii_case("x-test"))
2201        );
2202        assert!(
2203            resp.headers()
2204                .iter()
2205                .any(|(n, _)| n.eq_ignore_ascii_case("other"))
2206        );
2207    }
2208
2209    #[test]
2210    fn outcome_ok_maps_to_response() {
2211        let response = Response::created();
2212        let mapped = outcome_to_response::<Response, HttpError>(Outcome::Ok(response));
2213        assert_eq!(mapped.status().as_u16(), 201);
2214    }
2215
2216    #[test]
2217    fn outcome_err_maps_to_response() {
2218        let mapped =
2219            outcome_to_response::<Response, HttpError>(Outcome::Err(HttpError::bad_request()));
2220        assert_eq!(mapped.status().as_u16(), 400);
2221    }
2222
2223    #[test]
2224    fn outcome_cancelled_timeout_maps_to_504() {
2225        let mapped =
2226            outcome_to_response::<Response, HttpError>(Outcome::Cancelled(CancelReason::timeout()));
2227        assert_eq!(mapped.status().as_u16(), 504);
2228    }
2229
2230    #[test]
2231    fn outcome_cancelled_user_maps_to_499() {
2232        let mapped = outcome_to_response::<Response, HttpError>(Outcome::Cancelled(
2233            CancelReason::user("client disconnected"),
2234        ));
2235        assert_eq!(mapped.status().as_u16(), 499);
2236    }
2237
2238    #[test]
2239    fn outcome_panicked_maps_to_500() {
2240        let mapped = outcome_to_response::<Response, HttpError>(Outcome::Panicked(
2241            PanicPayload::new("boom"),
2242        ));
2243        assert_eq!(mapped.status().as_u16(), 500);
2244    }
2245
2246    // =========================================================================
2247    // Redirect tests
2248    // =========================================================================
2249
2250    #[test]
2251    fn redirect_temporary_returns_307() {
2252        let redirect = Redirect::temporary("/new-location");
2253        let response = redirect.into_response();
2254        assert_eq!(response.status().as_u16(), 307);
2255    }
2256
2257    #[test]
2258    fn redirect_permanent_returns_308() {
2259        let redirect = Redirect::permanent("/moved");
2260        let response = redirect.into_response();
2261        assert_eq!(response.status().as_u16(), 308);
2262    }
2263
2264    #[test]
2265    fn redirect_see_other_returns_303() {
2266        let redirect = Redirect::see_other("/result");
2267        let response = redirect.into_response();
2268        assert_eq!(response.status().as_u16(), 303);
2269    }
2270
2271    #[test]
2272    fn redirect_moved_permanently_returns_301() {
2273        let redirect = Redirect::moved_permanently("/gone");
2274        let response = redirect.into_response();
2275        assert_eq!(response.status().as_u16(), 301);
2276    }
2277
2278    #[test]
2279    fn redirect_found_returns_302() {
2280        let redirect = Redirect::found("/elsewhere");
2281        let response = redirect.into_response();
2282        assert_eq!(response.status().as_u16(), 302);
2283    }
2284
2285    #[test]
2286    fn redirect_sets_location_header() {
2287        let redirect = Redirect::temporary("/target?query=1");
2288        let response = redirect.into_response();
2289
2290        let location = response
2291            .headers()
2292            .iter()
2293            .find(|(name, _)| name == "location")
2294            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2295
2296        assert_eq!(location, Some("/target?query=1".to_string()));
2297    }
2298
2299    #[test]
2300    fn redirect_location_accessor() {
2301        let redirect = Redirect::permanent("https://example.com/new");
2302        assert_eq!(redirect.location(), "https://example.com/new");
2303    }
2304
2305    #[test]
2306    fn redirect_status_accessor() {
2307        let redirect = Redirect::see_other("/done");
2308        assert_eq!(redirect.status().as_u16(), 303);
2309    }
2310
2311    // =========================================================================
2312    // Html tests
2313    // =========================================================================
2314
2315    #[test]
2316    fn html_response_has_correct_content_type() {
2317        let html = Html::new("<html><body>Hello</body></html>");
2318        let response = html.into_response();
2319
2320        let content_type = response
2321            .headers()
2322            .iter()
2323            .find(|(name, _)| name == "content-type")
2324            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2325
2326        assert_eq!(content_type, Some("text/html; charset=utf-8".to_string()));
2327    }
2328
2329    #[test]
2330    fn html_response_has_status_200() {
2331        let html = Html::new("<p>test</p>");
2332        let response = html.into_response();
2333        assert_eq!(response.status().as_u16(), 200);
2334    }
2335
2336    #[test]
2337    fn html_content_accessor() {
2338        let html = Html::new("<div>content</div>");
2339        assert_eq!(html.content(), "<div>content</div>");
2340    }
2341
2342    #[test]
2343    fn html_from_string() {
2344        let html: Html = "hello".into();
2345        assert_eq!(html.content(), "hello");
2346    }
2347
2348    // =========================================================================
2349    // Text tests
2350    // =========================================================================
2351
2352    #[test]
2353    fn text_response_has_correct_content_type() {
2354        let text = Text::new("Plain text content");
2355        let response = text.into_response();
2356
2357        let content_type = response
2358            .headers()
2359            .iter()
2360            .find(|(name, _)| name == "content-type")
2361            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2362
2363        assert_eq!(content_type, Some("text/plain; charset=utf-8".to_string()));
2364    }
2365
2366    #[test]
2367    fn text_response_has_status_200() {
2368        let text = Text::new("hello");
2369        let response = text.into_response();
2370        assert_eq!(response.status().as_u16(), 200);
2371    }
2372
2373    #[test]
2374    fn text_content_accessor() {
2375        let text = Text::new("my content");
2376        assert_eq!(text.content(), "my content");
2377    }
2378
2379    // =========================================================================
2380    // NoContent tests
2381    // =========================================================================
2382
2383    #[test]
2384    fn no_content_returns_204() {
2385        let response = NoContent.into_response();
2386        assert_eq!(response.status().as_u16(), 204);
2387    }
2388
2389    #[test]
2390    fn no_content_has_empty_body() {
2391        let response = NoContent.into_response();
2392        assert!(response.body_ref().is_empty());
2393    }
2394
2395    // =========================================================================
2396    // FileResponse tests
2397    // =========================================================================
2398
2399    #[test]
2400    fn file_response_infers_png_content_type() {
2401        let file = FileResponse::new("/path/to/image.png");
2402        // We test the internal method indirectly through the response
2403        assert_eq!(file.path().to_str(), Some("/path/to/image.png"));
2404    }
2405
2406    #[test]
2407    fn file_response_download_as_sets_attachment() {
2408        let file = FileResponse::new("/data/report.csv").download_as("my-report.csv");
2409        let disposition = file.content_disposition();
2410        assert!(disposition.contains("attachment"));
2411        assert!(disposition.contains("my-report.csv"));
2412    }
2413
2414    #[test]
2415    fn file_response_inline_sets_inline() {
2416        let file = FileResponse::new("/image.png").inline();
2417        let disposition = file.content_disposition();
2418        assert_eq!(disposition, "inline");
2419    }
2420
2421    #[test]
2422    fn file_response_custom_content_type() {
2423        // Create a temp file for testing
2424        let temp_dir = std::env::temp_dir();
2425        let test_file = temp_dir.join("test_response_file.txt");
2426        std::fs::write(&test_file, b"test content").unwrap();
2427
2428        let file = FileResponse::new(&test_file).content_type("application/custom");
2429        let response = file.into_response();
2430
2431        let content_type = response
2432            .headers()
2433            .iter()
2434            .find(|(name, _)| name == "content-type")
2435            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2436
2437        assert_eq!(content_type, Some("application/custom".to_string()));
2438
2439        // Cleanup
2440        let _ = std::fs::remove_file(test_file);
2441    }
2442
2443    #[test]
2444    fn file_response_includes_accept_ranges_header() {
2445        // Create a temp file for testing
2446        let temp_dir = std::env::temp_dir();
2447        let test_file = temp_dir.join("test_accept_ranges.txt");
2448        std::fs::write(&test_file, b"test content for range support").unwrap();
2449
2450        let file = FileResponse::new(&test_file);
2451        let response = file.into_response();
2452
2453        let accept_ranges = response
2454            .headers()
2455            .iter()
2456            .find(|(name, _)| name == "accept-ranges")
2457            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2458
2459        assert_eq!(accept_ranges, Some("bytes".to_string()));
2460
2461        // Cleanup
2462        let _ = std::fs::remove_file(test_file);
2463    }
2464
2465    #[test]
2466    fn file_response_not_found_returns_404() {
2467        let file = FileResponse::new("/nonexistent/path/file.txt");
2468        let response = file.into_response();
2469        assert_eq!(response.status().as_u16(), 404);
2470    }
2471
2472    // =========================================================================
2473    // MIME type tests
2474    // =========================================================================
2475
2476    #[test]
2477    fn mime_type_for_common_extensions() {
2478        assert_eq!(mime_type_for_extension("html"), "text/html; charset=utf-8");
2479        assert_eq!(mime_type_for_extension("css"), "text/css; charset=utf-8");
2480        assert_eq!(
2481            mime_type_for_extension("js"),
2482            "text/javascript; charset=utf-8"
2483        );
2484        assert_eq!(mime_type_for_extension("json"), "application/json");
2485        assert_eq!(mime_type_for_extension("png"), "image/png");
2486        assert_eq!(mime_type_for_extension("jpg"), "image/jpeg");
2487        assert_eq!(mime_type_for_extension("pdf"), "application/pdf");
2488        assert_eq!(mime_type_for_extension("zip"), "application/zip");
2489    }
2490
2491    #[test]
2492    fn mime_type_case_insensitive() {
2493        assert_eq!(mime_type_for_extension("HTML"), "text/html; charset=utf-8");
2494        assert_eq!(mime_type_for_extension("PNG"), "image/png");
2495        assert_eq!(mime_type_for_extension("Json"), "application/json");
2496    }
2497
2498    #[test]
2499    fn mime_type_unknown_returns_octet_stream() {
2500        assert_eq!(
2501            mime_type_for_extension("unknown"),
2502            "application/octet-stream"
2503        );
2504        assert_eq!(mime_type_for_extension("xyz"), "application/octet-stream");
2505    }
2506
2507    // =========================================================================
2508    // StatusCode tests
2509    // =========================================================================
2510
2511    #[test]
2512    fn status_code_see_other_is_303() {
2513        assert_eq!(StatusCode::SEE_OTHER.as_u16(), 303);
2514    }
2515
2516    #[test]
2517    fn status_code_see_other_canonical_reason() {
2518        assert_eq!(StatusCode::SEE_OTHER.canonical_reason(), "See Other");
2519    }
2520
2521    #[test]
2522    fn status_code_partial_content_is_206() {
2523        assert_eq!(StatusCode::PARTIAL_CONTENT.as_u16(), 206);
2524    }
2525
2526    #[test]
2527    fn status_code_partial_content_canonical_reason() {
2528        assert_eq!(
2529            StatusCode::PARTIAL_CONTENT.canonical_reason(),
2530            "Partial Content"
2531        );
2532    }
2533
2534    #[test]
2535    fn status_code_range_not_satisfiable_is_416() {
2536        assert_eq!(StatusCode::RANGE_NOT_SATISFIABLE.as_u16(), 416);
2537    }
2538
2539    #[test]
2540    fn status_code_range_not_satisfiable_canonical_reason() {
2541        assert_eq!(
2542            StatusCode::RANGE_NOT_SATISFIABLE.canonical_reason(),
2543            "Range Not Satisfiable"
2544        );
2545    }
2546
2547    #[test]
2548    fn response_partial_content_returns_206() {
2549        let response = Response::partial_content();
2550        assert_eq!(response.status().as_u16(), 206);
2551    }
2552
2553    #[test]
2554    fn response_range_not_satisfiable_returns_416() {
2555        let response = Response::range_not_satisfiable();
2556        assert_eq!(response.status().as_u16(), 416);
2557    }
2558
2559    // =========================================================================
2560    // Cookie setting tests
2561    // =========================================================================
2562
2563    #[test]
2564    fn response_set_cookie_adds_header() {
2565        let response = Response::ok().set_cookie(SetCookie::new("session", "abc123"));
2566
2567        let cookie_header = response
2568            .headers()
2569            .iter()
2570            .find(|(name, _)| name == "set-cookie")
2571            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2572
2573        assert!(cookie_header.is_some());
2574        let header_value = cookie_header.unwrap();
2575        assert!(header_value.contains("session=abc123"));
2576    }
2577
2578    #[test]
2579    fn response_set_cookie_with_attributes() {
2580        let response = Response::ok().set_cookie(
2581            SetCookie::new("session", "token123")
2582                .http_only(true)
2583                .secure(true)
2584                .same_site(SameSite::Strict)
2585                .max_age(3600)
2586                .path("/api"),
2587        );
2588
2589        let cookie_header = response
2590            .headers()
2591            .iter()
2592            .find(|(name, _)| name == "set-cookie")
2593            .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2594            .unwrap();
2595
2596        assert!(cookie_header.contains("session=token123"));
2597        assert!(cookie_header.contains("HttpOnly"));
2598        assert!(cookie_header.contains("Secure"));
2599        assert!(cookie_header.contains("SameSite=Strict"));
2600        assert!(cookie_header.contains("Max-Age=3600"));
2601        assert!(cookie_header.contains("Path=/api"));
2602    }
2603
2604    #[test]
2605    fn response_set_multiple_cookies() {
2606        let response = Response::ok()
2607            .set_cookie(SetCookie::new("session", "abc"))
2608            .set_cookie(SetCookie::new("prefs", "dark"));
2609
2610        let cookie_headers: Vec<_> = response
2611            .headers()
2612            .iter()
2613            .filter(|(name, _)| name == "set-cookie")
2614            .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2615            .collect();
2616
2617        assert_eq!(cookie_headers.len(), 2);
2618        assert!(cookie_headers.iter().any(|h| h.contains("session=abc")));
2619        assert!(cookie_headers.iter().any(|h| h.contains("prefs=dark")));
2620    }
2621
2622    #[test]
2623    fn response_delete_cookie_sets_max_age_zero() {
2624        let response = Response::ok().delete_cookie("session");
2625
2626        let cookie_header = response
2627            .headers()
2628            .iter()
2629            .find(|(name, _)| name == "set-cookie")
2630            .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2631            .unwrap();
2632
2633        assert!(cookie_header.contains("session="));
2634        assert!(cookie_header.contains("Max-Age=0"));
2635    }
2636
2637    #[test]
2638    fn response_set_and_delete_cookies() {
2639        // Set a new cookie and delete an old one in the same response
2640        let response = Response::ok()
2641            .set_cookie(SetCookie::new("new_session", "xyz"))
2642            .delete_cookie("old_session");
2643
2644        let cookie_headers: Vec<_> = response
2645            .headers()
2646            .iter()
2647            .filter(|(name, _)| name == "set-cookie")
2648            .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2649            .collect();
2650
2651        assert_eq!(cookie_headers.len(), 2);
2652        assert!(cookie_headers.iter().any(|h| h.contains("new_session=xyz")));
2653        assert!(
2654            cookie_headers
2655                .iter()
2656                .any(|h| h.contains("old_session=") && h.contains("Max-Age=0"))
2657        );
2658    }
2659
2660    // =========================================================================
2661    // Binary tests
2662    // =========================================================================
2663
2664    #[test]
2665    fn binary_new_creates_from_vec() {
2666        let data = vec![0x01, 0x02, 0x03, 0x04];
2667        let binary = Binary::new(data.clone());
2668        assert_eq!(binary.data(), &data[..]);
2669    }
2670
2671    #[test]
2672    fn binary_new_creates_from_slice() {
2673        let data = [0xDE, 0xAD, 0xBE, 0xEF];
2674        let binary = Binary::new(&data[..]);
2675        assert_eq!(binary.data(), &data);
2676    }
2677
2678    #[test]
2679    fn binary_into_response_has_correct_content_type() {
2680        let binary = Binary::new(vec![1, 2, 3]);
2681        let response = binary.into_response();
2682
2683        let content_type = response
2684            .headers()
2685            .iter()
2686            .find(|(name, _)| name == "content-type")
2687            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2688
2689        assert_eq!(content_type, Some("application/octet-stream".to_string()));
2690    }
2691
2692    #[test]
2693    fn binary_into_response_has_status_200() {
2694        let binary = Binary::new(vec![1, 2, 3]);
2695        let response = binary.into_response();
2696        assert_eq!(response.status().as_u16(), 200);
2697    }
2698
2699    #[test]
2700    fn binary_into_response_has_correct_body() {
2701        let data = vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]; // "Hello" in bytes
2702        let binary = Binary::new(data.clone());
2703        let response = binary.into_response();
2704
2705        if let ResponseBody::Bytes(bytes) = response.body_ref() {
2706            assert_eq!(bytes, &data);
2707        } else {
2708            panic!("Expected Bytes body");
2709        }
2710    }
2711
2712    #[test]
2713    fn binary_with_content_type_returns_binary_with_type() {
2714        let data = vec![0x89, 0x50, 0x4E, 0x47]; // PNG magic bytes
2715        let binary = Binary::new(data);
2716        let binary_typed = binary.with_content_type("image/png");
2717
2718        assert_eq!(binary_typed.content_type(), "image/png");
2719    }
2720
2721    #[test]
2722    fn binary_with_type_into_response_has_correct_content_type() {
2723        let data = vec![0xFF, 0xD8, 0xFF]; // JPEG magic bytes
2724        let binary = Binary::new(data).with_content_type("image/jpeg");
2725        let response = binary.into_response();
2726
2727        let content_type = response
2728            .headers()
2729            .iter()
2730            .find(|(name, _)| name == "content-type")
2731            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2732
2733        assert_eq!(content_type, Some("image/jpeg".to_string()));
2734    }
2735
2736    #[test]
2737    fn binary_with_type_into_response_has_correct_body() {
2738        let data = vec![0x25, 0x50, 0x44, 0x46]; // PDF magic bytes
2739        let binary = Binary::new(data.clone()).with_content_type("application/pdf");
2740        let response = binary.into_response();
2741
2742        if let ResponseBody::Bytes(bytes) = response.body_ref() {
2743            assert_eq!(bytes, &data);
2744        } else {
2745            panic!("Expected Bytes body");
2746        }
2747    }
2748
2749    #[test]
2750    fn binary_with_type_data_accessor() {
2751        let data = vec![1, 2, 3, 4, 5];
2752        let binary = Binary::new(data.clone()).with_content_type("application/custom");
2753        assert_eq!(binary.data(), &data[..]);
2754    }
2755
2756    #[test]
2757    fn binary_with_type_status_200() {
2758        let binary = Binary::new(vec![0]).with_content_type("text/plain");
2759        let response = binary.into_response();
2760        assert_eq!(response.status().as_u16(), 200);
2761    }
2762
2763    // =========================================================================
2764    // ResponseModelConfig tests
2765    // =========================================================================
2766
2767    #[test]
2768    fn response_model_config_default() {
2769        let config = ResponseModelConfig::new();
2770        assert!(config.include.is_none());
2771        assert!(config.exclude.is_none());
2772        assert!(!config.by_alias);
2773        assert!(!config.exclude_unset);
2774        assert!(!config.exclude_defaults);
2775        assert!(!config.exclude_none);
2776    }
2777
2778    #[test]
2779    fn response_model_config_include() {
2780        let fields: std::collections::HashSet<String> =
2781            ["id", "name"].iter().map(|s| (*s).to_string()).collect();
2782        let config = ResponseModelConfig::new().include(fields.clone());
2783        assert_eq!(config.include, Some(fields));
2784    }
2785
2786    #[test]
2787    fn response_model_config_exclude() {
2788        let fields: std::collections::HashSet<String> =
2789            ["password"].iter().map(|s| (*s).to_string()).collect();
2790        let config = ResponseModelConfig::new().exclude(fields.clone());
2791        assert_eq!(config.exclude, Some(fields));
2792    }
2793
2794    #[test]
2795    fn response_model_config_by_alias() {
2796        let config = ResponseModelConfig::new().by_alias(true);
2797        assert!(config.by_alias);
2798    }
2799
2800    #[test]
2801    fn response_model_config_exclude_none() {
2802        let config = ResponseModelConfig::new().exclude_none(true);
2803        assert!(config.exclude_none);
2804    }
2805
2806    #[test]
2807    fn response_model_config_exclude_unset() {
2808        let config = ResponseModelConfig::new().exclude_unset(true);
2809        assert!(config.exclude_unset);
2810    }
2811
2812    #[test]
2813    fn response_model_config_exclude_defaults() {
2814        let config = ResponseModelConfig::new().exclude_defaults(true);
2815        assert!(config.exclude_defaults);
2816    }
2817
2818    #[test]
2819    fn response_model_config_has_filtering() {
2820        let config = ResponseModelConfig::new();
2821        assert!(!config.has_filtering());
2822
2823        let config =
2824            ResponseModelConfig::new().include(["id"].iter().map(|s| (*s).to_string()).collect());
2825        assert!(config.has_filtering());
2826
2827        let config = ResponseModelConfig::new()
2828            .exclude(["password"].iter().map(|s| (*s).to_string()).collect());
2829        assert!(config.has_filtering());
2830
2831        let config = ResponseModelConfig::new().exclude_none(true);
2832        assert!(config.has_filtering());
2833    }
2834
2835    #[test]
2836    fn response_model_config_filter_json_include() {
2837        let config = ResponseModelConfig::new()
2838            .include(["id", "name"].iter().map(|s| (*s).to_string()).collect());
2839
2840        let value = serde_json::json!({
2841            "id": 1,
2842            "name": "Alice",
2843            "email": "alice@example.com",
2844            "password": "secret"
2845        });
2846
2847        let filtered = config.filter_json(value).unwrap();
2848        assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2849        assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2850        assert!(filtered.get("email").is_none());
2851        assert!(filtered.get("password").is_none());
2852    }
2853
2854    #[test]
2855    fn response_model_config_filter_json_exclude() {
2856        let config = ResponseModelConfig::new().exclude(
2857            ["password", "secret"]
2858                .iter()
2859                .map(|s| (*s).to_string())
2860                .collect(),
2861        );
2862
2863        let value = serde_json::json!({
2864            "id": 1,
2865            "name": "Alice",
2866            "password": "secret123",
2867            "secret": "hidden"
2868        });
2869
2870        let filtered = config.filter_json(value).unwrap();
2871        assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2872        assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2873        assert!(filtered.get("password").is_none());
2874        assert!(filtered.get("secret").is_none());
2875    }
2876
2877    #[test]
2878    fn response_model_config_filter_json_exclude_none() {
2879        let config = ResponseModelConfig::new().exclude_none(true);
2880
2881        let value = serde_json::json!({
2882            "id": 1,
2883            "name": "Alice",
2884            "middle_name": null,
2885            "nickname": null
2886        });
2887
2888        let filtered = config.filter_json(value).unwrap();
2889        assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2890        assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2891        assert!(filtered.get("middle_name").is_none());
2892        assert!(filtered.get("nickname").is_none());
2893    }
2894
2895    #[test]
2896    fn response_model_config_filter_json_combined() {
2897        let config = ResponseModelConfig::new()
2898            .include(
2899                ["id", "name", "email", "middle_name"]
2900                    .iter()
2901                    .map(|s| (*s).to_string())
2902                    .collect(),
2903            )
2904            .exclude_none(true);
2905
2906        let value = serde_json::json!({
2907            "id": 1,
2908            "name": "Alice",
2909            "email": "alice@example.com",
2910            "middle_name": null,
2911            "password": "secret"
2912        });
2913
2914        let filtered = config.filter_json(value).unwrap();
2915        assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2916        assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2917        assert_eq!(
2918            filtered.get("email"),
2919            Some(&serde_json::json!("alice@example.com"))
2920        );
2921        assert!(filtered.get("middle_name").is_none()); // null, excluded
2922        assert!(filtered.get("password").is_none()); // not in include
2923    }
2924
2925    #[test]
2926    fn response_model_config_by_alias_requires_alias_metadata() {
2927        let config = ResponseModelConfig::new().by_alias(true);
2928        let value = serde_json::json!({"userId": 1, "name": "Alice"});
2929        assert!(config.filter_json(value).is_err());
2930    }
2931
2932    #[test]
2933    fn response_model_config_by_alias_normalizes_and_realiases() {
2934        static ALIASES: &[(&str, &str)] = &[("user_id", "userId")];
2935
2936        // Input uses alias, output canonical when by_alias is false.
2937        let config = ResponseModelConfig::new().with_aliases(ALIASES);
2938        let value = serde_json::json!({"userId": 1, "name": "Alice"});
2939        let filtered = config.filter_json(value).unwrap();
2940        assert_eq!(filtered.get("user_id"), Some(&serde_json::json!(1)));
2941        assert!(filtered.get("userId").is_none());
2942
2943        // Input uses canonical, output alias when by_alias is true.
2944        let config = ResponseModelConfig::new()
2945            .with_aliases(ALIASES)
2946            .by_alias(true);
2947        let value = serde_json::json!({"user_id": 1, "name": "Alice"});
2948        let filtered = config.filter_json(value).unwrap();
2949        assert_eq!(filtered.get("userId"), Some(&serde_json::json!(1)));
2950        assert!(filtered.get("user_id").is_none());
2951    }
2952
2953    #[test]
2954    fn response_model_config_exclude_defaults_requires_defaults_provider() {
2955        let config = ResponseModelConfig::new().exclude_defaults(true);
2956        let value = serde_json::json!({"active": false});
2957        assert!(config.filter_json(value).is_err());
2958    }
2959
2960    #[test]
2961    fn response_model_config_exclude_defaults_filters_matching_fields() {
2962        #[derive(Default, Serialize)]
2963        struct UserDefaults {
2964            active: bool,
2965            name: String,
2966        }
2967
2968        // Default active=false, name=""; should drop active but keep name when name != default.
2969        let config = ResponseModelConfig::new()
2970            .with_defaults_from::<UserDefaults>()
2971            .exclude_defaults(true);
2972        let value = serde_json::json!({"active": false, "name": "Alice"});
2973        let filtered = config.filter_json(value).unwrap();
2974        assert!(filtered.get("active").is_none());
2975        assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2976    }
2977
2978    #[test]
2979    fn response_model_config_exclude_unset_requires_set_fields() {
2980        let config = ResponseModelConfig::new().exclude_unset(true);
2981        let value = serde_json::json!({"id": 1, "name": "Alice"});
2982        assert!(config.filter_json(value).is_err());
2983    }
2984
2985    #[test]
2986    fn response_model_config_exclude_unset_filters_not_set() {
2987        let set_fields: std::collections::HashSet<String> =
2988            ["id", "name"].iter().map(|s| (*s).to_string()).collect();
2989        let config = ResponseModelConfig::new()
2990            .with_set_fields(set_fields)
2991            .exclude_unset(true);
2992        let value = serde_json::json!({"id": 1, "name": "Alice", "email": "a@b.com"});
2993        let filtered = config.filter_json(value).unwrap();
2994        assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2995        assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2996        assert!(filtered.get("email").is_none());
2997    }
2998
2999    // =========================================================================
3000    // ValidatedResponse tests
3001    // =========================================================================
3002
3003    #[test]
3004    fn validated_response_serializes_struct() {
3005        #[derive(Serialize)]
3006        struct User {
3007            id: i64,
3008            name: String,
3009        }
3010
3011        let user = User {
3012            id: 1,
3013            name: "Alice".to_string(),
3014        };
3015
3016        let response = ValidatedResponse::new(user).into_response();
3017        assert_eq!(response.status().as_u16(), 200);
3018
3019        if let ResponseBody::Bytes(bytes) = response.body_ref() {
3020            let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3021            assert_eq!(parsed["id"], 1);
3022            assert_eq!(parsed["name"], "Alice");
3023        } else {
3024            panic!("Expected Bytes body");
3025        }
3026    }
3027
3028    #[test]
3029    fn validated_response_excludes_fields() {
3030        #[derive(Serialize)]
3031        struct User {
3032            id: i64,
3033            name: String,
3034            password: String,
3035        }
3036
3037        let user = User {
3038            id: 1,
3039            name: "Alice".to_string(),
3040            password: "secret123".to_string(),
3041        };
3042
3043        let response = ValidatedResponse::new(user)
3044            .with_config(
3045                ResponseModelConfig::new()
3046                    .exclude(["password"].iter().map(|s| (*s).to_string()).collect()),
3047            )
3048            .into_response();
3049
3050        assert_eq!(response.status().as_u16(), 200);
3051
3052        if let ResponseBody::Bytes(bytes) = response.body_ref() {
3053            let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3054            assert_eq!(parsed["id"], 1);
3055            assert_eq!(parsed["name"], "Alice");
3056            assert!(parsed.get("password").is_none());
3057        } else {
3058            panic!("Expected Bytes body");
3059        }
3060    }
3061
3062    #[test]
3063    fn validated_response_includes_fields() {
3064        #[derive(Serialize)]
3065        struct User {
3066            id: i64,
3067            name: String,
3068            email: String,
3069            password: String,
3070        }
3071
3072        let user = User {
3073            id: 1,
3074            name: "Alice".to_string(),
3075            email: "alice@example.com".to_string(),
3076            password: "secret123".to_string(),
3077        };
3078
3079        let response = ValidatedResponse::new(user)
3080            .with_config(
3081                ResponseModelConfig::new()
3082                    .include(["id", "name"].iter().map(|s| (*s).to_string()).collect()),
3083            )
3084            .into_response();
3085
3086        assert_eq!(response.status().as_u16(), 200);
3087
3088        if let ResponseBody::Bytes(bytes) = response.body_ref() {
3089            let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3090            assert_eq!(parsed["id"], 1);
3091            assert_eq!(parsed["name"], "Alice");
3092            assert!(parsed.get("email").is_none());
3093            assert!(parsed.get("password").is_none());
3094        } else {
3095            panic!("Expected Bytes body");
3096        }
3097    }
3098
3099    #[test]
3100    fn validated_response_exclude_none_values() {
3101        #[derive(Serialize)]
3102        struct User {
3103            id: i64,
3104            name: String,
3105            nickname: Option<String>,
3106        }
3107
3108        let user = User {
3109            id: 1,
3110            name: "Alice".to_string(),
3111            nickname: None,
3112        };
3113
3114        let response = ValidatedResponse::new(user)
3115            .with_config(ResponseModelConfig::new().exclude_none(true))
3116            .into_response();
3117
3118        assert_eq!(response.status().as_u16(), 200);
3119
3120        if let ResponseBody::Bytes(bytes) = response.body_ref() {
3121            let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3122            assert_eq!(parsed["id"], 1);
3123            assert_eq!(parsed["name"], "Alice");
3124            assert!(parsed.get("nickname").is_none());
3125        } else {
3126            panic!("Expected Bytes body");
3127        }
3128    }
3129
3130    #[test]
3131    fn validated_response_content_type_is_json() {
3132        #[derive(Serialize)]
3133        struct Data {
3134            value: i32,
3135        }
3136
3137        let response = ValidatedResponse::new(Data { value: 42 }).into_response();
3138
3139        let content_type = response
3140            .headers()
3141            .iter()
3142            .find(|(name, _)| name == "content-type")
3143            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
3144
3145        assert_eq!(content_type, Some("application/json".to_string()));
3146    }
3147
3148    // =========================================================================
3149    // Helper function tests
3150    // =========================================================================
3151
3152    #[test]
3153    fn exclude_fields_helper() {
3154        #[derive(Serialize)]
3155        struct User {
3156            id: i64,
3157            name: String,
3158            password: String,
3159        }
3160
3161        let user = User {
3162            id: 1,
3163            name: "Alice".to_string(),
3164            password: "secret".to_string(),
3165        };
3166
3167        let response = exclude_fields(user, &["password"]).into_response();
3168
3169        if let ResponseBody::Bytes(bytes) = response.body_ref() {
3170            let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3171            assert!(parsed.get("id").is_some());
3172            assert!(parsed.get("name").is_some());
3173            assert!(parsed.get("password").is_none());
3174        } else {
3175            panic!("Expected Bytes body");
3176        }
3177    }
3178
3179    #[test]
3180    fn include_fields_helper() {
3181        #[derive(Serialize)]
3182        struct User {
3183            id: i64,
3184            name: String,
3185            email: String,
3186            password: String,
3187        }
3188
3189        let user = User {
3190            id: 1,
3191            name: "Alice".to_string(),
3192            email: "alice@example.com".to_string(),
3193            password: "secret".to_string(),
3194        };
3195
3196        let response = include_fields(user, &["id", "name"]).into_response();
3197
3198        if let ResponseBody::Bytes(bytes) = response.body_ref() {
3199            let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3200            assert!(parsed.get("id").is_some());
3201            assert!(parsed.get("name").is_some());
3202            assert!(parsed.get("email").is_none());
3203            assert!(parsed.get("password").is_none());
3204        } else {
3205            panic!("Expected Bytes body");
3206        }
3207    }
3208
3209    // ====================================================================
3210    // Conditional request (ETag) tests
3211    // ====================================================================
3212
3213    #[test]
3214    fn status_code_precondition_failed() {
3215        assert_eq!(StatusCode::PRECONDITION_FAILED.as_u16(), 412);
3216        assert_eq!(
3217            StatusCode::PRECONDITION_FAILED.canonical_reason(),
3218            "Precondition Failed"
3219        );
3220    }
3221
3222    #[test]
3223    fn response_not_modified_status() {
3224        let resp = Response::not_modified();
3225        assert_eq!(resp.status().as_u16(), 304);
3226    }
3227
3228    #[test]
3229    fn response_precondition_failed_status() {
3230        let resp = Response::precondition_failed();
3231        assert_eq!(resp.status().as_u16(), 412);
3232    }
3233
3234    #[test]
3235    fn response_with_etag() {
3236        let resp = Response::ok().with_etag("\"abc123\"");
3237        let etag = resp
3238            .headers()
3239            .iter()
3240            .find(|(n, _)| n == "ETag")
3241            .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3242        assert_eq!(etag, Some("\"abc123\"".to_string()));
3243    }
3244
3245    #[test]
3246    fn response_with_weak_etag() {
3247        let resp = Response::ok().with_weak_etag("\"abc123\"");
3248        let etag = resp
3249            .headers()
3250            .iter()
3251            .find(|(n, _)| n == "ETag")
3252            .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3253        assert_eq!(etag, Some("W/\"abc123\"".to_string()));
3254    }
3255
3256    #[test]
3257    fn response_with_weak_etag_already_prefixed() {
3258        let resp = Response::ok().with_weak_etag("W/\"abc123\"");
3259        let etag = resp
3260            .headers()
3261            .iter()
3262            .find(|(n, _)| n == "ETag")
3263            .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3264        assert_eq!(etag, Some("W/\"abc123\"".to_string()));
3265    }
3266
3267    #[test]
3268    fn check_if_none_match_exact() {
3269        // Exact match -> false (send 304)
3270        assert!(!check_if_none_match("\"abc\"", "\"abc\""));
3271    }
3272
3273    #[test]
3274    fn check_if_none_match_no_match() {
3275        // No match -> true (send full response)
3276        assert!(check_if_none_match("\"abc\"", "\"def\""));
3277    }
3278
3279    #[test]
3280    fn check_if_none_match_wildcard() {
3281        assert!(!check_if_none_match("*", "\"anything\""));
3282    }
3283
3284    #[test]
3285    fn check_if_none_match_multiple_etags() {
3286        // Second ETag matches
3287        assert!(!check_if_none_match("\"aaa\", \"bbb\", \"ccc\"", "\"bbb\""));
3288        // None match
3289        assert!(check_if_none_match("\"aaa\", \"bbb\"", "\"ccc\""));
3290    }
3291
3292    #[test]
3293    fn check_if_none_match_weak_comparison() {
3294        // Weak ETags should match in If-None-Match (weak comparison)
3295        assert!(!check_if_none_match("W/\"abc\"", "\"abc\""));
3296        assert!(!check_if_none_match("\"abc\"", "W/\"abc\""));
3297        assert!(!check_if_none_match("W/\"abc\"", "W/\"abc\""));
3298    }
3299
3300    #[test]
3301    fn check_if_match_exact() {
3302        // Exact match -> true (proceed)
3303        assert!(check_if_match("\"abc\"", "\"abc\""));
3304    }
3305
3306    #[test]
3307    fn check_if_match_no_match() {
3308        // No match -> false (412)
3309        assert!(!check_if_match("\"abc\"", "\"def\""));
3310    }
3311
3312    #[test]
3313    fn check_if_match_wildcard() {
3314        assert!(check_if_match("*", "\"anything\""));
3315    }
3316
3317    #[test]
3318    fn check_if_match_weak_etag_fails() {
3319        // If-Match requires strong comparison - weak ETags never match
3320        assert!(!check_if_match("W/\"abc\"", "\"abc\""));
3321        assert!(!check_if_match("\"abc\"", "W/\"abc\""));
3322    }
3323
3324    #[test]
3325    fn check_if_match_multiple_etags() {
3326        assert!(check_if_match("\"aaa\", \"bbb\"", "\"bbb\""));
3327        assert!(!check_if_match("\"aaa\", \"bbb\"", "\"ccc\""));
3328    }
3329
3330    #[test]
3331    fn apply_conditional_get_304() {
3332        use crate::request::Method;
3333
3334        let headers = vec![("If-None-Match".to_string(), b"\"abc123\"".to_vec())];
3335        let response = Response::ok().with_etag("\"abc123\"");
3336        let result = apply_conditional(&headers, Method::Get, response);
3337        assert_eq!(result.status().as_u16(), 304);
3338    }
3339
3340    #[test]
3341    fn apply_conditional_get_no_match_200() {
3342        use crate::request::Method;
3343
3344        let headers = vec![("If-None-Match".to_string(), b"\"old\"".to_vec())];
3345        let response = Response::ok().with_etag("\"new\"");
3346        let result = apply_conditional(&headers, Method::Get, response);
3347        assert_eq!(result.status().as_u16(), 200);
3348    }
3349
3350    #[test]
3351    fn apply_conditional_put_412() {
3352        use crate::request::Method;
3353
3354        let headers = vec![("If-Match".to_string(), b"\"old\"".to_vec())];
3355        let response = Response::ok().with_etag("\"new\"");
3356        let result = apply_conditional(&headers, Method::Put, response);
3357        assert_eq!(result.status().as_u16(), 412);
3358    }
3359
3360    #[test]
3361    fn apply_conditional_put_match_200() {
3362        use crate::request::Method;
3363
3364        let headers = vec![("If-Match".to_string(), b"\"current\"".to_vec())];
3365        let response = Response::ok().with_etag("\"current\"");
3366        let result = apply_conditional(&headers, Method::Put, response);
3367        assert_eq!(result.status().as_u16(), 200);
3368    }
3369
3370    #[test]
3371    fn apply_conditional_no_etag_passthrough() {
3372        use crate::request::Method;
3373
3374        let headers = vec![("If-None-Match".to_string(), b"\"abc\"".to_vec())];
3375        let response = Response::ok(); // No ETag
3376        let result = apply_conditional(&headers, Method::Get, response);
3377        assert_eq!(result.status().as_u16(), 200);
3378    }
3379
3380    // ====================================================================
3381    // Link Header Tests
3382    // ====================================================================
3383
3384    #[test]
3385    fn link_header_single() {
3386        let h = LinkHeader::new().link("https://example.com/next", LinkRel::Next);
3387        assert_eq!(h.to_string(), r#"<https://example.com/next>; rel="next""#);
3388    }
3389
3390    #[test]
3391    fn link_header_multiple() {
3392        let h = LinkHeader::new()
3393            .link("/page/2", LinkRel::Next)
3394            .link("/page/0", LinkRel::Prev);
3395        let s = h.to_string();
3396        assert!(s.contains(r#"</page/2>; rel="next""#));
3397        assert!(s.contains(r#"</page/0>; rel="prev""#));
3398        assert!(s.contains(", "));
3399    }
3400
3401    #[test]
3402    fn link_with_title_and_type() {
3403        let link = Link::new("https://api.example.com", LinkRel::Related)
3404            .title("API Docs")
3405            .media_type("text/html");
3406        let s = link.to_string();
3407        assert!(s.contains(r#"title="API Docs""#));
3408        assert!(s.contains(r#"type="text/html""#));
3409    }
3410
3411    #[test]
3412    fn link_header_custom_rel() {
3413        let h = LinkHeader::new().link("/schema", LinkRel::Custom("describedby".to_string()));
3414        assert!(h.to_string().contains(r#"rel="describedby""#));
3415    }
3416
3417    #[test]
3418    fn link_header_paginate_first_page() {
3419        let h = LinkHeader::new().paginate("/users", 1, 10, 50);
3420        let s = h.to_string();
3421        assert!(s.contains(r#"rel="self""#));
3422        assert!(s.contains(r#"rel="first""#));
3423        assert!(s.contains(r#"rel="last""#));
3424        assert!(s.contains(r#"rel="next""#));
3425        assert!(!s.contains(r#"rel="prev""#)); // No prev on first page
3426        assert!(s.contains("page=5")); // last page = 50/10 = 5
3427    }
3428
3429    #[test]
3430    fn link_header_paginate_middle_page() {
3431        let h = LinkHeader::new().paginate("/users", 3, 10, 50);
3432        let s = h.to_string();
3433        assert!(s.contains(r#"rel="prev""#));
3434        assert!(s.contains(r#"rel="next""#));
3435        assert!(s.contains("page=2")); // prev
3436        assert!(s.contains("page=4")); // next
3437    }
3438
3439    #[test]
3440    fn link_header_paginate_last_page() {
3441        let h = LinkHeader::new().paginate("/users", 5, 10, 50);
3442        let s = h.to_string();
3443        assert!(s.contains(r#"rel="prev""#));
3444        assert!(!s.contains(r#"rel="next""#)); // No next on last page
3445    }
3446
3447    #[test]
3448    fn link_header_paginate_with_existing_query() {
3449        let h = LinkHeader::new().paginate("/users?sort=name", 1, 10, 20);
3450        let s = h.to_string();
3451        assert!(s.contains("sort=name&page="));
3452    }
3453
3454    #[test]
3455    fn link_header_empty() {
3456        let h = LinkHeader::new();
3457        assert!(h.is_empty());
3458        assert_eq!(h.len(), 0);
3459        assert_eq!(h.to_string(), "");
3460    }
3461
3462    #[test]
3463    fn link_header_apply_to_response() {
3464        let h = LinkHeader::new().link("/next", LinkRel::Next);
3465        let response = h.apply(Response::ok());
3466        let link_hdr = response
3467            .headers()
3468            .iter()
3469            .find(|(n, _)| n == "link")
3470            .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string());
3471        assert!(link_hdr.unwrap().contains("rel=\"next\""));
3472    }
3473
3474    #[test]
3475    fn link_header_apply_empty_noop() {
3476        let h = LinkHeader::new();
3477        let response = h.apply(Response::ok());
3478        let has_link = response.headers().iter().any(|(n, _)| n == "link");
3479        assert!(!has_link);
3480    }
3481
3482    #[test]
3483    fn link_rel_display() {
3484        assert_eq!(LinkRel::Self_.to_string(), "self");
3485        assert_eq!(LinkRel::Next.to_string(), "next");
3486        assert_eq!(LinkRel::Prev.to_string(), "prev");
3487        assert_eq!(LinkRel::First.to_string(), "first");
3488        assert_eq!(LinkRel::Last.to_string(), "last");
3489        assert_eq!(LinkRel::Related.to_string(), "related");
3490        assert_eq!(LinkRel::Alternate.to_string(), "alternate");
3491    }
3492}