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