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