1use serde::Serialize;
4use std::fmt;
5use std::pin::Pin;
6
7use asupersync::stream::Stream;
8#[cfg(test)]
9use asupersync::types::PanicPayload;
10use asupersync::types::{CancelKind, CancelReason, Outcome};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct StatusCode(u16);
15
16impl StatusCode {
17 pub const CONTINUE: Self = Self(100);
20 pub const SWITCHING_PROTOCOLS: Self = Self(101);
22
23 pub const OK: Self = Self(200);
26 pub const CREATED: Self = Self(201);
28 pub const ACCEPTED: Self = Self(202);
30 pub const NO_CONTENT: Self = Self(204);
32 pub const PARTIAL_CONTENT: Self = Self(206);
34
35 pub const MOVED_PERMANENTLY: Self = Self(301);
38 pub const FOUND: Self = Self(302);
40 pub const SEE_OTHER: Self = Self(303);
42 pub const NOT_MODIFIED: Self = Self(304);
44 pub const TEMPORARY_REDIRECT: Self = Self(307);
46 pub const PERMANENT_REDIRECT: Self = Self(308);
48
49 pub const BAD_REQUEST: Self = Self(400);
52 pub const UNAUTHORIZED: Self = Self(401);
54 pub const FORBIDDEN: Self = Self(403);
56 pub const NOT_FOUND: Self = Self(404);
58 pub const METHOD_NOT_ALLOWED: Self = Self(405);
60 pub const NOT_ACCEPTABLE: Self = Self(406);
62 pub const PRECONDITION_FAILED: Self = Self(412);
64 pub const PAYLOAD_TOO_LARGE: Self = Self(413);
66 pub const UNSUPPORTED_MEDIA_TYPE: Self = Self(415);
68 pub const RANGE_NOT_SATISFIABLE: Self = Self(416);
70 pub const UNPROCESSABLE_ENTITY: Self = Self(422);
72 pub const TOO_MANY_REQUESTS: Self = Self(429);
74 pub const CLIENT_CLOSED_REQUEST: Self = Self(499);
76
77 pub const INTERNAL_SERVER_ERROR: Self = Self(500);
80 pub const SERVICE_UNAVAILABLE: Self = Self(503);
82 pub const GATEWAY_TIMEOUT: Self = Self(504);
84
85 #[must_use]
87 pub const fn from_u16(code: u16) -> Self {
88 Self(code)
89 }
90
91 #[must_use]
93 pub const fn as_u16(self) -> u16 {
94 self.0
95 }
96
97 #[must_use]
99 pub const fn allows_body(self) -> bool {
100 !matches!(self.0, 100..=103 | 204 | 304)
101 }
102
103 #[must_use]
105 pub const fn canonical_reason(self) -> &'static str {
106 match self.0 {
107 100 => "Continue",
108 101 => "Switching Protocols",
109 200 => "OK",
110 201 => "Created",
111 202 => "Accepted",
112 204 => "No Content",
113 206 => "Partial Content",
114 301 => "Moved Permanently",
115 302 => "Found",
116 303 => "See Other",
117 304 => "Not Modified",
118 307 => "Temporary Redirect",
119 308 => "Permanent Redirect",
120 400 => "Bad Request",
121 401 => "Unauthorized",
122 403 => "Forbidden",
123 404 => "Not Found",
124 405 => "Method Not Allowed",
125 406 => "Not Acceptable",
126 412 => "Precondition Failed",
127 413 => "Payload Too Large",
128 415 => "Unsupported Media Type",
129 416 => "Range Not Satisfiable",
130 422 => "Unprocessable Entity",
131 429 => "Too Many Requests",
132 499 => "Client Closed Request",
133 500 => "Internal Server Error",
134 503 => "Service Unavailable",
135 504 => "Gateway Timeout",
136 _ => "Unknown",
137 }
138 }
139}
140
141pub type BodyStream = Pin<Box<dyn Stream<Item = Vec<u8>> + Send>>;
143
144pub enum ResponseBody {
146 Empty,
148 Bytes(Vec<u8>),
150 Stream(BodyStream),
152}
153
154impl ResponseBody {
155 #[must_use]
157 pub fn stream<S>(stream: S) -> Self
158 where
159 S: Stream<Item = Vec<u8>> + Send + 'static,
160 {
161 Self::Stream(Box::pin(stream))
162 }
163
164 #[must_use]
166 pub fn is_empty(&self) -> bool {
167 matches!(self, Self::Empty) || matches!(self, Self::Bytes(b) if b.is_empty())
168 }
169
170 #[must_use]
172 pub fn len(&self) -> usize {
173 match self {
174 Self::Empty => 0,
175 Self::Bytes(b) => b.len(),
176 Self::Stream(_) => 0,
177 }
178 }
179}
180
181impl fmt::Debug for ResponseBody {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 match self {
184 Self::Empty => f.debug_tuple("Empty").finish(),
185 Self::Bytes(bytes) => f.debug_tuple("Bytes").field(bytes).finish(),
186 Self::Stream(_) => f.debug_tuple("Stream").finish(),
187 }
188 }
189}
190
191fn is_valid_header_name(name: &str) -> bool {
200 !name.is_empty()
201 && name.bytes().all(|b| {
202 matches!(b,
203 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
204 b'0'..=b'9' | b'A'..=b'Z' | b'^' | b'_' | b'`' | b'a'..=b'z' | b'|' | b'~'
205 )
206 })
207}
208
209fn sanitize_header_value(value: Vec<u8>) -> Vec<u8> {
214 value
215 .into_iter()
216 .filter(|&b| b != b'\r' && b != b'\n' && b != 0)
217 .collect()
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum SameSite {
227 Strict,
229 Lax,
231 None,
233}
234
235impl SameSite {
236 #[must_use]
237 pub const fn as_str(self) -> &'static str {
238 match self {
239 Self::Strict => "Strict",
240 Self::Lax => "Lax",
241 Self::None => "None",
242 }
243 }
244}
245
246#[derive(Debug, Clone)]
248pub struct SetCookie {
249 name: String,
250 value: String,
251 path: Option<String>,
252 domain: Option<String>,
253 max_age: Option<i64>,
254 http_only: bool,
255 secure: bool,
256 same_site: Option<SameSite>,
257}
258
259impl SetCookie {
260 #[must_use]
262 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
263 Self {
264 name: name.into(),
265 value: value.into(),
266 path: Some("/".to_string()),
267 domain: None,
268 max_age: None,
269 http_only: false,
270 secure: false,
271 same_site: None,
272 }
273 }
274
275 #[must_use]
277 pub fn path(mut self, path: impl Into<String>) -> Self {
278 self.path = Some(path.into());
279 self
280 }
281
282 #[must_use]
284 pub fn domain(mut self, domain: impl Into<String>) -> Self {
285 self.domain = Some(domain.into());
286 self
287 }
288
289 #[must_use]
291 pub fn max_age(mut self, seconds: i64) -> Self {
292 self.max_age = Some(seconds);
293 self
294 }
295
296 #[must_use]
298 pub fn http_only(mut self, on: bool) -> Self {
299 self.http_only = on;
300 self
301 }
302
303 #[must_use]
305 pub fn secure(mut self, on: bool) -> Self {
306 self.secure = on;
307 self
308 }
309
310 #[must_use]
312 pub fn same_site(mut self, same_site: SameSite) -> Self {
313 self.same_site = Some(same_site);
314 self
315 }
316
317 #[must_use]
319 pub fn to_header_value(&self) -> String {
320 fn is_valid_cookie_name(name: &str) -> bool {
325 is_valid_header_name(name)
327 }
328
329 fn is_valid_cookie_value(value: &str) -> bool {
330 value.is_empty()
333 || value.bytes().all(|b| {
334 matches!(
335 b,
336 0x21
337 | 0x23..=0x2B
338 | 0x2D..=0x3A
339 | 0x3C..=0x5B
340 | 0x5D..=0x7E
341 )
342 })
343 }
344
345 fn is_valid_attr_value(value: &str) -> bool {
346 value
348 .bytes()
349 .all(|b| (0x21..=0x7E).contains(&b) && b != b';' && b != b',')
350 }
351
352 if !is_valid_cookie_name(&self.name) || !is_valid_cookie_value(&self.value) {
353 return String::new();
356 }
357
358 let mut out = String::new();
359 out.push_str(&self.name);
360 out.push('=');
361 out.push_str(&self.value);
362
363 if let Some(ref path) = self.path {
364 if is_valid_attr_value(path) {
365 out.push_str("; Path=");
366 out.push_str(path);
367 }
368 }
369 if let Some(ref domain) = self.domain {
370 if is_valid_attr_value(domain) {
371 out.push_str("; Domain=");
372 out.push_str(domain);
373 }
374 }
375 if let Some(max_age) = self.max_age {
376 out.push_str("; Max-Age=");
377 out.push_str(&max_age.to_string());
378 }
379 if let Some(same_site) = self.same_site {
380 out.push_str("; SameSite=");
381 out.push_str(same_site.as_str());
382 }
383 if self.http_only {
384 out.push_str("; HttpOnly");
385 }
386 if self.secure {
387 out.push_str("; Secure");
388 }
389
390 out
391 }
392}
393
394#[derive(Debug)]
396pub struct Response {
397 status: StatusCode,
398 headers: Vec<(String, Vec<u8>)>,
399 body: ResponseBody,
400}
401
402impl Response {
403 #[must_use]
405 pub fn with_status(status: StatusCode) -> Self {
406 Self {
407 status,
408 headers: Vec::new(),
409 body: ResponseBody::Empty,
410 }
411 }
412
413 #[must_use]
415 pub fn ok() -> Self {
416 Self::with_status(StatusCode::OK)
417 }
418
419 #[must_use]
421 pub fn created() -> Self {
422 Self::with_status(StatusCode::CREATED)
423 }
424
425 #[must_use]
427 pub fn no_content() -> Self {
428 Self::with_status(StatusCode::NO_CONTENT)
429 }
430
431 #[must_use]
433 pub fn internal_error() -> Self {
434 Self::with_status(StatusCode::INTERNAL_SERVER_ERROR)
435 }
436
437 #[must_use]
452 pub fn partial_content() -> Self {
453 Self::with_status(StatusCode::PARTIAL_CONTENT)
454 }
455
456 #[must_use]
470 pub fn range_not_satisfiable() -> Self {
471 Self::with_status(StatusCode::RANGE_NOT_SATISFIABLE)
472 }
473
474 #[must_use]
479 pub fn not_modified() -> Self {
480 Self::with_status(StatusCode::NOT_MODIFIED)
481 }
482
483 #[must_use]
487 pub fn precondition_failed() -> Self {
488 Self::with_status(StatusCode::PRECONDITION_FAILED)
489 }
490
491 #[must_use]
501 pub fn with_etag(self, etag: impl Into<String>) -> Self {
502 self.header("ETag", etag.into().into_bytes())
503 }
504
505 #[must_use]
509 pub fn with_weak_etag(self, etag: impl Into<String>) -> Self {
510 let etag = etag.into();
511 let value = if etag.starts_with("W/") {
512 etag
513 } else {
514 format!("W/{}", etag)
515 };
516 self.header("ETag", value.into_bytes())
517 }
518
519 #[must_use]
527 pub fn header(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
528 let name = name.into();
529 let value = value.into();
530
531 if !is_valid_header_name(&name) {
533 return self;
535 }
536
537 let sanitized_value = sanitize_header_value(value);
539
540 self.headers.push((name, sanitized_value));
541 self
542 }
543
544 #[must_use]
549 pub fn remove_header(mut self, name: &str) -> Self {
550 self.headers.retain(|(n, _)| !n.eq_ignore_ascii_case(name));
551 self
552 }
553
554 #[must_use]
556 pub fn body(mut self, body: ResponseBody) -> Self {
557 self.body = body;
558 self
559 }
560
561 #[must_use]
576 pub fn set_cookie(self, cookie: SetCookie) -> Self {
577 let v = cookie.to_header_value();
578 if v.is_empty() {
579 return self;
580 }
581 self.header("set-cookie", v.into_bytes())
582 }
583
584 #[must_use]
598 pub fn delete_cookie(self, name: &str) -> Self {
599 let cookie = SetCookie::new(name, "").max_age(0);
601 self.set_cookie(cookie)
602 }
603
604 pub fn json<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> {
610 let bytes = serde_json::to_vec(value)?;
611 Ok(Self::ok()
612 .header("content-type", b"application/json".to_vec())
613 .body(ResponseBody::Bytes(bytes)))
614 }
615
616 #[must_use]
618 pub fn status(&self) -> StatusCode {
619 self.status
620 }
621
622 #[must_use]
624 pub fn headers(&self) -> &[(String, Vec<u8>)] {
625 &self.headers
626 }
627
628 #[must_use]
630 pub fn body_ref(&self) -> &ResponseBody {
631 &self.body
632 }
633
634 #[must_use]
636 pub fn into_parts(self) -> (StatusCode, Vec<(String, Vec<u8>)>, ResponseBody) {
637 (self.status, self.headers, self.body)
638 }
639
640 #[must_use]
655 pub fn rebuild_with_headers(mut self, headers: Vec<(String, Vec<u8>)>) -> Self {
656 for (name, value) in headers {
657 self = self.header(name, value);
658 }
659 self
660 }
661}
662
663pub trait IntoResponse {
665 fn into_response(self) -> Response;
667}
668
669impl IntoResponse for Response {
670 fn into_response(self) -> Response {
671 self
672 }
673}
674
675impl IntoResponse for () {
676 fn into_response(self) -> Response {
677 Response::no_content()
678 }
679}
680
681impl IntoResponse for &'static str {
682 fn into_response(self) -> Response {
683 Response::ok()
684 .header("content-type", b"text/plain; charset=utf-8".to_vec())
685 .body(ResponseBody::Bytes(self.as_bytes().to_vec()))
686 }
687}
688
689impl IntoResponse for String {
690 fn into_response(self) -> Response {
691 Response::ok()
692 .header("content-type", b"text/plain; charset=utf-8".to_vec())
693 .body(ResponseBody::Bytes(self.into_bytes()))
694 }
695}
696
697impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
698 fn into_response(self) -> Response {
699 match self {
700 Ok(v) => v.into_response(),
701 Err(e) => e.into_response(),
702 }
703 }
704}
705
706impl IntoResponse for std::convert::Infallible {
707 fn into_response(self) -> Response {
708 match self {}
709 }
710}
711
712impl<T: Serialize> IntoResponse for crate::extract::Json<T> {
717 fn into_response(self) -> Response {
718 match Response::json(&self.0) {
719 Ok(response) => response,
720 Err(_) => crate::error::HttpError::internal().into_response(),
721 }
722 }
723}
724
725pub trait ResponseProduces<T> {}
762
763impl<T> ResponseProduces<T> for T {}
765
766impl<T: serde::Serialize + 'static> ResponseProduces<T> for crate::extract::Json<T> {}
768
769#[derive(Debug, Clone)]
792pub struct Redirect {
793 status: StatusCode,
794 location: String,
795}
796
797impl Redirect {
798 #[must_use]
802 pub fn temporary(location: impl Into<String>) -> Self {
803 Self {
804 status: StatusCode::TEMPORARY_REDIRECT,
805 location: location.into(),
806 }
807 }
808
809 #[must_use]
814 pub fn permanent(location: impl Into<String>) -> Self {
815 Self {
816 status: StatusCode::PERMANENT_REDIRECT,
817 location: location.into(),
818 }
819 }
820
821 #[must_use]
826 pub fn see_other(location: impl Into<String>) -> Self {
827 Self {
828 status: StatusCode::SEE_OTHER,
829 location: location.into(),
830 }
831 }
832
833 #[must_use]
837 pub fn moved_permanently(location: impl Into<String>) -> Self {
838 Self {
839 status: StatusCode::MOVED_PERMANENTLY,
840 location: location.into(),
841 }
842 }
843
844 #[must_use]
848 pub fn found(location: impl Into<String>) -> Self {
849 Self {
850 status: StatusCode::FOUND,
851 location: location.into(),
852 }
853 }
854
855 #[must_use]
857 pub fn location(&self) -> &str {
858 &self.location
859 }
860
861 #[must_use]
863 pub fn status(&self) -> StatusCode {
864 self.status
865 }
866}
867
868impl IntoResponse for Redirect {
869 fn into_response(self) -> Response {
870 Response::with_status(self.status).header("location", self.location.into_bytes())
871 }
872}
873
874#[derive(Debug, Clone)]
884pub struct Html(String);
885
886impl Html {
887 #[must_use]
894 pub fn new(content: impl Into<String>) -> Self {
895 Self(content.into())
896 }
897
898 #[must_use]
903 pub fn escaped(content: impl AsRef<str>) -> Self {
904 Self(escape_html(content.as_ref()))
905 }
906
907 #[must_use]
909 pub fn content(&self) -> &str {
910 &self.0
911 }
912}
913
914fn escape_html(s: &str) -> String {
916 let mut out = String::with_capacity(s.len());
917 for c in s.chars() {
918 match c {
919 '&' => out.push_str("&"),
920 '<' => out.push_str("<"),
921 '>' => out.push_str(">"),
922 '"' => out.push_str("""),
923 '\'' => out.push_str("'"),
924 _ => out.push(c),
925 }
926 }
927 out
928}
929
930impl IntoResponse for Html {
931 fn into_response(self) -> Response {
932 Response::ok()
933 .header("content-type", b"text/html; charset=utf-8".to_vec())
934 .body(ResponseBody::Bytes(self.0.into_bytes()))
935 }
936}
937
938impl<S: Into<String>> From<S> for Html {
939 fn from(s: S) -> Self {
940 Self::new(s)
941 }
942}
943
944#[derive(Debug, Clone)]
957pub struct Text(String);
958
959impl Text {
960 #[must_use]
962 pub fn new(content: impl Into<String>) -> Self {
963 Self(content.into())
964 }
965
966 #[must_use]
968 pub fn content(&self) -> &str {
969 &self.0
970 }
971}
972
973impl IntoResponse for Text {
974 fn into_response(self) -> Response {
975 Response::ok()
976 .header("content-type", b"text/plain; charset=utf-8".to_vec())
977 .body(ResponseBody::Bytes(self.0.into_bytes()))
978 }
979}
980
981impl<S: Into<String>> From<S> for Text {
982 fn from(s: S) -> Self {
983 Self::new(s)
984 }
985}
986
987#[derive(Debug, Clone, Copy, Default)]
1001pub struct NoContent;
1002
1003impl IntoResponse for NoContent {
1004 fn into_response(self) -> Response {
1005 Response::no_content()
1006 }
1007}
1008
1009#[derive(Debug, Clone)]
1022pub struct Binary(Vec<u8>);
1023
1024impl Binary {
1025 #[must_use]
1027 pub fn new(data: impl Into<Vec<u8>>) -> Self {
1028 Self(data.into())
1029 }
1030
1031 #[must_use]
1033 pub fn data(&self) -> &[u8] {
1034 &self.0
1035 }
1036
1037 #[must_use]
1039 pub fn with_content_type(self, content_type: &str) -> BinaryWithType {
1040 BinaryWithType {
1041 data: self.0,
1042 content_type: content_type.to_string(),
1043 }
1044 }
1045}
1046
1047impl IntoResponse for Binary {
1048 fn into_response(self) -> Response {
1049 Response::ok()
1050 .header("content-type", b"application/octet-stream".to_vec())
1051 .body(ResponseBody::Bytes(self.0))
1052 }
1053}
1054
1055impl From<Vec<u8>> for Binary {
1056 fn from(data: Vec<u8>) -> Self {
1057 Self::new(data)
1058 }
1059}
1060
1061impl From<&[u8]> for Binary {
1062 fn from(data: &[u8]) -> Self {
1063 Self::new(data.to_vec())
1064 }
1065}
1066
1067#[derive(Debug, Clone)]
1078pub struct BinaryWithType {
1079 data: Vec<u8>,
1080 content_type: String,
1081}
1082
1083impl BinaryWithType {
1084 pub fn data(&self) -> &[u8] {
1086 &self.data
1087 }
1088
1089 pub fn content_type(&self) -> &str {
1091 &self.content_type
1092 }
1093}
1094
1095impl IntoResponse for BinaryWithType {
1096 fn into_response(self) -> Response {
1097 Response::ok()
1098 .header("content-type", self.content_type.into_bytes())
1099 .body(ResponseBody::Bytes(self.data))
1100 }
1101}
1102
1103#[derive(Debug)]
1124pub struct FileResponse {
1125 path: std::path::PathBuf,
1126 content_type: Option<String>,
1127 download_name: Option<String>,
1128 inline: bool,
1129}
1130
1131impl FileResponse {
1132 #[must_use]
1136 pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
1137 Self {
1138 path: path.into(),
1139 content_type: None,
1140 download_name: None,
1141 inline: true,
1142 }
1143 }
1144
1145 #[must_use]
1147 pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
1148 self.content_type = Some(content_type.into());
1149 self
1150 }
1151
1152 #[must_use]
1156 pub fn download_as(mut self, filename: impl Into<String>) -> Self {
1157 self.download_name = Some(filename.into());
1158 self.inline = false;
1159 self
1160 }
1161
1162 #[must_use]
1166 pub fn inline(mut self) -> Self {
1167 self.inline = true;
1168 self.download_name = None;
1169 self
1170 }
1171
1172 #[must_use]
1174 pub fn path(&self) -> &std::path::Path {
1175 &self.path
1176 }
1177
1178 fn infer_content_type(&self) -> &'static str {
1180 self.path
1181 .extension()
1182 .and_then(|ext| ext.to_str())
1183 .map(|ext| mime_type_for_extension(ext))
1184 .unwrap_or("application/octet-stream")
1185 }
1186
1187 fn content_disposition(&self) -> String {
1189 if self.inline {
1190 "inline".to_string()
1191 } else if let Some(ref name) = self.download_name {
1192 format!("attachment; filename=\"{}\"", name.replace('"', "\\\""))
1194 } else {
1195 let filename = self
1197 .path
1198 .file_name()
1199 .and_then(|n| n.to_str())
1200 .unwrap_or("download");
1201 format!("attachment; filename=\"{}\"", filename.replace('"', "\\\""))
1202 }
1203 }
1204
1205 #[must_use]
1211 pub fn into_response_sync(self) -> Response {
1212 match std::fs::read(&self.path) {
1213 Ok(contents) => {
1214 let content_type = self
1215 .content_type
1216 .as_deref()
1217 .unwrap_or_else(|| self.infer_content_type());
1218
1219 Response::ok()
1220 .header("content-type", content_type.as_bytes().to_vec())
1221 .header(
1222 "content-disposition",
1223 self.content_disposition().into_bytes(),
1224 )
1225 .header("accept-ranges", b"bytes".to_vec())
1226 .body(ResponseBody::Bytes(contents))
1227 }
1228 Err(_) => Response::with_status(StatusCode::NOT_FOUND),
1229 }
1230 }
1231}
1232
1233impl IntoResponse for FileResponse {
1234 fn into_response(self) -> Response {
1235 self.into_response_sync()
1236 }
1237}
1238
1239#[must_use]
1244pub fn mime_type_for_extension(ext: &str) -> &'static str {
1245 match ext.to_ascii_lowercase().as_str() {
1246 "html" | "htm" => "text/html; charset=utf-8",
1248 "css" => "text/css; charset=utf-8",
1249 "js" | "mjs" => "text/javascript; charset=utf-8",
1250 "json" | "map" => "application/json",
1251 "xml" => "application/xml",
1252 "txt" => "text/plain; charset=utf-8",
1253 "csv" => "text/csv; charset=utf-8",
1254 "md" => "text/markdown; charset=utf-8",
1255
1256 "png" => "image/png",
1258 "jpg" | "jpeg" => "image/jpeg",
1259 "gif" => "image/gif",
1260 "webp" => "image/webp",
1261 "svg" => "image/svg+xml",
1262 "ico" => "image/x-icon",
1263 "bmp" => "image/bmp",
1264 "avif" => "image/avif",
1265
1266 "woff" => "font/woff",
1268 "woff2" => "font/woff2",
1269 "ttf" => "font/ttf",
1270 "otf" => "font/otf",
1271 "eot" => "application/vnd.ms-fontobject",
1272
1273 "mp3" => "audio/mpeg",
1275 "wav" => "audio/wav",
1276 "ogg" => "audio/ogg",
1277 "flac" => "audio/flac",
1278 "aac" => "audio/aac",
1279 "m4a" => "audio/mp4",
1280
1281 "mp4" => "video/mp4",
1283 "webm" => "video/webm",
1284 "avi" => "video/x-msvideo",
1285 "mov" => "video/quicktime",
1286 "mkv" => "video/x-matroska",
1287
1288 "pdf" => "application/pdf",
1290 "doc" => "application/msword",
1291 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1292 "xls" => "application/vnd.ms-excel",
1293 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1294 "ppt" => "application/vnd.ms-powerpoint",
1295 "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1296
1297 "zip" => "application/zip",
1299 "gz" | "gzip" => "application/gzip",
1300 "tar" => "application/x-tar",
1301 "rar" => "application/vnd.rar",
1302 "7z" => "application/x-7z-compressed",
1303
1304 "wasm" => "application/wasm",
1306
1307 _ => "application/octet-stream",
1308 }
1309}
1310
1311#[must_use]
1317#[allow(dead_code)] pub fn outcome_to_response<T, E>(outcome: Outcome<T, E>) -> Response
1319where
1320 T: IntoResponse,
1321 E: IntoResponse,
1322{
1323 match outcome {
1324 Outcome::Ok(value) => value.into_response(),
1325 Outcome::Err(err) => err.into_response(),
1326 Outcome::Cancelled(reason) => cancelled_to_response(&reason),
1327 Outcome::Panicked(_payload) => Response::with_status(StatusCode::INTERNAL_SERVER_ERROR),
1328 }
1329}
1330
1331#[allow(dead_code)] fn cancelled_to_response(reason: &CancelReason) -> Response {
1333 let status = match reason.kind() {
1334 CancelKind::Timeout => StatusCode::GATEWAY_TIMEOUT,
1335 CancelKind::Shutdown => StatusCode::SERVICE_UNAVAILABLE,
1336 _ => StatusCode::CLIENT_CLOSED_REQUEST,
1337 };
1338 Response::with_status(status)
1339}
1340
1341#[derive(Debug, Clone, Default)]
1369#[allow(clippy::struct_excessive_bools)] pub struct ResponseModelConfig {
1371 pub include: Option<std::collections::HashSet<String>>,
1374
1375 pub exclude: Option<std::collections::HashSet<String>>,
1377
1378 pub by_alias: bool,
1380
1381 pub exclude_unset: bool,
1384
1385 pub exclude_defaults: bool,
1387
1388 pub exclude_none: bool,
1390
1391 aliases: Option<&'static [(&'static str, &'static str)]>,
1401
1402 defaults_json: Option<fn() -> Result<serde_json::Value, String>>,
1408
1409 set_fields: Option<std::collections::HashSet<String>>,
1413}
1414
1415pub trait ResponseModelAliases {
1419 fn response_model_aliases() -> &'static [(&'static str, &'static str)];
1424}
1425
1426impl ResponseModelConfig {
1427 #[must_use]
1429 pub fn new() -> Self {
1430 Self::default()
1431 }
1432
1433 #[must_use]
1435 pub fn include(mut self, fields: std::collections::HashSet<String>) -> Self {
1436 self.include = Some(fields);
1437 self
1438 }
1439
1440 #[must_use]
1442 pub fn exclude(mut self, fields: std::collections::HashSet<String>) -> Self {
1443 self.exclude = Some(fields);
1444 self
1445 }
1446
1447 #[must_use]
1452 pub fn by_alias(mut self, value: bool) -> Self {
1453 self.by_alias = value;
1454 self
1455 }
1456
1457 #[must_use]
1462 pub fn exclude_unset(mut self, value: bool) -> Self {
1463 self.exclude_unset = value;
1464 self
1465 }
1466
1467 #[must_use]
1473 pub fn exclude_defaults(mut self, value: bool) -> Self {
1474 self.exclude_defaults = value;
1475 self
1476 }
1477
1478 #[must_use]
1480 pub fn exclude_none(mut self, value: bool) -> Self {
1481 self.exclude_none = value;
1482 self
1483 }
1484
1485 #[must_use]
1487 pub fn with_aliases(mut self, aliases: &'static [(&'static str, &'static str)]) -> Self {
1488 self.aliases = Some(aliases);
1489 self
1490 }
1491
1492 #[must_use]
1494 pub fn with_aliases_from<T: ResponseModelAliases>(mut self) -> Self {
1495 self.aliases = Some(T::response_model_aliases());
1496 self
1497 }
1498
1499 #[must_use]
1501 pub fn with_defaults_json_provider(
1502 mut self,
1503 provider: fn() -> Result<serde_json::Value, String>,
1504 ) -> Self {
1505 self.defaults_json = Some(provider);
1506 self
1507 }
1508
1509 fn defaults_json_for<T: Default + Serialize>() -> Result<serde_json::Value, String> {
1510 serde_json::to_value(T::default()).map_err(|e| e.to_string())
1511 }
1512
1513 #[must_use]
1515 pub fn with_defaults_from<T: Default + Serialize>(mut self) -> Self {
1516 self.defaults_json = Some(Self::defaults_json_for::<T>);
1517 self
1518 }
1519
1520 #[must_use]
1522 pub fn with_set_fields(mut self, fields: std::collections::HashSet<String>) -> Self {
1523 self.set_fields = Some(fields);
1524 self
1525 }
1526
1527 #[must_use]
1529 pub fn has_filtering(&self) -> bool {
1530 self.include.is_some()
1531 || self.exclude.is_some()
1532 || self.exclude_none
1533 || self.exclude_unset
1534 || self.exclude_defaults
1535 || self.by_alias
1536 }
1537
1538 #[allow(clippy::result_large_err)]
1545 pub fn filter_json(
1546 &self,
1547 value: serde_json::Value,
1548 ) -> Result<serde_json::Value, crate::error::ResponseValidationError> {
1549 let serde_json::Value::Object(mut map) = value else {
1550 return Ok(value);
1551 };
1552
1553 if let Some(aliases) = self.aliases {
1556 normalize_to_canonical(&mut map, aliases)?;
1557 }
1558
1559 if self.exclude_unset {
1561 let set_fields = self.set_fields.as_ref().ok_or_else(|| {
1562 crate::error::ResponseValidationError::serialization_failed(
1563 "response_model_exclude_unset requires set-fields metadata \
1564 (use ResponseModelConfig::with_set_fields)",
1565 )
1566 })?;
1567 map.retain(|k, _| set_fields.contains(k));
1568 }
1569
1570 if let Some(ref include_set) = self.include {
1572 map.retain(|key, _| include_set.contains(key));
1573 }
1574
1575 if let Some(ref exclude_set) = self.exclude {
1577 map.retain(|key, _| !exclude_set.contains(key));
1578 }
1579
1580 if self.exclude_none {
1582 map.retain(|_, v| !v.is_null());
1583 }
1584
1585 if self.exclude_defaults {
1587 let provider = self.defaults_json.ok_or_else(|| {
1588 crate::error::ResponseValidationError::serialization_failed(
1589 "response_model_exclude_defaults requires defaults metadata \
1590 (use ResponseModelConfig::with_defaults_from::<T>() or \
1591 ResponseModelConfig::with_defaults_json_provider)",
1592 )
1593 })?;
1594 let defaults =
1595 provider().map_err(crate::error::ResponseValidationError::serialization_failed)?;
1596 let serde_json::Value::Object(defaults_map) = defaults else {
1597 return Err(crate::error::ResponseValidationError::serialization_failed(
1598 "defaults provider did not return a JSON object",
1599 ));
1600 };
1601
1602 for (k, default_v) in defaults_map {
1604 if map.get(&k).is_some_and(|v| v == &default_v) {
1605 map.remove(&k);
1606 }
1607 }
1608 }
1609
1610 if self.by_alias {
1612 let aliases = self.aliases.ok_or_else(|| {
1613 crate::error::ResponseValidationError::serialization_failed(
1614 "response_model_by_alias requires alias metadata \
1615 (use ResponseModelConfig::with_aliases(...) or \
1616 ResponseModelConfig::with_aliases_from::<T>())",
1617 )
1618 })?;
1619 apply_aliases(&mut map, aliases)?;
1620 }
1621
1622 Ok(serde_json::Value::Object(map))
1623 }
1624}
1625
1626#[allow(clippy::result_large_err)]
1627fn normalize_to_canonical(
1628 map: &mut serde_json::Map<String, serde_json::Value>,
1629 aliases: &[(&'static str, &'static str)],
1630) -> Result<(), crate::error::ResponseValidationError> {
1631 for (canonical, alias) in aliases {
1632 if canonical == alias {
1633 continue;
1634 }
1635 let canonical = *canonical;
1636 let alias = *alias;
1637
1638 if map.contains_key(canonical) && map.contains_key(alias) {
1639 return Err(crate::error::ResponseValidationError::serialization_failed(
1641 format!(
1642 "response model contains both canonical field '{canonical}' and alias '{alias}'"
1643 ),
1644 ));
1645 }
1646
1647 if let Some(v) = map.remove(alias) {
1648 map.insert(canonical.to_string(), v);
1649 }
1650 }
1651 Ok(())
1652}
1653
1654#[allow(clippy::result_large_err)]
1655fn apply_aliases(
1656 map: &mut serde_json::Map<String, serde_json::Value>,
1657 aliases: &[(&'static str, &'static str)],
1658) -> Result<(), crate::error::ResponseValidationError> {
1659 for (canonical, alias) in aliases {
1660 if canonical == alias {
1661 continue;
1662 }
1663 let canonical = *canonical;
1664 let alias = *alias;
1665
1666 if map.contains_key(canonical) && map.contains_key(alias) {
1667 return Err(crate::error::ResponseValidationError::serialization_failed(
1668 format!(
1669 "response model contains both canonical field '{canonical}' and alias '{alias}'"
1670 ),
1671 ));
1672 }
1673
1674 if let Some(v) = map.remove(canonical) {
1675 map.insert(alias.to_string(), v);
1676 }
1677 }
1678 Ok(())
1679}
1680
1681pub trait ResponseModel: Serialize {
1687 #[allow(clippy::result_large_err)] fn validate(&self) -> Result<(), crate::error::ResponseValidationError> {
1692 Ok(())
1694 }
1695
1696 fn model_name() -> &'static str {
1698 std::any::type_name::<Self>()
1699 }
1700}
1701
1702impl<T: Serialize> ResponseModel for T {}
1704
1705#[derive(Debug)]
1737pub struct ValidatedResponse<T> {
1738 pub value: T,
1740 pub config: ResponseModelConfig,
1742}
1743
1744impl<T> ValidatedResponse<T> {
1745 #[must_use]
1747 pub fn new(value: T) -> Self {
1748 Self {
1749 value,
1750 config: ResponseModelConfig::default(),
1751 }
1752 }
1753
1754 #[must_use]
1756 pub fn with_config(mut self, config: ResponseModelConfig) -> Self {
1757 self.config = config;
1758 self
1759 }
1760}
1761
1762impl<T: Serialize + ResponseModel> IntoResponse for ValidatedResponse<T> {
1763 fn into_response(self) -> Response {
1764 if let Err(error) = self.value.validate() {
1766 return error.into_response();
1767 }
1768
1769 let json_value = match serde_json::to_value(&self.value) {
1771 Ok(v) => v,
1772 Err(e) => {
1773 let error =
1775 crate::error::ResponseValidationError::serialization_failed(e.to_string());
1776 return error.into_response();
1777 }
1778 };
1779
1780 let filtered = match self.config.filter_json(json_value) {
1782 Ok(v) => v,
1783 Err(e) => return e.into_response(),
1784 };
1785
1786 let bytes = match serde_json::to_vec(&filtered) {
1788 Ok(b) => b,
1789 Err(e) => {
1790 let error =
1791 crate::error::ResponseValidationError::serialization_failed(e.to_string());
1792 return error.into_response();
1793 }
1794 };
1795
1796 Response::ok()
1797 .header("content-type", b"application/json".to_vec())
1798 .body(ResponseBody::Bytes(bytes))
1799 }
1800}
1801
1802#[must_use]
1806pub fn exclude_fields<T: Serialize + ResponseModel>(
1807 value: T,
1808 fields: &[&str],
1809) -> ValidatedResponse<T> {
1810 ValidatedResponse::new(value).with_config(
1811 ResponseModelConfig::new().exclude(fields.iter().map(|s| (*s).to_string()).collect()),
1812 )
1813}
1814
1815#[must_use]
1819pub fn include_fields<T: Serialize + ResponseModel>(
1820 value: T,
1821 fields: &[&str],
1822) -> ValidatedResponse<T> {
1823 ValidatedResponse::new(value).with_config(
1824 ResponseModelConfig::new().include(fields.iter().map(|s| (*s).to_string()).collect()),
1825 )
1826}
1827
1828pub fn check_if_none_match(if_none_match: &str, current_etag: &str) -> bool {
1854 let if_none_match = if_none_match.trim();
1855
1856 if if_none_match == "*" {
1858 return false; }
1860
1861 let current_stripped = strip_weak_prefix(current_etag.trim());
1862
1863 for candidate in if_none_match.split(',') {
1865 let candidate = strip_weak_prefix(candidate.trim());
1866 if candidate == current_stripped {
1867 return false; }
1869 }
1870
1871 true }
1873
1874pub fn check_if_match(if_match: &str, current_etag: &str) -> bool {
1882 let if_match = if_match.trim();
1883
1884 if if_match == "*" {
1886 return true;
1887 }
1888
1889 let current = current_etag.trim();
1890
1891 if current.starts_with("W/") {
1893 return false;
1894 }
1895
1896 for candidate in if_match.split(',') {
1897 let candidate = candidate.trim();
1898 if candidate.starts_with("W/") {
1900 continue;
1901 }
1902 if candidate == current {
1903 return true;
1904 }
1905 }
1906
1907 false
1908}
1909
1910fn strip_weak_prefix(etag: &str) -> &str {
1912 etag.strip_prefix("W/").unwrap_or(etag)
1913}
1914
1915pub fn apply_conditional(
1931 request_headers: &[(String, Vec<u8>)],
1932 method: crate::request::Method,
1933 response: Response,
1934) -> Response {
1935 let response_etag = response
1937 .headers()
1938 .iter()
1939 .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
1940 .and_then(|(_, value)| std::str::from_utf8(value).ok())
1941 .map(String::from);
1942
1943 let Some(response_etag) = response_etag else {
1944 return response; };
1946
1947 if matches!(
1949 method,
1950 crate::request::Method::Get | crate::request::Method::Head
1951 ) {
1952 if let Some(if_none_match) = find_header(request_headers, "if-none-match") {
1953 if !check_if_none_match(&if_none_match, &response_etag) {
1954 return Response::not_modified().with_etag(response_etag);
1955 }
1956 }
1957 }
1958
1959 if matches!(
1961 method,
1962 crate::request::Method::Put
1963 | crate::request::Method::Patch
1964 | crate::request::Method::Delete
1965 ) {
1966 if let Some(if_match) = find_header(request_headers, "if-match") {
1967 if !check_if_match(&if_match, &response_etag) {
1968 return Response::precondition_failed();
1969 }
1970 }
1971 }
1972
1973 response
1974}
1975
1976fn find_header(headers: &[(String, Vec<u8>)], name: &str) -> Option<String> {
1978 headers
1979 .iter()
1980 .find(|(n, _)| n.eq_ignore_ascii_case(name))
1981 .and_then(|(_, v)| std::str::from_utf8(v).ok())
1982 .map(String::from)
1983}
1984
1985#[derive(Debug, Clone, PartialEq, Eq)]
1991pub enum LinkRel {
1992 Self_,
1994 Next,
1996 Prev,
1998 First,
2000 Last,
2002 Related,
2004 Alternate,
2006 Custom(String),
2008}
2009
2010impl fmt::Display for LinkRel {
2011 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2012 match self {
2013 Self::Self_ => write!(f, "self"),
2014 Self::Next => write!(f, "next"),
2015 Self::Prev => write!(f, "prev"),
2016 Self::First => write!(f, "first"),
2017 Self::Last => write!(f, "last"),
2018 Self::Related => write!(f, "related"),
2019 Self::Alternate => write!(f, "alternate"),
2020 Self::Custom(s) => write!(f, "{s}"),
2021 }
2022 }
2023}
2024
2025#[derive(Debug, Clone)]
2027pub struct Link {
2028 url: String,
2029 rel: LinkRel,
2030 title: Option<String>,
2031 media_type: Option<String>,
2032}
2033
2034impl Link {
2035 pub fn new(url: impl Into<String>, rel: LinkRel) -> Self {
2037 Self {
2038 url: url.into(),
2039 rel,
2040 title: None,
2041 media_type: None,
2042 }
2043 }
2044
2045 #[must_use]
2047 pub fn title(mut self, title: impl Into<String>) -> Self {
2048 self.title = Some(title.into());
2049 self
2050 }
2051
2052 #[must_use]
2054 pub fn media_type(mut self, media_type: impl Into<String>) -> Self {
2055 self.media_type = Some(media_type.into());
2056 self
2057 }
2058}
2059
2060impl fmt::Display for Link {
2061 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2062 write!(f, "<{}>; rel=\"{}\"", self.url, self.rel)?;
2063 if let Some(ref title) = self.title {
2064 write!(f, "; title=\"{title}\"")?;
2065 }
2066 if let Some(ref mt) = self.media_type {
2067 write!(f, "; type=\"{mt}\"")?;
2068 }
2069 Ok(())
2070 }
2071}
2072
2073#[derive(Debug, Clone, Default)]
2092pub struct LinkHeader {
2093 links: Vec<Link>,
2094}
2095
2096impl LinkHeader {
2097 #[must_use]
2099 pub fn new() -> Self {
2100 Self::default()
2101 }
2102
2103 #[must_use]
2105 pub fn link(mut self, url: impl Into<String>, rel: LinkRel) -> Self {
2106 self.links.push(Link::new(url, rel));
2107 self
2108 }
2109
2110 #[must_use]
2112 #[allow(clippy::should_implement_trait)]
2113 pub fn add(mut self, link: Link) -> Self {
2114 self.links.push(link);
2115 self
2116 }
2117
2118 #[must_use]
2123 pub fn paginate(self, base_url: &str, page: u64, per_page: u64, total: u64) -> Self {
2124 let last_page = if total == 0 {
2125 1
2126 } else {
2127 total.div_ceil(per_page)
2128 };
2129 let sep = if base_url.contains('?') { '&' } else { '?' };
2130
2131 let mut h = self.link(
2132 format!("{base_url}{sep}page={page}&per_page={per_page}"),
2133 LinkRel::Self_,
2134 );
2135 h = h.link(
2136 format!("{base_url}{sep}page=1&per_page={per_page}"),
2137 LinkRel::First,
2138 );
2139 h = h.link(
2140 format!("{base_url}{sep}page={last_page}&per_page={per_page}"),
2141 LinkRel::Last,
2142 );
2143 if page > 1 {
2144 h = h.link(
2145 format!("{base_url}{sep}page={}&per_page={per_page}", page - 1),
2146 LinkRel::Prev,
2147 );
2148 }
2149 if page < last_page {
2150 h = h.link(
2151 format!("{base_url}{sep}page={}&per_page={per_page}", page + 1),
2152 LinkRel::Next,
2153 );
2154 }
2155 h
2156 }
2157
2158 #[must_use]
2160 pub fn is_empty(&self) -> bool {
2161 self.links.is_empty()
2162 }
2163
2164 #[must_use]
2166 pub fn len(&self) -> usize {
2167 self.links.len()
2168 }
2169
2170 #[must_use]
2172 pub fn to_header_value(&self) -> String {
2173 self.to_string()
2174 }
2175
2176 pub fn apply(self, response: Response) -> Response {
2178 if self.is_empty() {
2179 return response;
2180 }
2181 response.header("link", self.to_string().into_bytes())
2182 }
2183}
2184
2185impl fmt::Display for LinkHeader {
2186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2187 for (i, link) in self.links.iter().enumerate() {
2188 if i > 0 {
2189 write!(f, ", ")?;
2190 }
2191 write!(f, "{link}")?;
2192 }
2193 Ok(())
2194 }
2195}
2196
2197#[cfg(test)]
2198mod tests {
2199 use super::*;
2200 use crate::error::HttpError;
2201
2202 #[test]
2203 fn response_remove_header_removes_all_instances_case_insensitive() {
2204 let resp = Response::ok()
2205 .header("X-Test", b"1".to_vec())
2206 .header("x-test", b"2".to_vec())
2207 .header("Other", b"3".to_vec())
2208 .remove_header("X-Test");
2209
2210 assert!(
2211 resp.headers()
2212 .iter()
2213 .all(|(n, _)| !n.eq_ignore_ascii_case("x-test"))
2214 );
2215 assert!(
2216 resp.headers()
2217 .iter()
2218 .any(|(n, _)| n.eq_ignore_ascii_case("other"))
2219 );
2220 }
2221
2222 #[test]
2223 fn outcome_ok_maps_to_response() {
2224 let response = Response::created();
2225 let mapped = outcome_to_response::<Response, HttpError>(Outcome::Ok(response));
2226 assert_eq!(mapped.status().as_u16(), 201);
2227 }
2228
2229 #[test]
2230 fn outcome_err_maps_to_response() {
2231 let mapped =
2232 outcome_to_response::<Response, HttpError>(Outcome::Err(HttpError::bad_request()));
2233 assert_eq!(mapped.status().as_u16(), 400);
2234 }
2235
2236 #[test]
2237 fn outcome_cancelled_timeout_maps_to_504() {
2238 let mapped =
2239 outcome_to_response::<Response, HttpError>(Outcome::Cancelled(CancelReason::timeout()));
2240 assert_eq!(mapped.status().as_u16(), 504);
2241 }
2242
2243 #[test]
2244 fn outcome_cancelled_user_maps_to_499() {
2245 let mapped = outcome_to_response::<Response, HttpError>(Outcome::Cancelled(
2246 CancelReason::user("client disconnected"),
2247 ));
2248 assert_eq!(mapped.status().as_u16(), 499);
2249 }
2250
2251 #[test]
2252 fn outcome_panicked_maps_to_500() {
2253 let mapped = outcome_to_response::<Response, HttpError>(Outcome::Panicked(
2254 PanicPayload::new("boom"),
2255 ));
2256 assert_eq!(mapped.status().as_u16(), 500);
2257 }
2258
2259 #[test]
2264 fn redirect_temporary_returns_307() {
2265 let redirect = Redirect::temporary("/new-location");
2266 let response = redirect.into_response();
2267 assert_eq!(response.status().as_u16(), 307);
2268 }
2269
2270 #[test]
2271 fn redirect_permanent_returns_308() {
2272 let redirect = Redirect::permanent("/moved");
2273 let response = redirect.into_response();
2274 assert_eq!(response.status().as_u16(), 308);
2275 }
2276
2277 #[test]
2278 fn redirect_see_other_returns_303() {
2279 let redirect = Redirect::see_other("/result");
2280 let response = redirect.into_response();
2281 assert_eq!(response.status().as_u16(), 303);
2282 }
2283
2284 #[test]
2285 fn redirect_moved_permanently_returns_301() {
2286 let redirect = Redirect::moved_permanently("/gone");
2287 let response = redirect.into_response();
2288 assert_eq!(response.status().as_u16(), 301);
2289 }
2290
2291 #[test]
2292 fn redirect_found_returns_302() {
2293 let redirect = Redirect::found("/elsewhere");
2294 let response = redirect.into_response();
2295 assert_eq!(response.status().as_u16(), 302);
2296 }
2297
2298 #[test]
2299 fn redirect_sets_location_header() {
2300 let redirect = Redirect::temporary("/target?query=1");
2301 let response = redirect.into_response();
2302
2303 let location = response
2304 .headers()
2305 .iter()
2306 .find(|(name, _)| name == "location")
2307 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2308
2309 assert_eq!(location, Some("/target?query=1".to_string()));
2310 }
2311
2312 #[test]
2313 fn redirect_location_accessor() {
2314 let redirect = Redirect::permanent("https://example.com/new");
2315 assert_eq!(redirect.location(), "https://example.com/new");
2316 }
2317
2318 #[test]
2319 fn redirect_status_accessor() {
2320 let redirect = Redirect::see_other("/done");
2321 assert_eq!(redirect.status().as_u16(), 303);
2322 }
2323
2324 #[test]
2329 fn html_response_has_correct_content_type() {
2330 let html = Html::new("<html><body>Hello</body></html>");
2331 let response = html.into_response();
2332
2333 let content_type = response
2334 .headers()
2335 .iter()
2336 .find(|(name, _)| name == "content-type")
2337 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2338
2339 assert_eq!(content_type, Some("text/html; charset=utf-8".to_string()));
2340 }
2341
2342 #[test]
2343 fn html_response_has_status_200() {
2344 let html = Html::new("<p>test</p>");
2345 let response = html.into_response();
2346 assert_eq!(response.status().as_u16(), 200);
2347 }
2348
2349 #[test]
2350 fn html_content_accessor() {
2351 let html = Html::new("<div>content</div>");
2352 assert_eq!(html.content(), "<div>content</div>");
2353 }
2354
2355 #[test]
2356 fn html_from_string() {
2357 let html: Html = "hello".into();
2358 assert_eq!(html.content(), "hello");
2359 }
2360
2361 #[test]
2366 fn text_response_has_correct_content_type() {
2367 let text = Text::new("Plain text content");
2368 let response = text.into_response();
2369
2370 let content_type = response
2371 .headers()
2372 .iter()
2373 .find(|(name, _)| name == "content-type")
2374 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2375
2376 assert_eq!(content_type, Some("text/plain; charset=utf-8".to_string()));
2377 }
2378
2379 #[test]
2380 fn text_response_has_status_200() {
2381 let text = Text::new("hello");
2382 let response = text.into_response();
2383 assert_eq!(response.status().as_u16(), 200);
2384 }
2385
2386 #[test]
2387 fn text_content_accessor() {
2388 let text = Text::new("my content");
2389 assert_eq!(text.content(), "my content");
2390 }
2391
2392 #[test]
2397 fn no_content_returns_204() {
2398 let response = NoContent.into_response();
2399 assert_eq!(response.status().as_u16(), 204);
2400 }
2401
2402 #[test]
2403 fn no_content_has_empty_body() {
2404 let response = NoContent.into_response();
2405 assert!(response.body_ref().is_empty());
2406 }
2407
2408 #[test]
2413 fn file_response_infers_png_content_type() {
2414 let file = FileResponse::new("/path/to/image.png");
2415 assert_eq!(file.path().to_str(), Some("/path/to/image.png"));
2417 }
2418
2419 #[test]
2420 fn file_response_download_as_sets_attachment() {
2421 let file = FileResponse::new("/data/report.csv").download_as("my-report.csv");
2422 let disposition = file.content_disposition();
2423 assert!(disposition.contains("attachment"));
2424 assert!(disposition.contains("my-report.csv"));
2425 }
2426
2427 #[test]
2428 fn file_response_inline_sets_inline() {
2429 let file = FileResponse::new("/image.png").inline();
2430 let disposition = file.content_disposition();
2431 assert_eq!(disposition, "inline");
2432 }
2433
2434 #[test]
2435 fn file_response_custom_content_type() {
2436 let temp_dir = std::env::temp_dir();
2438 let test_file = temp_dir.join("test_response_file.txt");
2439 std::fs::write(&test_file, b"test content").unwrap();
2440
2441 let file = FileResponse::new(&test_file).content_type("application/custom");
2442 let response = file.into_response();
2443
2444 let content_type = response
2445 .headers()
2446 .iter()
2447 .find(|(name, _)| name == "content-type")
2448 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2449
2450 assert_eq!(content_type, Some("application/custom".to_string()));
2451
2452 let _ = std::fs::remove_file(test_file);
2454 }
2455
2456 #[test]
2457 fn file_response_includes_accept_ranges_header() {
2458 let temp_dir = std::env::temp_dir();
2460 let test_file = temp_dir.join("test_accept_ranges.txt");
2461 std::fs::write(&test_file, b"test content for range support").unwrap();
2462
2463 let file = FileResponse::new(&test_file);
2464 let response = file.into_response();
2465
2466 let accept_ranges = response
2467 .headers()
2468 .iter()
2469 .find(|(name, _)| name == "accept-ranges")
2470 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2471
2472 assert_eq!(accept_ranges, Some("bytes".to_string()));
2473
2474 let _ = std::fs::remove_file(test_file);
2476 }
2477
2478 #[test]
2479 fn file_response_not_found_returns_404() {
2480 let file = FileResponse::new("/nonexistent/path/file.txt");
2481 let response = file.into_response();
2482 assert_eq!(response.status().as_u16(), 404);
2483 }
2484
2485 #[test]
2490 fn mime_type_for_common_extensions() {
2491 assert_eq!(mime_type_for_extension("html"), "text/html; charset=utf-8");
2492 assert_eq!(mime_type_for_extension("css"), "text/css; charset=utf-8");
2493 assert_eq!(
2494 mime_type_for_extension("js"),
2495 "text/javascript; charset=utf-8"
2496 );
2497 assert_eq!(mime_type_for_extension("json"), "application/json");
2498 assert_eq!(mime_type_for_extension("png"), "image/png");
2499 assert_eq!(mime_type_for_extension("jpg"), "image/jpeg");
2500 assert_eq!(mime_type_for_extension("pdf"), "application/pdf");
2501 assert_eq!(mime_type_for_extension("zip"), "application/zip");
2502 }
2503
2504 #[test]
2505 fn mime_type_case_insensitive() {
2506 assert_eq!(mime_type_for_extension("HTML"), "text/html; charset=utf-8");
2507 assert_eq!(mime_type_for_extension("PNG"), "image/png");
2508 assert_eq!(mime_type_for_extension("Json"), "application/json");
2509 }
2510
2511 #[test]
2512 fn mime_type_unknown_returns_octet_stream() {
2513 assert_eq!(
2514 mime_type_for_extension("unknown"),
2515 "application/octet-stream"
2516 );
2517 assert_eq!(mime_type_for_extension("xyz"), "application/octet-stream");
2518 }
2519
2520 #[test]
2525 fn status_code_see_other_is_303() {
2526 assert_eq!(StatusCode::SEE_OTHER.as_u16(), 303);
2527 }
2528
2529 #[test]
2530 fn status_code_see_other_canonical_reason() {
2531 assert_eq!(StatusCode::SEE_OTHER.canonical_reason(), "See Other");
2532 }
2533
2534 #[test]
2535 fn status_code_partial_content_is_206() {
2536 assert_eq!(StatusCode::PARTIAL_CONTENT.as_u16(), 206);
2537 }
2538
2539 #[test]
2540 fn status_code_partial_content_canonical_reason() {
2541 assert_eq!(
2542 StatusCode::PARTIAL_CONTENT.canonical_reason(),
2543 "Partial Content"
2544 );
2545 }
2546
2547 #[test]
2548 fn status_code_range_not_satisfiable_is_416() {
2549 assert_eq!(StatusCode::RANGE_NOT_SATISFIABLE.as_u16(), 416);
2550 }
2551
2552 #[test]
2553 fn status_code_range_not_satisfiable_canonical_reason() {
2554 assert_eq!(
2555 StatusCode::RANGE_NOT_SATISFIABLE.canonical_reason(),
2556 "Range Not Satisfiable"
2557 );
2558 }
2559
2560 #[test]
2561 fn response_partial_content_returns_206() {
2562 let response = Response::partial_content();
2563 assert_eq!(response.status().as_u16(), 206);
2564 }
2565
2566 #[test]
2567 fn response_range_not_satisfiable_returns_416() {
2568 let response = Response::range_not_satisfiable();
2569 assert_eq!(response.status().as_u16(), 416);
2570 }
2571
2572 #[test]
2577 fn response_set_cookie_adds_header() {
2578 let response = Response::ok().set_cookie(SetCookie::new("session", "abc123"));
2579
2580 let cookie_header = response
2581 .headers()
2582 .iter()
2583 .find(|(name, _)| name == "set-cookie")
2584 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2585
2586 assert!(cookie_header.is_some());
2587 let header_value = cookie_header.unwrap();
2588 assert!(header_value.contains("session=abc123"));
2589 }
2590
2591 #[test]
2592 fn response_set_cookie_with_attributes() {
2593 let response = Response::ok().set_cookie(
2594 SetCookie::new("session", "token123")
2595 .http_only(true)
2596 .secure(true)
2597 .same_site(SameSite::Strict)
2598 .max_age(3600)
2599 .path("/api"),
2600 );
2601
2602 let cookie_header = response
2603 .headers()
2604 .iter()
2605 .find(|(name, _)| name == "set-cookie")
2606 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2607 .unwrap();
2608
2609 assert!(cookie_header.contains("session=token123"));
2610 assert!(cookie_header.contains("HttpOnly"));
2611 assert!(cookie_header.contains("Secure"));
2612 assert!(cookie_header.contains("SameSite=Strict"));
2613 assert!(cookie_header.contains("Max-Age=3600"));
2614 assert!(cookie_header.contains("Path=/api"));
2615 }
2616
2617 #[test]
2618 fn response_set_multiple_cookies() {
2619 let response = Response::ok()
2620 .set_cookie(SetCookie::new("session", "abc"))
2621 .set_cookie(SetCookie::new("prefs", "dark"));
2622
2623 let cookie_headers: Vec<_> = response
2624 .headers()
2625 .iter()
2626 .filter(|(name, _)| name == "set-cookie")
2627 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2628 .collect();
2629
2630 assert_eq!(cookie_headers.len(), 2);
2631 assert!(cookie_headers.iter().any(|h| h.contains("session=abc")));
2632 assert!(cookie_headers.iter().any(|h| h.contains("prefs=dark")));
2633 }
2634
2635 #[test]
2636 fn response_delete_cookie_sets_max_age_zero() {
2637 let response = Response::ok().delete_cookie("session");
2638
2639 let cookie_header = response
2640 .headers()
2641 .iter()
2642 .find(|(name, _)| name == "set-cookie")
2643 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2644 .unwrap();
2645
2646 assert!(cookie_header.contains("session="));
2647 assert!(cookie_header.contains("Max-Age=0"));
2648 }
2649
2650 #[test]
2651 fn response_set_and_delete_cookies() {
2652 let response = Response::ok()
2654 .set_cookie(SetCookie::new("new_session", "xyz"))
2655 .delete_cookie("old_session");
2656
2657 let cookie_headers: Vec<_> = response
2658 .headers()
2659 .iter()
2660 .filter(|(name, _)| name == "set-cookie")
2661 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2662 .collect();
2663
2664 assert_eq!(cookie_headers.len(), 2);
2665 assert!(cookie_headers.iter().any(|h| h.contains("new_session=xyz")));
2666 assert!(
2667 cookie_headers
2668 .iter()
2669 .any(|h| h.contains("old_session=") && h.contains("Max-Age=0"))
2670 );
2671 }
2672
2673 #[test]
2678 fn binary_new_creates_from_vec() {
2679 let data = vec![0x01, 0x02, 0x03, 0x04];
2680 let binary = Binary::new(data.clone());
2681 assert_eq!(binary.data(), &data[..]);
2682 }
2683
2684 #[test]
2685 fn binary_new_creates_from_slice() {
2686 let data = [0xDE, 0xAD, 0xBE, 0xEF];
2687 let binary = Binary::new(&data[..]);
2688 assert_eq!(binary.data(), &data);
2689 }
2690
2691 #[test]
2692 fn binary_into_response_has_correct_content_type() {
2693 let binary = Binary::new(vec![1, 2, 3]);
2694 let response = binary.into_response();
2695
2696 let content_type = response
2697 .headers()
2698 .iter()
2699 .find(|(name, _)| name == "content-type")
2700 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2701
2702 assert_eq!(content_type, Some("application/octet-stream".to_string()));
2703 }
2704
2705 #[test]
2706 fn binary_into_response_has_status_200() {
2707 let binary = Binary::new(vec![1, 2, 3]);
2708 let response = binary.into_response();
2709 assert_eq!(response.status().as_u16(), 200);
2710 }
2711
2712 #[test]
2713 fn binary_into_response_has_correct_body() {
2714 let data = vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]; let binary = Binary::new(data.clone());
2716 let response = binary.into_response();
2717
2718 if let ResponseBody::Bytes(bytes) = response.body_ref() {
2719 assert_eq!(bytes, &data);
2720 } else {
2721 panic!("Expected Bytes body");
2722 }
2723 }
2724
2725 #[test]
2726 fn json_into_response_is_200_application_json_with_serialized_body() {
2727 #[derive(serde::Serialize)]
2728 struct Item {
2729 id: i64,
2730 name: &'static str,
2731 }
2732 let response = crate::extract::Json(Item {
2733 id: 7,
2734 name: "Widget",
2735 })
2736 .into_response();
2737
2738 assert_eq!(response.status().as_u16(), 200);
2739 let content_type = response
2740 .headers()
2741 .iter()
2742 .find(|(name, _)| name == "content-type")
2743 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2744 assert_eq!(content_type, Some("application/json".to_string()));
2745 if let ResponseBody::Bytes(bytes) = response.body_ref() {
2746 assert_eq!(bytes, br#"{"id":7,"name":"Widget"}"#);
2747 } else {
2748 panic!("Expected Bytes body");
2749 }
2750 }
2751
2752 #[test]
2753 fn json_into_response_maps_serialization_failure_to_500() {
2754 struct Unserializable;
2755 impl serde::Serialize for Unserializable {
2756 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
2757 Err(serde::ser::Error::custom("boom"))
2758 }
2759 }
2760 let response = crate::extract::Json(Unserializable).into_response();
2761 assert_eq!(response.status().as_u16(), 500);
2762 }
2763
2764 #[test]
2765 fn binary_with_content_type_returns_binary_with_type() {
2766 let data = vec![0x89, 0x50, 0x4E, 0x47]; let binary = Binary::new(data);
2768 let binary_typed = binary.with_content_type("image/png");
2769
2770 assert_eq!(binary_typed.content_type(), "image/png");
2771 }
2772
2773 #[test]
2774 fn binary_with_type_into_response_has_correct_content_type() {
2775 let data = vec![0xFF, 0xD8, 0xFF]; let binary = Binary::new(data).with_content_type("image/jpeg");
2777 let response = binary.into_response();
2778
2779 let content_type = response
2780 .headers()
2781 .iter()
2782 .find(|(name, _)| name == "content-type")
2783 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2784
2785 assert_eq!(content_type, Some("image/jpeg".to_string()));
2786 }
2787
2788 #[test]
2789 fn binary_with_type_into_response_has_correct_body() {
2790 let data = vec![0x25, 0x50, 0x44, 0x46]; let binary = Binary::new(data.clone()).with_content_type("application/pdf");
2792 let response = binary.into_response();
2793
2794 if let ResponseBody::Bytes(bytes) = response.body_ref() {
2795 assert_eq!(bytes, &data);
2796 } else {
2797 panic!("Expected Bytes body");
2798 }
2799 }
2800
2801 #[test]
2802 fn binary_with_type_data_accessor() {
2803 let data = vec![1, 2, 3, 4, 5];
2804 let binary = Binary::new(data.clone()).with_content_type("application/custom");
2805 assert_eq!(binary.data(), &data[..]);
2806 }
2807
2808 #[test]
2809 fn binary_with_type_status_200() {
2810 let binary = Binary::new(vec![0]).with_content_type("text/plain");
2811 let response = binary.into_response();
2812 assert_eq!(response.status().as_u16(), 200);
2813 }
2814
2815 #[test]
2820 fn response_model_config_default() {
2821 let config = ResponseModelConfig::new();
2822 assert!(config.include.is_none());
2823 assert!(config.exclude.is_none());
2824 assert!(!config.by_alias);
2825 assert!(!config.exclude_unset);
2826 assert!(!config.exclude_defaults);
2827 assert!(!config.exclude_none);
2828 }
2829
2830 #[test]
2831 fn response_model_config_include() {
2832 let fields: std::collections::HashSet<String> =
2833 ["id", "name"].iter().map(|s| (*s).to_string()).collect();
2834 let config = ResponseModelConfig::new().include(fields.clone());
2835 assert_eq!(config.include, Some(fields));
2836 }
2837
2838 #[test]
2839 fn response_model_config_exclude() {
2840 let fields: std::collections::HashSet<String> =
2841 ["password"].iter().map(|s| (*s).to_string()).collect();
2842 let config = ResponseModelConfig::new().exclude(fields.clone());
2843 assert_eq!(config.exclude, Some(fields));
2844 }
2845
2846 #[test]
2847 fn response_model_config_by_alias() {
2848 let config = ResponseModelConfig::new().by_alias(true);
2849 assert!(config.by_alias);
2850 }
2851
2852 #[test]
2853 fn response_model_config_exclude_none() {
2854 let config = ResponseModelConfig::new().exclude_none(true);
2855 assert!(config.exclude_none);
2856 }
2857
2858 #[test]
2859 fn response_model_config_exclude_unset() {
2860 let config = ResponseModelConfig::new().exclude_unset(true);
2861 assert!(config.exclude_unset);
2862 }
2863
2864 #[test]
2865 fn response_model_config_exclude_defaults() {
2866 let config = ResponseModelConfig::new().exclude_defaults(true);
2867 assert!(config.exclude_defaults);
2868 }
2869
2870 #[test]
2871 fn response_model_config_has_filtering() {
2872 let config = ResponseModelConfig::new();
2873 assert!(!config.has_filtering());
2874
2875 let config =
2876 ResponseModelConfig::new().include(["id"].iter().map(|s| (*s).to_string()).collect());
2877 assert!(config.has_filtering());
2878
2879 let config = ResponseModelConfig::new()
2880 .exclude(["password"].iter().map(|s| (*s).to_string()).collect());
2881 assert!(config.has_filtering());
2882
2883 let config = ResponseModelConfig::new().exclude_none(true);
2884 assert!(config.has_filtering());
2885 }
2886
2887 #[test]
2888 fn response_model_config_filter_json_include() {
2889 let config = ResponseModelConfig::new()
2890 .include(["id", "name"].iter().map(|s| (*s).to_string()).collect());
2891
2892 let value = serde_json::json!({
2893 "id": 1,
2894 "name": "Alice",
2895 "email": "alice@example.com",
2896 "password": "secret"
2897 });
2898
2899 let filtered = config.filter_json(value).unwrap();
2900 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2901 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2902 assert!(filtered.get("email").is_none());
2903 assert!(filtered.get("password").is_none());
2904 }
2905
2906 #[test]
2907 fn response_model_config_filter_json_exclude() {
2908 let config = ResponseModelConfig::new().exclude(
2909 ["password", "secret"]
2910 .iter()
2911 .map(|s| (*s).to_string())
2912 .collect(),
2913 );
2914
2915 let value = serde_json::json!({
2916 "id": 1,
2917 "name": "Alice",
2918 "password": "secret123",
2919 "secret": "hidden"
2920 });
2921
2922 let filtered = config.filter_json(value).unwrap();
2923 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2924 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2925 assert!(filtered.get("password").is_none());
2926 assert!(filtered.get("secret").is_none());
2927 }
2928
2929 #[test]
2930 fn response_model_config_filter_json_exclude_none() {
2931 let config = ResponseModelConfig::new().exclude_none(true);
2932
2933 let value = serde_json::json!({
2934 "id": 1,
2935 "name": "Alice",
2936 "middle_name": null,
2937 "nickname": null
2938 });
2939
2940 let filtered = config.filter_json(value).unwrap();
2941 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2942 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2943 assert!(filtered.get("middle_name").is_none());
2944 assert!(filtered.get("nickname").is_none());
2945 }
2946
2947 #[test]
2948 fn response_model_config_filter_json_combined() {
2949 let config = ResponseModelConfig::new()
2950 .include(
2951 ["id", "name", "email", "middle_name"]
2952 .iter()
2953 .map(|s| (*s).to_string())
2954 .collect(),
2955 )
2956 .exclude_none(true);
2957
2958 let value = serde_json::json!({
2959 "id": 1,
2960 "name": "Alice",
2961 "email": "alice@example.com",
2962 "middle_name": null,
2963 "password": "secret"
2964 });
2965
2966 let filtered = config.filter_json(value).unwrap();
2967 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2968 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2969 assert_eq!(
2970 filtered.get("email"),
2971 Some(&serde_json::json!("alice@example.com"))
2972 );
2973 assert!(filtered.get("middle_name").is_none()); assert!(filtered.get("password").is_none()); }
2976
2977 #[test]
2978 fn response_model_config_by_alias_requires_alias_metadata() {
2979 let config = ResponseModelConfig::new().by_alias(true);
2980 let value = serde_json::json!({"userId": 1, "name": "Alice"});
2981 assert!(config.filter_json(value).is_err());
2982 }
2983
2984 #[test]
2985 fn response_model_config_by_alias_normalizes_and_realiases() {
2986 static ALIASES: &[(&str, &str)] = &[("user_id", "userId")];
2987
2988 let config = ResponseModelConfig::new().with_aliases(ALIASES);
2990 let value = serde_json::json!({"userId": 1, "name": "Alice"});
2991 let filtered = config.filter_json(value).unwrap();
2992 assert_eq!(filtered.get("user_id"), Some(&serde_json::json!(1)));
2993 assert!(filtered.get("userId").is_none());
2994
2995 let config = ResponseModelConfig::new()
2997 .with_aliases(ALIASES)
2998 .by_alias(true);
2999 let value = serde_json::json!({"user_id": 1, "name": "Alice"});
3000 let filtered = config.filter_json(value).unwrap();
3001 assert_eq!(filtered.get("userId"), Some(&serde_json::json!(1)));
3002 assert!(filtered.get("user_id").is_none());
3003 }
3004
3005 #[test]
3006 fn response_model_config_exclude_defaults_requires_defaults_provider() {
3007 let config = ResponseModelConfig::new().exclude_defaults(true);
3008 let value = serde_json::json!({"active": false});
3009 assert!(config.filter_json(value).is_err());
3010 }
3011
3012 #[test]
3013 fn response_model_config_exclude_defaults_filters_matching_fields() {
3014 #[derive(Default, Serialize)]
3015 struct UserDefaults {
3016 active: bool,
3017 name: String,
3018 }
3019
3020 let config = ResponseModelConfig::new()
3022 .with_defaults_from::<UserDefaults>()
3023 .exclude_defaults(true);
3024 let value = serde_json::json!({"active": false, "name": "Alice"});
3025 let filtered = config.filter_json(value).unwrap();
3026 assert!(filtered.get("active").is_none());
3027 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
3028 }
3029
3030 #[test]
3031 fn response_model_config_exclude_unset_requires_set_fields() {
3032 let config = ResponseModelConfig::new().exclude_unset(true);
3033 let value = serde_json::json!({"id": 1, "name": "Alice"});
3034 assert!(config.filter_json(value).is_err());
3035 }
3036
3037 #[test]
3038 fn response_model_config_exclude_unset_filters_not_set() {
3039 let set_fields: std::collections::HashSet<String> =
3040 ["id", "name"].iter().map(|s| (*s).to_string()).collect();
3041 let config = ResponseModelConfig::new()
3042 .with_set_fields(set_fields)
3043 .exclude_unset(true);
3044 let value = serde_json::json!({"id": 1, "name": "Alice", "email": "a@b.com"});
3045 let filtered = config.filter_json(value).unwrap();
3046 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
3047 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
3048 assert!(filtered.get("email").is_none());
3049 }
3050
3051 #[test]
3056 fn validated_response_serializes_struct() {
3057 #[derive(Serialize)]
3058 struct User {
3059 id: i64,
3060 name: String,
3061 }
3062
3063 let user = User {
3064 id: 1,
3065 name: "Alice".to_string(),
3066 };
3067
3068 let response = ValidatedResponse::new(user).into_response();
3069 assert_eq!(response.status().as_u16(), 200);
3070
3071 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3072 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3073 assert_eq!(parsed["id"], 1);
3074 assert_eq!(parsed["name"], "Alice");
3075 } else {
3076 panic!("Expected Bytes body");
3077 }
3078 }
3079
3080 #[test]
3081 fn validated_response_excludes_fields() {
3082 #[derive(Serialize)]
3083 struct User {
3084 id: i64,
3085 name: String,
3086 password: String,
3087 }
3088
3089 let user = User {
3090 id: 1,
3091 name: "Alice".to_string(),
3092 password: "secret123".to_string(),
3093 };
3094
3095 let response = ValidatedResponse::new(user)
3096 .with_config(
3097 ResponseModelConfig::new()
3098 .exclude(["password"].iter().map(|s| (*s).to_string()).collect()),
3099 )
3100 .into_response();
3101
3102 assert_eq!(response.status().as_u16(), 200);
3103
3104 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3105 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3106 assert_eq!(parsed["id"], 1);
3107 assert_eq!(parsed["name"], "Alice");
3108 assert!(parsed.get("password").is_none());
3109 } else {
3110 panic!("Expected Bytes body");
3111 }
3112 }
3113
3114 #[test]
3115 fn validated_response_includes_fields() {
3116 #[derive(Serialize)]
3117 struct User {
3118 id: i64,
3119 name: String,
3120 email: String,
3121 password: String,
3122 }
3123
3124 let user = User {
3125 id: 1,
3126 name: "Alice".to_string(),
3127 email: "alice@example.com".to_string(),
3128 password: "secret123".to_string(),
3129 };
3130
3131 let response = ValidatedResponse::new(user)
3132 .with_config(
3133 ResponseModelConfig::new()
3134 .include(["id", "name"].iter().map(|s| (*s).to_string()).collect()),
3135 )
3136 .into_response();
3137
3138 assert_eq!(response.status().as_u16(), 200);
3139
3140 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3141 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3142 assert_eq!(parsed["id"], 1);
3143 assert_eq!(parsed["name"], "Alice");
3144 assert!(parsed.get("email").is_none());
3145 assert!(parsed.get("password").is_none());
3146 } else {
3147 panic!("Expected Bytes body");
3148 }
3149 }
3150
3151 #[test]
3152 fn validated_response_exclude_none_values() {
3153 #[derive(Serialize)]
3154 struct User {
3155 id: i64,
3156 name: String,
3157 nickname: Option<String>,
3158 }
3159
3160 let user = User {
3161 id: 1,
3162 name: "Alice".to_string(),
3163 nickname: None,
3164 };
3165
3166 let response = ValidatedResponse::new(user)
3167 .with_config(ResponseModelConfig::new().exclude_none(true))
3168 .into_response();
3169
3170 assert_eq!(response.status().as_u16(), 200);
3171
3172 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3173 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3174 assert_eq!(parsed["id"], 1);
3175 assert_eq!(parsed["name"], "Alice");
3176 assert!(parsed.get("nickname").is_none());
3177 } else {
3178 panic!("Expected Bytes body");
3179 }
3180 }
3181
3182 #[test]
3183 fn validated_response_content_type_is_json() {
3184 #[derive(Serialize)]
3185 struct Data {
3186 value: i32,
3187 }
3188
3189 let response = ValidatedResponse::new(Data { value: 42 }).into_response();
3190
3191 let content_type = response
3192 .headers()
3193 .iter()
3194 .find(|(name, _)| name == "content-type")
3195 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
3196
3197 assert_eq!(content_type, Some("application/json".to_string()));
3198 }
3199
3200 #[test]
3205 fn exclude_fields_helper() {
3206 #[derive(Serialize)]
3207 struct User {
3208 id: i64,
3209 name: String,
3210 password: String,
3211 }
3212
3213 let user = User {
3214 id: 1,
3215 name: "Alice".to_string(),
3216 password: "secret".to_string(),
3217 };
3218
3219 let response = exclude_fields(user, &["password"]).into_response();
3220
3221 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3222 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3223 assert!(parsed.get("id").is_some());
3224 assert!(parsed.get("name").is_some());
3225 assert!(parsed.get("password").is_none());
3226 } else {
3227 panic!("Expected Bytes body");
3228 }
3229 }
3230
3231 #[test]
3232 fn include_fields_helper() {
3233 #[derive(Serialize)]
3234 struct User {
3235 id: i64,
3236 name: String,
3237 email: String,
3238 password: String,
3239 }
3240
3241 let user = User {
3242 id: 1,
3243 name: "Alice".to_string(),
3244 email: "alice@example.com".to_string(),
3245 password: "secret".to_string(),
3246 };
3247
3248 let response = include_fields(user, &["id", "name"]).into_response();
3249
3250 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3251 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3252 assert!(parsed.get("id").is_some());
3253 assert!(parsed.get("name").is_some());
3254 assert!(parsed.get("email").is_none());
3255 assert!(parsed.get("password").is_none());
3256 } else {
3257 panic!("Expected Bytes body");
3258 }
3259 }
3260
3261 #[test]
3266 fn status_code_precondition_failed() {
3267 assert_eq!(StatusCode::PRECONDITION_FAILED.as_u16(), 412);
3268 assert_eq!(
3269 StatusCode::PRECONDITION_FAILED.canonical_reason(),
3270 "Precondition Failed"
3271 );
3272 }
3273
3274 #[test]
3275 fn response_not_modified_status() {
3276 let resp = Response::not_modified();
3277 assert_eq!(resp.status().as_u16(), 304);
3278 }
3279
3280 #[test]
3281 fn response_precondition_failed_status() {
3282 let resp = Response::precondition_failed();
3283 assert_eq!(resp.status().as_u16(), 412);
3284 }
3285
3286 #[test]
3287 fn response_with_etag() {
3288 let resp = Response::ok().with_etag("\"abc123\"");
3289 let etag = resp
3290 .headers()
3291 .iter()
3292 .find(|(n, _)| n == "ETag")
3293 .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3294 assert_eq!(etag, Some("\"abc123\"".to_string()));
3295 }
3296
3297 #[test]
3298 fn response_with_weak_etag() {
3299 let resp = Response::ok().with_weak_etag("\"abc123\"");
3300 let etag = resp
3301 .headers()
3302 .iter()
3303 .find(|(n, _)| n == "ETag")
3304 .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3305 assert_eq!(etag, Some("W/\"abc123\"".to_string()));
3306 }
3307
3308 #[test]
3309 fn response_with_weak_etag_already_prefixed() {
3310 let resp = Response::ok().with_weak_etag("W/\"abc123\"");
3311 let etag = resp
3312 .headers()
3313 .iter()
3314 .find(|(n, _)| n == "ETag")
3315 .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3316 assert_eq!(etag, Some("W/\"abc123\"".to_string()));
3317 }
3318
3319 #[test]
3320 fn check_if_none_match_exact() {
3321 assert!(!check_if_none_match("\"abc\"", "\"abc\""));
3323 }
3324
3325 #[test]
3326 fn check_if_none_match_no_match() {
3327 assert!(check_if_none_match("\"abc\"", "\"def\""));
3329 }
3330
3331 #[test]
3332 fn check_if_none_match_wildcard() {
3333 assert!(!check_if_none_match("*", "\"anything\""));
3334 }
3335
3336 #[test]
3337 fn check_if_none_match_multiple_etags() {
3338 assert!(!check_if_none_match("\"aaa\", \"bbb\", \"ccc\"", "\"bbb\""));
3340 assert!(check_if_none_match("\"aaa\", \"bbb\"", "\"ccc\""));
3342 }
3343
3344 #[test]
3345 fn check_if_none_match_weak_comparison() {
3346 assert!(!check_if_none_match("W/\"abc\"", "\"abc\""));
3348 assert!(!check_if_none_match("\"abc\"", "W/\"abc\""));
3349 assert!(!check_if_none_match("W/\"abc\"", "W/\"abc\""));
3350 }
3351
3352 #[test]
3353 fn check_if_match_exact() {
3354 assert!(check_if_match("\"abc\"", "\"abc\""));
3356 }
3357
3358 #[test]
3359 fn check_if_match_no_match() {
3360 assert!(!check_if_match("\"abc\"", "\"def\""));
3362 }
3363
3364 #[test]
3365 fn check_if_match_wildcard() {
3366 assert!(check_if_match("*", "\"anything\""));
3367 }
3368
3369 #[test]
3370 fn check_if_match_weak_etag_fails() {
3371 assert!(!check_if_match("W/\"abc\"", "\"abc\""));
3373 assert!(!check_if_match("\"abc\"", "W/\"abc\""));
3374 }
3375
3376 #[test]
3377 fn check_if_match_multiple_etags() {
3378 assert!(check_if_match("\"aaa\", \"bbb\"", "\"bbb\""));
3379 assert!(!check_if_match("\"aaa\", \"bbb\"", "\"ccc\""));
3380 }
3381
3382 #[test]
3383 fn apply_conditional_get_304() {
3384 use crate::request::Method;
3385
3386 let headers = vec![("If-None-Match".to_string(), b"\"abc123\"".to_vec())];
3387 let response = Response::ok().with_etag("\"abc123\"");
3388 let result = apply_conditional(&headers, Method::Get, response);
3389 assert_eq!(result.status().as_u16(), 304);
3390 }
3391
3392 #[test]
3393 fn apply_conditional_get_no_match_200() {
3394 use crate::request::Method;
3395
3396 let headers = vec![("If-None-Match".to_string(), b"\"old\"".to_vec())];
3397 let response = Response::ok().with_etag("\"new\"");
3398 let result = apply_conditional(&headers, Method::Get, response);
3399 assert_eq!(result.status().as_u16(), 200);
3400 }
3401
3402 #[test]
3403 fn apply_conditional_put_412() {
3404 use crate::request::Method;
3405
3406 let headers = vec![("If-Match".to_string(), b"\"old\"".to_vec())];
3407 let response = Response::ok().with_etag("\"new\"");
3408 let result = apply_conditional(&headers, Method::Put, response);
3409 assert_eq!(result.status().as_u16(), 412);
3410 }
3411
3412 #[test]
3413 fn apply_conditional_put_match_200() {
3414 use crate::request::Method;
3415
3416 let headers = vec![("If-Match".to_string(), b"\"current\"".to_vec())];
3417 let response = Response::ok().with_etag("\"current\"");
3418 let result = apply_conditional(&headers, Method::Put, response);
3419 assert_eq!(result.status().as_u16(), 200);
3420 }
3421
3422 #[test]
3423 fn apply_conditional_no_etag_passthrough() {
3424 use crate::request::Method;
3425
3426 let headers = vec![("If-None-Match".to_string(), b"\"abc\"".to_vec())];
3427 let response = Response::ok(); let result = apply_conditional(&headers, Method::Get, response);
3429 assert_eq!(result.status().as_u16(), 200);
3430 }
3431
3432 #[test]
3437 fn link_header_single() {
3438 let h = LinkHeader::new().link("https://example.com/next", LinkRel::Next);
3439 assert_eq!(h.to_string(), r#"<https://example.com/next>; rel="next""#);
3440 }
3441
3442 #[test]
3443 fn link_header_multiple() {
3444 let h = LinkHeader::new()
3445 .link("/page/2", LinkRel::Next)
3446 .link("/page/0", LinkRel::Prev);
3447 let s = h.to_string();
3448 assert!(s.contains(r#"</page/2>; rel="next""#));
3449 assert!(s.contains(r#"</page/0>; rel="prev""#));
3450 assert!(s.contains(", "));
3451 }
3452
3453 #[test]
3454 fn link_with_title_and_type() {
3455 let link = Link::new("https://api.example.com", LinkRel::Related)
3456 .title("API Docs")
3457 .media_type("text/html");
3458 let s = link.to_string();
3459 assert!(s.contains(r#"title="API Docs""#));
3460 assert!(s.contains(r#"type="text/html""#));
3461 }
3462
3463 #[test]
3464 fn link_header_custom_rel() {
3465 let h = LinkHeader::new().link("/schema", LinkRel::Custom("describedby".to_string()));
3466 assert!(h.to_string().contains(r#"rel="describedby""#));
3467 }
3468
3469 #[test]
3470 fn link_header_paginate_first_page() {
3471 let h = LinkHeader::new().paginate("/users", 1, 10, 50);
3472 let s = h.to_string();
3473 assert!(s.contains(r#"rel="self""#));
3474 assert!(s.contains(r#"rel="first""#));
3475 assert!(s.contains(r#"rel="last""#));
3476 assert!(s.contains(r#"rel="next""#));
3477 assert!(!s.contains(r#"rel="prev""#)); assert!(s.contains("page=5")); }
3480
3481 #[test]
3482 fn link_header_paginate_middle_page() {
3483 let h = LinkHeader::new().paginate("/users", 3, 10, 50);
3484 let s = h.to_string();
3485 assert!(s.contains(r#"rel="prev""#));
3486 assert!(s.contains(r#"rel="next""#));
3487 assert!(s.contains("page=2")); assert!(s.contains("page=4")); }
3490
3491 #[test]
3492 fn link_header_paginate_last_page() {
3493 let h = LinkHeader::new().paginate("/users", 5, 10, 50);
3494 let s = h.to_string();
3495 assert!(s.contains(r#"rel="prev""#));
3496 assert!(!s.contains(r#"rel="next""#)); }
3498
3499 #[test]
3500 fn link_header_paginate_with_existing_query() {
3501 let h = LinkHeader::new().paginate("/users?sort=name", 1, 10, 20);
3502 let s = h.to_string();
3503 assert!(s.contains("sort=name&page="));
3504 }
3505
3506 #[test]
3507 fn link_header_empty() {
3508 let h = LinkHeader::new();
3509 assert!(h.is_empty());
3510 assert_eq!(h.len(), 0);
3511 assert_eq!(h.to_string(), "");
3512 }
3513
3514 #[test]
3515 fn link_header_apply_to_response() {
3516 let h = LinkHeader::new().link("/next", LinkRel::Next);
3517 let response = h.apply(Response::ok());
3518 let link_hdr = response
3519 .headers()
3520 .iter()
3521 .find(|(n, _)| n == "link")
3522 .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string());
3523 assert!(link_hdr.unwrap().contains("rel=\"next\""));
3524 }
3525
3526 #[test]
3527 fn link_header_apply_empty_noop() {
3528 let h = LinkHeader::new();
3529 let response = h.apply(Response::ok());
3530 let has_link = response.headers().iter().any(|(n, _)| n == "link");
3531 assert!(!has_link);
3532 }
3533
3534 #[test]
3535 fn link_rel_display() {
3536 assert_eq!(LinkRel::Self_.to_string(), "self");
3537 assert_eq!(LinkRel::Next.to_string(), "next");
3538 assert_eq!(LinkRel::Prev.to_string(), "prev");
3539 assert_eq!(LinkRel::First.to_string(), "first");
3540 assert_eq!(LinkRel::Last.to_string(), "last");
3541 assert_eq!(LinkRel::Related.to_string(), "related");
3542 assert_eq!(LinkRel::Alternate.to_string(), "alternate");
3543 }
3544}