1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct StatusCode(u16);
17
18impl StatusCode {
19 pub const CONTINUE: Self = Self(100);
22 pub const SWITCHING_PROTOCOLS: Self = Self(101);
24
25 pub const OK: Self = Self(200);
28 pub const CREATED: Self = Self(201);
30 pub const ACCEPTED: Self = Self(202);
32 pub const NO_CONTENT: Self = Self(204);
34 pub const PARTIAL_CONTENT: Self = Self(206);
36
37 pub const MOVED_PERMANENTLY: Self = Self(301);
40 pub const FOUND: Self = Self(302);
42 pub const SEE_OTHER: Self = Self(303);
44 pub const NOT_MODIFIED: Self = Self(304);
46 pub const TEMPORARY_REDIRECT: Self = Self(307);
48 pub const PERMANENT_REDIRECT: Self = Self(308);
50
51 pub const BAD_REQUEST: Self = Self(400);
54 pub const UNAUTHORIZED: Self = Self(401);
56 pub const FORBIDDEN: Self = Self(403);
58 pub const NOT_FOUND: Self = Self(404);
60 pub const METHOD_NOT_ALLOWED: Self = Self(405);
62 pub const NOT_ACCEPTABLE: Self = Self(406);
64 pub const PRECONDITION_FAILED: Self = Self(412);
66 pub const PAYLOAD_TOO_LARGE: Self = Self(413);
68 pub const UNSUPPORTED_MEDIA_TYPE: Self = Self(415);
70 pub const RANGE_NOT_SATISFIABLE: Self = Self(416);
72 pub const UNPROCESSABLE_ENTITY: Self = Self(422);
74 pub const TOO_MANY_REQUESTS: Self = Self(429);
76 pub const CLIENT_CLOSED_REQUEST: Self = Self(499);
78
79 pub const INTERNAL_SERVER_ERROR: Self = Self(500);
82 pub const SERVICE_UNAVAILABLE: Self = Self(503);
84 pub const GATEWAY_TIMEOUT: Self = Self(504);
86
87 #[must_use]
89 pub const fn from_u16(code: u16) -> Self {
90 Self(code)
91 }
92
93 #[must_use]
95 pub const fn as_u16(self) -> u16 {
96 self.0
97 }
98
99 #[must_use]
101 pub const fn allows_body(self) -> bool {
102 !matches!(self.0, 100..=103 | 204 | 304)
103 }
104
105 #[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
143pub type BodyStream = Pin<Box<dyn Stream<Item = Vec<u8>> + Send>>;
145
146pub enum ResponseBody {
148 Empty,
150 Bytes(Vec<u8>),
152 Stream(BodyStream),
154}
155
156impl ResponseBody {
157 #[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 #[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 #[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
193fn 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
211fn 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#[derive(Debug)]
224pub struct Response {
225 status: StatusCode,
226 headers: Vec<(String, Vec<u8>)>,
227 body: ResponseBody,
228}
229
230impl Response {
231 #[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 #[must_use]
243 pub fn ok() -> Self {
244 Self::with_status(StatusCode::OK)
245 }
246
247 #[must_use]
249 pub fn created() -> Self {
250 Self::with_status(StatusCode::CREATED)
251 }
252
253 #[must_use]
255 pub fn no_content() -> Self {
256 Self::with_status(StatusCode::NO_CONTENT)
257 }
258
259 #[must_use]
261 pub fn internal_error() -> Self {
262 Self::with_status(StatusCode::INTERNAL_SERVER_ERROR)
263 }
264
265 #[must_use]
280 pub fn partial_content() -> Self {
281 Self::with_status(StatusCode::PARTIAL_CONTENT)
282 }
283
284 #[must_use]
298 pub fn range_not_satisfiable() -> Self {
299 Self::with_status(StatusCode::RANGE_NOT_SATISFIABLE)
300 }
301
302 #[must_use]
307 pub fn not_modified() -> Self {
308 Self::with_status(StatusCode::NOT_MODIFIED)
309 }
310
311 #[must_use]
315 pub fn precondition_failed() -> Self {
316 Self::with_status(StatusCode::PRECONDITION_FAILED)
317 }
318
319 #[must_use]
329 pub fn with_etag(self, etag: impl Into<String>) -> Self {
330 self.header("ETag", etag.into().into_bytes())
331 }
332
333 #[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 #[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 if !is_valid_header_name(&name) {
361 return self;
363 }
364
365 let sanitized_value = sanitize_header_value(value);
367
368 self.headers.push((name, sanitized_value));
369 self
370 }
371
372 #[must_use]
374 pub fn body(mut self, body: ResponseBody) -> Self {
375 self.body = body;
376 self
377 }
378
379 #[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 #[must_use]
412 pub fn delete_cookie(self, name: &str) -> Self {
413 let cookie = Cookie::new(name, "").max_age(0);
415 self.set_cookie(cookie)
416 }
417
418 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 #[must_use]
432 pub fn status(&self) -> StatusCode {
433 self.status
434 }
435
436 #[must_use]
438 pub fn headers(&self) -> &[(String, Vec<u8>)] {
439 &self.headers
440 }
441
442 #[must_use]
444 pub fn body_ref(&self) -> &ResponseBody {
445 &self.body
446 }
447
448 #[must_use]
450 pub fn into_parts(self) -> (StatusCode, Vec<(String, Vec<u8>)>, ResponseBody) {
451 (self.status, self.headers, self.body)
452 }
453
454 #[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
477pub trait IntoResponse {
479 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
526pub trait ResponseProduces<T> {}
563
564impl<T> ResponseProduces<T> for T {}
566
567impl<T: serde::Serialize + 'static> ResponseProduces<T> for crate::extract::Json<T> {}
569
570#[derive(Debug, Clone)]
593pub struct Redirect {
594 status: StatusCode,
595 location: String,
596}
597
598impl Redirect {
599 #[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 #[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 #[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 #[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 #[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 #[must_use]
658 pub fn location(&self) -> &str {
659 &self.location
660 }
661
662 #[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#[derive(Debug, Clone)]
685pub struct Html(String);
686
687impl Html {
688 #[must_use]
695 pub fn new(content: impl Into<String>) -> Self {
696 Self(content.into())
697 }
698
699 #[must_use]
704 pub fn escaped(content: impl AsRef<str>) -> Self {
705 Self(escape_html(content.as_ref()))
706 }
707
708 #[must_use]
710 pub fn content(&self) -> &str {
711 &self.0
712 }
713}
714
715fn 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("&"),
721 '<' => out.push_str("<"),
722 '>' => out.push_str(">"),
723 '"' => out.push_str("""),
724 '\'' => out.push_str("'"),
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#[derive(Debug, Clone)]
758pub struct Text(String);
759
760impl Text {
761 #[must_use]
763 pub fn new(content: impl Into<String>) -> Self {
764 Self(content.into())
765 }
766
767 #[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#[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#[derive(Debug, Clone)]
823pub struct Binary(Vec<u8>);
824
825impl Binary {
826 #[must_use]
828 pub fn new(data: impl Into<Vec<u8>>) -> Self {
829 Self(data.into())
830 }
831
832 #[must_use]
834 pub fn data(&self) -> &[u8] {
835 &self.0
836 }
837
838 #[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#[derive(Debug, Clone)]
879pub struct BinaryWithType {
880 data: Vec<u8>,
881 content_type: String,
882}
883
884impl BinaryWithType {
885 pub fn data(&self) -> &[u8] {
887 &self.data
888 }
889
890 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#[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 #[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 #[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 #[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 #[must_use]
967 pub fn inline(mut self) -> Self {
968 self.inline = true;
969 self.download_name = None;
970 self
971 }
972
973 #[must_use]
975 pub fn path(&self) -> &std::path::Path {
976 &self.path
977 }
978
979 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 fn content_disposition(&self) -> String {
990 if self.inline {
991 "inline".to_string()
992 } else if let Some(ref name) = self.download_name {
993 format!("attachment; filename=\"{}\"", name.replace('"', "\\\""))
995 } else {
996 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 #[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#[must_use]
1045pub fn mime_type_for_extension(ext: &str) -> &'static str {
1046 match ext.to_ascii_lowercase().as_str() {
1047 "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 "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 "woff" => "font/woff",
1069 "woff2" => "font/woff2",
1070 "ttf" => "font/ttf",
1071 "otf" => "font/otf",
1072 "eot" => "application/vnd.ms-fontobject",
1073
1074 "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 "mp4" => "video/mp4",
1084 "webm" => "video/webm",
1085 "avi" => "video/x-msvideo",
1086 "mov" => "video/quicktime",
1087 "mkv" => "video/x-matroska",
1088
1089 "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 "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 "wasm" => "application/wasm",
1107
1108 _ => "application/octet-stream",
1109 }
1110}
1111
1112#[must_use]
1118#[allow(dead_code)] pub 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)] fn 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#[derive(Debug, Clone, Default)]
1170#[allow(clippy::struct_excessive_bools)] pub struct ResponseModelConfig {
1172 pub include: Option<std::collections::HashSet<String>>,
1175
1176 pub exclude: Option<std::collections::HashSet<String>>,
1178
1179 pub by_alias: bool,
1181
1182 pub exclude_unset: bool,
1185
1186 pub exclude_defaults: bool,
1188
1189 pub exclude_none: bool,
1191}
1192
1193impl ResponseModelConfig {
1194 #[must_use]
1196 pub fn new() -> Self {
1197 Self::default()
1198 }
1199
1200 #[must_use]
1202 pub fn include(mut self, fields: std::collections::HashSet<String>) -> Self {
1203 self.include = Some(fields);
1204 self
1205 }
1206
1207 #[must_use]
1209 pub fn exclude(mut self, fields: std::collections::HashSet<String>) -> Self {
1210 self.exclude = Some(fields);
1211 self
1212 }
1213
1214 #[must_use]
1219 pub fn by_alias(mut self, value: bool) -> Self {
1220 self.by_alias = value;
1221 self
1222 }
1223
1224 #[must_use]
1229 pub fn exclude_unset(mut self, value: bool) -> Self {
1230 self.exclude_unset = value;
1231 self
1232 }
1233
1234 #[must_use]
1239 pub fn exclude_defaults(mut self, value: bool) -> Self {
1240 self.exclude_defaults = value;
1241 self
1242 }
1243
1244 #[must_use]
1246 pub fn exclude_none(mut self, value: bool) -> Self {
1247 self.exclude_none = value;
1248 self
1249 }
1250
1251 #[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 #[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 if let Some(ref include_set) = self.include {
1272 map.retain(|key, _| include_set.contains(key));
1273 }
1274
1275 if let Some(ref exclude_set) = self.exclude {
1277 map.retain(|key, _| !exclude_set.contains(key));
1278 }
1279
1280 if self.exclude_none {
1282 map.retain(|_, v| !v.is_null());
1283 }
1284 }
1285
1286 value
1287 }
1288}
1289
1290pub trait ResponseModel: Serialize {
1296 #[allow(clippy::result_large_err)] fn validate(&self) -> Result<(), crate::error::ResponseValidationError> {
1301 Ok(())
1303 }
1304
1305 fn model_name() -> &'static str {
1307 std::any::type_name::<Self>()
1308 }
1309}
1310
1311impl<T: Serialize> ResponseModel for T {}
1313
1314#[derive(Debug)]
1346pub struct ValidatedResponse<T> {
1347 pub value: T,
1349 pub config: ResponseModelConfig,
1351}
1352
1353impl<T> ValidatedResponse<T> {
1354 #[must_use]
1356 pub fn new(value: T) -> Self {
1357 Self {
1358 value,
1359 config: ResponseModelConfig::default(),
1360 }
1361 }
1362
1363 #[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 if let Err(error) = self.value.validate() {
1375 return error.into_response();
1376 }
1377
1378 let json_value = match serde_json::to_value(&self.value) {
1380 Ok(v) => v,
1381 Err(e) => {
1382 let error =
1384 crate::error::ResponseValidationError::serialization_failed(e.to_string());
1385 return error.into_response();
1386 }
1387 };
1388
1389 let filtered = self.config.filter_json(json_value);
1391
1392 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#[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#[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
1434pub fn check_if_none_match(if_none_match: &str, current_etag: &str) -> bool {
1460 let if_none_match = if_none_match.trim();
1461
1462 if if_none_match == "*" {
1464 return false; }
1466
1467 let current_stripped = strip_weak_prefix(current_etag.trim());
1468
1469 for candidate in if_none_match.split(',') {
1471 let candidate = strip_weak_prefix(candidate.trim());
1472 if candidate == current_stripped {
1473 return false; }
1475 }
1476
1477 true }
1479
1480pub fn check_if_match(if_match: &str, current_etag: &str) -> bool {
1488 let if_match = if_match.trim();
1489
1490 if if_match == "*" {
1492 return true;
1493 }
1494
1495 let current = current_etag.trim();
1496
1497 if current.starts_with("W/") {
1499 return false;
1500 }
1501
1502 for candidate in if_match.split(',') {
1503 let candidate = candidate.trim();
1504 if candidate.starts_with("W/") {
1506 continue;
1507 }
1508 if candidate == current {
1509 return true;
1510 }
1511 }
1512
1513 false
1514}
1515
1516fn strip_weak_prefix(etag: &str) -> &str {
1518 etag.strip_prefix("W/").unwrap_or(etag)
1519}
1520
1521pub fn apply_conditional(
1537 request_headers: &[(String, Vec<u8>)],
1538 method: &crate::request::Method,
1539 response: Response,
1540) -> Response {
1541 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; };
1552
1553 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 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
1582fn 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#[derive(Debug, Clone, PartialEq, Eq)]
1597pub enum LinkRel {
1598 Self_,
1600 Next,
1602 Prev,
1604 First,
1606 Last,
1608 Related,
1610 Alternate,
1612 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#[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 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 #[must_use]
1653 pub fn title(mut self, title: impl Into<String>) -> Self {
1654 self.title = Some(title.into());
1655 self
1656 }
1657
1658 #[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#[derive(Debug, Clone, Default)]
1698pub struct LinkHeader {
1699 links: Vec<Link>,
1700}
1701
1702impl LinkHeader {
1703 #[must_use]
1705 pub fn new() -> Self {
1706 Self::default()
1707 }
1708
1709 #[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 #[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 #[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 #[must_use]
1766 pub fn is_empty(&self) -> bool {
1767 self.links.is_empty()
1768 }
1769
1770 #[must_use]
1772 pub fn len(&self) -> usize {
1773 self.links.len()
1774 }
1775
1776 #[must_use]
1778 pub fn to_header_value(&self) -> String {
1779 self.to_string()
1780 }
1781
1782 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 #[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 #[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 #[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 #[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 #[test]
1999 fn file_response_infers_png_content_type() {
2000 let file = FileResponse::new("/path/to/image.png");
2001 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 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 let _ = std::fs::remove_file(test_file);
2040 }
2041
2042 #[test]
2043 fn file_response_includes_accept_ranges_header() {
2044 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 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 #[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 #[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 #[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 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 #[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]; 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]; 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]; 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]; 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 #[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()); assert!(filtered.get("password").is_none()); }
2531
2532 #[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 #[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 #[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 assert!(!check_if_none_match("\"abc\"", "\"abc\""));
2804 }
2805
2806 #[test]
2807 fn check_if_none_match_no_match() {
2808 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 assert!(!check_if_none_match("\"aaa\", \"bbb\", \"ccc\"", "\"bbb\""));
2821 assert!(check_if_none_match("\"aaa\", \"bbb\"", "\"ccc\""));
2823 }
2824
2825 #[test]
2826 fn check_if_none_match_weak_comparison() {
2827 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 assert!(check_if_match("\"abc\"", "\"abc\""));
2837 }
2838
2839 #[test]
2840 fn check_if_match_no_match() {
2841 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 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(); let result = apply_conditional(&headers, &Method::Get, response);
2910 assert_eq!(result.status().as_u16(), 200);
2911 }
2912
2913 #[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""#)); assert!(s.contains("page=5")); }
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")); assert!(s.contains("page=4")); }
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""#)); }
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}