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
712pub trait ResponseProduces<T> {}
749
750impl<T> ResponseProduces<T> for T {}
752
753impl<T: serde::Serialize + 'static> ResponseProduces<T> for crate::extract::Json<T> {}
755
756#[derive(Debug, Clone)]
779pub struct Redirect {
780 status: StatusCode,
781 location: String,
782}
783
784impl Redirect {
785 #[must_use]
789 pub fn temporary(location: impl Into<String>) -> Self {
790 Self {
791 status: StatusCode::TEMPORARY_REDIRECT,
792 location: location.into(),
793 }
794 }
795
796 #[must_use]
801 pub fn permanent(location: impl Into<String>) -> Self {
802 Self {
803 status: StatusCode::PERMANENT_REDIRECT,
804 location: location.into(),
805 }
806 }
807
808 #[must_use]
813 pub fn see_other(location: impl Into<String>) -> Self {
814 Self {
815 status: StatusCode::SEE_OTHER,
816 location: location.into(),
817 }
818 }
819
820 #[must_use]
824 pub fn moved_permanently(location: impl Into<String>) -> Self {
825 Self {
826 status: StatusCode::MOVED_PERMANENTLY,
827 location: location.into(),
828 }
829 }
830
831 #[must_use]
835 pub fn found(location: impl Into<String>) -> Self {
836 Self {
837 status: StatusCode::FOUND,
838 location: location.into(),
839 }
840 }
841
842 #[must_use]
844 pub fn location(&self) -> &str {
845 &self.location
846 }
847
848 #[must_use]
850 pub fn status(&self) -> StatusCode {
851 self.status
852 }
853}
854
855impl IntoResponse for Redirect {
856 fn into_response(self) -> Response {
857 Response::with_status(self.status).header("location", self.location.into_bytes())
858 }
859}
860
861#[derive(Debug, Clone)]
871pub struct Html(String);
872
873impl Html {
874 #[must_use]
881 pub fn new(content: impl Into<String>) -> Self {
882 Self(content.into())
883 }
884
885 #[must_use]
890 pub fn escaped(content: impl AsRef<str>) -> Self {
891 Self(escape_html(content.as_ref()))
892 }
893
894 #[must_use]
896 pub fn content(&self) -> &str {
897 &self.0
898 }
899}
900
901fn escape_html(s: &str) -> String {
903 let mut out = String::with_capacity(s.len());
904 for c in s.chars() {
905 match c {
906 '&' => out.push_str("&"),
907 '<' => out.push_str("<"),
908 '>' => out.push_str(">"),
909 '"' => out.push_str("""),
910 '\'' => out.push_str("'"),
911 _ => out.push(c),
912 }
913 }
914 out
915}
916
917impl IntoResponse for Html {
918 fn into_response(self) -> Response {
919 Response::ok()
920 .header("content-type", b"text/html; charset=utf-8".to_vec())
921 .body(ResponseBody::Bytes(self.0.into_bytes()))
922 }
923}
924
925impl<S: Into<String>> From<S> for Html {
926 fn from(s: S) -> Self {
927 Self::new(s)
928 }
929}
930
931#[derive(Debug, Clone)]
944pub struct Text(String);
945
946impl Text {
947 #[must_use]
949 pub fn new(content: impl Into<String>) -> Self {
950 Self(content.into())
951 }
952
953 #[must_use]
955 pub fn content(&self) -> &str {
956 &self.0
957 }
958}
959
960impl IntoResponse for Text {
961 fn into_response(self) -> Response {
962 Response::ok()
963 .header("content-type", b"text/plain; charset=utf-8".to_vec())
964 .body(ResponseBody::Bytes(self.0.into_bytes()))
965 }
966}
967
968impl<S: Into<String>> From<S> for Text {
969 fn from(s: S) -> Self {
970 Self::new(s)
971 }
972}
973
974#[derive(Debug, Clone, Copy, Default)]
988pub struct NoContent;
989
990impl IntoResponse for NoContent {
991 fn into_response(self) -> Response {
992 Response::no_content()
993 }
994}
995
996#[derive(Debug, Clone)]
1009pub struct Binary(Vec<u8>);
1010
1011impl Binary {
1012 #[must_use]
1014 pub fn new(data: impl Into<Vec<u8>>) -> Self {
1015 Self(data.into())
1016 }
1017
1018 #[must_use]
1020 pub fn data(&self) -> &[u8] {
1021 &self.0
1022 }
1023
1024 #[must_use]
1026 pub fn with_content_type(self, content_type: &str) -> BinaryWithType {
1027 BinaryWithType {
1028 data: self.0,
1029 content_type: content_type.to_string(),
1030 }
1031 }
1032}
1033
1034impl IntoResponse for Binary {
1035 fn into_response(self) -> Response {
1036 Response::ok()
1037 .header("content-type", b"application/octet-stream".to_vec())
1038 .body(ResponseBody::Bytes(self.0))
1039 }
1040}
1041
1042impl From<Vec<u8>> for Binary {
1043 fn from(data: Vec<u8>) -> Self {
1044 Self::new(data)
1045 }
1046}
1047
1048impl From<&[u8]> for Binary {
1049 fn from(data: &[u8]) -> Self {
1050 Self::new(data.to_vec())
1051 }
1052}
1053
1054#[derive(Debug, Clone)]
1065pub struct BinaryWithType {
1066 data: Vec<u8>,
1067 content_type: String,
1068}
1069
1070impl BinaryWithType {
1071 pub fn data(&self) -> &[u8] {
1073 &self.data
1074 }
1075
1076 pub fn content_type(&self) -> &str {
1078 &self.content_type
1079 }
1080}
1081
1082impl IntoResponse for BinaryWithType {
1083 fn into_response(self) -> Response {
1084 Response::ok()
1085 .header("content-type", self.content_type.into_bytes())
1086 .body(ResponseBody::Bytes(self.data))
1087 }
1088}
1089
1090#[derive(Debug)]
1111pub struct FileResponse {
1112 path: std::path::PathBuf,
1113 content_type: Option<String>,
1114 download_name: Option<String>,
1115 inline: bool,
1116}
1117
1118impl FileResponse {
1119 #[must_use]
1123 pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
1124 Self {
1125 path: path.into(),
1126 content_type: None,
1127 download_name: None,
1128 inline: true,
1129 }
1130 }
1131
1132 #[must_use]
1134 pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
1135 self.content_type = Some(content_type.into());
1136 self
1137 }
1138
1139 #[must_use]
1143 pub fn download_as(mut self, filename: impl Into<String>) -> Self {
1144 self.download_name = Some(filename.into());
1145 self.inline = false;
1146 self
1147 }
1148
1149 #[must_use]
1153 pub fn inline(mut self) -> Self {
1154 self.inline = true;
1155 self.download_name = None;
1156 self
1157 }
1158
1159 #[must_use]
1161 pub fn path(&self) -> &std::path::Path {
1162 &self.path
1163 }
1164
1165 fn infer_content_type(&self) -> &'static str {
1167 self.path
1168 .extension()
1169 .and_then(|ext| ext.to_str())
1170 .map(|ext| mime_type_for_extension(ext))
1171 .unwrap_or("application/octet-stream")
1172 }
1173
1174 fn content_disposition(&self) -> String {
1176 if self.inline {
1177 "inline".to_string()
1178 } else if let Some(ref name) = self.download_name {
1179 format!("attachment; filename=\"{}\"", name.replace('"', "\\\""))
1181 } else {
1182 let filename = self
1184 .path
1185 .file_name()
1186 .and_then(|n| n.to_str())
1187 .unwrap_or("download");
1188 format!("attachment; filename=\"{}\"", filename.replace('"', "\\\""))
1189 }
1190 }
1191
1192 #[must_use]
1198 pub fn into_response_sync(self) -> Response {
1199 match std::fs::read(&self.path) {
1200 Ok(contents) => {
1201 let content_type = self
1202 .content_type
1203 .as_deref()
1204 .unwrap_or_else(|| self.infer_content_type());
1205
1206 Response::ok()
1207 .header("content-type", content_type.as_bytes().to_vec())
1208 .header(
1209 "content-disposition",
1210 self.content_disposition().into_bytes(),
1211 )
1212 .header("accept-ranges", b"bytes".to_vec())
1213 .body(ResponseBody::Bytes(contents))
1214 }
1215 Err(_) => Response::with_status(StatusCode::NOT_FOUND),
1216 }
1217 }
1218}
1219
1220impl IntoResponse for FileResponse {
1221 fn into_response(self) -> Response {
1222 self.into_response_sync()
1223 }
1224}
1225
1226#[must_use]
1231pub fn mime_type_for_extension(ext: &str) -> &'static str {
1232 match ext.to_ascii_lowercase().as_str() {
1233 "html" | "htm" => "text/html; charset=utf-8",
1235 "css" => "text/css; charset=utf-8",
1236 "js" | "mjs" => "text/javascript; charset=utf-8",
1237 "json" | "map" => "application/json",
1238 "xml" => "application/xml",
1239 "txt" => "text/plain; charset=utf-8",
1240 "csv" => "text/csv; charset=utf-8",
1241 "md" => "text/markdown; charset=utf-8",
1242
1243 "png" => "image/png",
1245 "jpg" | "jpeg" => "image/jpeg",
1246 "gif" => "image/gif",
1247 "webp" => "image/webp",
1248 "svg" => "image/svg+xml",
1249 "ico" => "image/x-icon",
1250 "bmp" => "image/bmp",
1251 "avif" => "image/avif",
1252
1253 "woff" => "font/woff",
1255 "woff2" => "font/woff2",
1256 "ttf" => "font/ttf",
1257 "otf" => "font/otf",
1258 "eot" => "application/vnd.ms-fontobject",
1259
1260 "mp3" => "audio/mpeg",
1262 "wav" => "audio/wav",
1263 "ogg" => "audio/ogg",
1264 "flac" => "audio/flac",
1265 "aac" => "audio/aac",
1266 "m4a" => "audio/mp4",
1267
1268 "mp4" => "video/mp4",
1270 "webm" => "video/webm",
1271 "avi" => "video/x-msvideo",
1272 "mov" => "video/quicktime",
1273 "mkv" => "video/x-matroska",
1274
1275 "pdf" => "application/pdf",
1277 "doc" => "application/msword",
1278 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1279 "xls" => "application/vnd.ms-excel",
1280 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1281 "ppt" => "application/vnd.ms-powerpoint",
1282 "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1283
1284 "zip" => "application/zip",
1286 "gz" | "gzip" => "application/gzip",
1287 "tar" => "application/x-tar",
1288 "rar" => "application/vnd.rar",
1289 "7z" => "application/x-7z-compressed",
1290
1291 "wasm" => "application/wasm",
1293
1294 _ => "application/octet-stream",
1295 }
1296}
1297
1298#[must_use]
1304#[allow(dead_code)] pub fn outcome_to_response<T, E>(outcome: Outcome<T, E>) -> Response
1306where
1307 T: IntoResponse,
1308 E: IntoResponse,
1309{
1310 match outcome {
1311 Outcome::Ok(value) => value.into_response(),
1312 Outcome::Err(err) => err.into_response(),
1313 Outcome::Cancelled(reason) => cancelled_to_response(&reason),
1314 Outcome::Panicked(_payload) => Response::with_status(StatusCode::INTERNAL_SERVER_ERROR),
1315 }
1316}
1317
1318#[allow(dead_code)] fn cancelled_to_response(reason: &CancelReason) -> Response {
1320 let status = match reason.kind() {
1321 CancelKind::Timeout => StatusCode::GATEWAY_TIMEOUT,
1322 CancelKind::Shutdown => StatusCode::SERVICE_UNAVAILABLE,
1323 _ => StatusCode::CLIENT_CLOSED_REQUEST,
1324 };
1325 Response::with_status(status)
1326}
1327
1328#[derive(Debug, Clone, Default)]
1356#[allow(clippy::struct_excessive_bools)] pub struct ResponseModelConfig {
1358 pub include: Option<std::collections::HashSet<String>>,
1361
1362 pub exclude: Option<std::collections::HashSet<String>>,
1364
1365 pub by_alias: bool,
1367
1368 pub exclude_unset: bool,
1371
1372 pub exclude_defaults: bool,
1374
1375 pub exclude_none: bool,
1377
1378 aliases: Option<&'static [(&'static str, &'static str)]>,
1388
1389 defaults_json: Option<fn() -> Result<serde_json::Value, String>>,
1395
1396 set_fields: Option<std::collections::HashSet<String>>,
1400}
1401
1402pub trait ResponseModelAliases {
1406 fn response_model_aliases() -> &'static [(&'static str, &'static str)];
1411}
1412
1413impl ResponseModelConfig {
1414 #[must_use]
1416 pub fn new() -> Self {
1417 Self::default()
1418 }
1419
1420 #[must_use]
1422 pub fn include(mut self, fields: std::collections::HashSet<String>) -> Self {
1423 self.include = Some(fields);
1424 self
1425 }
1426
1427 #[must_use]
1429 pub fn exclude(mut self, fields: std::collections::HashSet<String>) -> Self {
1430 self.exclude = Some(fields);
1431 self
1432 }
1433
1434 #[must_use]
1439 pub fn by_alias(mut self, value: bool) -> Self {
1440 self.by_alias = value;
1441 self
1442 }
1443
1444 #[must_use]
1449 pub fn exclude_unset(mut self, value: bool) -> Self {
1450 self.exclude_unset = value;
1451 self
1452 }
1453
1454 #[must_use]
1460 pub fn exclude_defaults(mut self, value: bool) -> Self {
1461 self.exclude_defaults = value;
1462 self
1463 }
1464
1465 #[must_use]
1467 pub fn exclude_none(mut self, value: bool) -> Self {
1468 self.exclude_none = value;
1469 self
1470 }
1471
1472 #[must_use]
1474 pub fn with_aliases(mut self, aliases: &'static [(&'static str, &'static str)]) -> Self {
1475 self.aliases = Some(aliases);
1476 self
1477 }
1478
1479 #[must_use]
1481 pub fn with_aliases_from<T: ResponseModelAliases>(mut self) -> Self {
1482 self.aliases = Some(T::response_model_aliases());
1483 self
1484 }
1485
1486 #[must_use]
1488 pub fn with_defaults_json_provider(
1489 mut self,
1490 provider: fn() -> Result<serde_json::Value, String>,
1491 ) -> Self {
1492 self.defaults_json = Some(provider);
1493 self
1494 }
1495
1496 fn defaults_json_for<T: Default + Serialize>() -> Result<serde_json::Value, String> {
1497 serde_json::to_value(T::default()).map_err(|e| e.to_string())
1498 }
1499
1500 #[must_use]
1502 pub fn with_defaults_from<T: Default + Serialize>(mut self) -> Self {
1503 self.defaults_json = Some(Self::defaults_json_for::<T>);
1504 self
1505 }
1506
1507 #[must_use]
1509 pub fn with_set_fields(mut self, fields: std::collections::HashSet<String>) -> Self {
1510 self.set_fields = Some(fields);
1511 self
1512 }
1513
1514 #[must_use]
1516 pub fn has_filtering(&self) -> bool {
1517 self.include.is_some()
1518 || self.exclude.is_some()
1519 || self.exclude_none
1520 || self.exclude_unset
1521 || self.exclude_defaults
1522 || self.by_alias
1523 }
1524
1525 #[allow(clippy::result_large_err)]
1532 pub fn filter_json(
1533 &self,
1534 value: serde_json::Value,
1535 ) -> Result<serde_json::Value, crate::error::ResponseValidationError> {
1536 let serde_json::Value::Object(mut map) = value else {
1537 return Ok(value);
1538 };
1539
1540 if let Some(aliases) = self.aliases {
1543 normalize_to_canonical(&mut map, aliases)?;
1544 }
1545
1546 if self.exclude_unset {
1548 let set_fields = self.set_fields.as_ref().ok_or_else(|| {
1549 crate::error::ResponseValidationError::serialization_failed(
1550 "response_model_exclude_unset requires set-fields metadata \
1551 (use ResponseModelConfig::with_set_fields)",
1552 )
1553 })?;
1554 map.retain(|k, _| set_fields.contains(k));
1555 }
1556
1557 if let Some(ref include_set) = self.include {
1559 map.retain(|key, _| include_set.contains(key));
1560 }
1561
1562 if let Some(ref exclude_set) = self.exclude {
1564 map.retain(|key, _| !exclude_set.contains(key));
1565 }
1566
1567 if self.exclude_none {
1569 map.retain(|_, v| !v.is_null());
1570 }
1571
1572 if self.exclude_defaults {
1574 let provider = self.defaults_json.ok_or_else(|| {
1575 crate::error::ResponseValidationError::serialization_failed(
1576 "response_model_exclude_defaults requires defaults metadata \
1577 (use ResponseModelConfig::with_defaults_from::<T>() or \
1578 ResponseModelConfig::with_defaults_json_provider)",
1579 )
1580 })?;
1581 let defaults =
1582 provider().map_err(crate::error::ResponseValidationError::serialization_failed)?;
1583 let serde_json::Value::Object(defaults_map) = defaults else {
1584 return Err(crate::error::ResponseValidationError::serialization_failed(
1585 "defaults provider did not return a JSON object",
1586 ));
1587 };
1588
1589 for (k, default_v) in defaults_map {
1591 if map.get(&k).is_some_and(|v| v == &default_v) {
1592 map.remove(&k);
1593 }
1594 }
1595 }
1596
1597 if self.by_alias {
1599 let aliases = self.aliases.ok_or_else(|| {
1600 crate::error::ResponseValidationError::serialization_failed(
1601 "response_model_by_alias requires alias metadata \
1602 (use ResponseModelConfig::with_aliases(...) or \
1603 ResponseModelConfig::with_aliases_from::<T>())",
1604 )
1605 })?;
1606 apply_aliases(&mut map, aliases)?;
1607 }
1608
1609 Ok(serde_json::Value::Object(map))
1610 }
1611}
1612
1613#[allow(clippy::result_large_err)]
1614fn normalize_to_canonical(
1615 map: &mut serde_json::Map<String, serde_json::Value>,
1616 aliases: &[(&'static str, &'static str)],
1617) -> Result<(), crate::error::ResponseValidationError> {
1618 for (canonical, alias) in aliases {
1619 if canonical == alias {
1620 continue;
1621 }
1622 let canonical = *canonical;
1623 let alias = *alias;
1624
1625 if map.contains_key(canonical) && map.contains_key(alias) {
1626 return Err(crate::error::ResponseValidationError::serialization_failed(
1628 format!(
1629 "response model contains both canonical field '{canonical}' and alias '{alias}'"
1630 ),
1631 ));
1632 }
1633
1634 if let Some(v) = map.remove(alias) {
1635 map.insert(canonical.to_string(), v);
1636 }
1637 }
1638 Ok(())
1639}
1640
1641#[allow(clippy::result_large_err)]
1642fn apply_aliases(
1643 map: &mut serde_json::Map<String, serde_json::Value>,
1644 aliases: &[(&'static str, &'static str)],
1645) -> Result<(), crate::error::ResponseValidationError> {
1646 for (canonical, alias) in aliases {
1647 if canonical == alias {
1648 continue;
1649 }
1650 let canonical = *canonical;
1651 let alias = *alias;
1652
1653 if map.contains_key(canonical) && map.contains_key(alias) {
1654 return Err(crate::error::ResponseValidationError::serialization_failed(
1655 format!(
1656 "response model contains both canonical field '{canonical}' and alias '{alias}'"
1657 ),
1658 ));
1659 }
1660
1661 if let Some(v) = map.remove(canonical) {
1662 map.insert(alias.to_string(), v);
1663 }
1664 }
1665 Ok(())
1666}
1667
1668pub trait ResponseModel: Serialize {
1674 #[allow(clippy::result_large_err)] fn validate(&self) -> Result<(), crate::error::ResponseValidationError> {
1679 Ok(())
1681 }
1682
1683 fn model_name() -> &'static str {
1685 std::any::type_name::<Self>()
1686 }
1687}
1688
1689impl<T: Serialize> ResponseModel for T {}
1691
1692#[derive(Debug)]
1724pub struct ValidatedResponse<T> {
1725 pub value: T,
1727 pub config: ResponseModelConfig,
1729}
1730
1731impl<T> ValidatedResponse<T> {
1732 #[must_use]
1734 pub fn new(value: T) -> Self {
1735 Self {
1736 value,
1737 config: ResponseModelConfig::default(),
1738 }
1739 }
1740
1741 #[must_use]
1743 pub fn with_config(mut self, config: ResponseModelConfig) -> Self {
1744 self.config = config;
1745 self
1746 }
1747}
1748
1749impl<T: Serialize + ResponseModel> IntoResponse for ValidatedResponse<T> {
1750 fn into_response(self) -> Response {
1751 if let Err(error) = self.value.validate() {
1753 return error.into_response();
1754 }
1755
1756 let json_value = match serde_json::to_value(&self.value) {
1758 Ok(v) => v,
1759 Err(e) => {
1760 let error =
1762 crate::error::ResponseValidationError::serialization_failed(e.to_string());
1763 return error.into_response();
1764 }
1765 };
1766
1767 let filtered = match self.config.filter_json(json_value) {
1769 Ok(v) => v,
1770 Err(e) => return e.into_response(),
1771 };
1772
1773 let bytes = match serde_json::to_vec(&filtered) {
1775 Ok(b) => b,
1776 Err(e) => {
1777 let error =
1778 crate::error::ResponseValidationError::serialization_failed(e.to_string());
1779 return error.into_response();
1780 }
1781 };
1782
1783 Response::ok()
1784 .header("content-type", b"application/json".to_vec())
1785 .body(ResponseBody::Bytes(bytes))
1786 }
1787}
1788
1789#[must_use]
1793pub fn exclude_fields<T: Serialize + ResponseModel>(
1794 value: T,
1795 fields: &[&str],
1796) -> ValidatedResponse<T> {
1797 ValidatedResponse::new(value).with_config(
1798 ResponseModelConfig::new().exclude(fields.iter().map(|s| (*s).to_string()).collect()),
1799 )
1800}
1801
1802#[must_use]
1806pub fn include_fields<T: Serialize + ResponseModel>(
1807 value: T,
1808 fields: &[&str],
1809) -> ValidatedResponse<T> {
1810 ValidatedResponse::new(value).with_config(
1811 ResponseModelConfig::new().include(fields.iter().map(|s| (*s).to_string()).collect()),
1812 )
1813}
1814
1815pub fn check_if_none_match(if_none_match: &str, current_etag: &str) -> bool {
1841 let if_none_match = if_none_match.trim();
1842
1843 if if_none_match == "*" {
1845 return false; }
1847
1848 let current_stripped = strip_weak_prefix(current_etag.trim());
1849
1850 for candidate in if_none_match.split(',') {
1852 let candidate = strip_weak_prefix(candidate.trim());
1853 if candidate == current_stripped {
1854 return false; }
1856 }
1857
1858 true }
1860
1861pub fn check_if_match(if_match: &str, current_etag: &str) -> bool {
1869 let if_match = if_match.trim();
1870
1871 if if_match == "*" {
1873 return true;
1874 }
1875
1876 let current = current_etag.trim();
1877
1878 if current.starts_with("W/") {
1880 return false;
1881 }
1882
1883 for candidate in if_match.split(',') {
1884 let candidate = candidate.trim();
1885 if candidate.starts_with("W/") {
1887 continue;
1888 }
1889 if candidate == current {
1890 return true;
1891 }
1892 }
1893
1894 false
1895}
1896
1897fn strip_weak_prefix(etag: &str) -> &str {
1899 etag.strip_prefix("W/").unwrap_or(etag)
1900}
1901
1902pub fn apply_conditional(
1918 request_headers: &[(String, Vec<u8>)],
1919 method: crate::request::Method,
1920 response: Response,
1921) -> Response {
1922 let response_etag = response
1924 .headers()
1925 .iter()
1926 .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
1927 .and_then(|(_, value)| std::str::from_utf8(value).ok())
1928 .map(String::from);
1929
1930 let Some(response_etag) = response_etag else {
1931 return response; };
1933
1934 if matches!(
1936 method,
1937 crate::request::Method::Get | crate::request::Method::Head
1938 ) {
1939 if let Some(if_none_match) = find_header(request_headers, "if-none-match") {
1940 if !check_if_none_match(&if_none_match, &response_etag) {
1941 return Response::not_modified().with_etag(response_etag);
1942 }
1943 }
1944 }
1945
1946 if matches!(
1948 method,
1949 crate::request::Method::Put
1950 | crate::request::Method::Patch
1951 | crate::request::Method::Delete
1952 ) {
1953 if let Some(if_match) = find_header(request_headers, "if-match") {
1954 if !check_if_match(&if_match, &response_etag) {
1955 return Response::precondition_failed();
1956 }
1957 }
1958 }
1959
1960 response
1961}
1962
1963fn find_header(headers: &[(String, Vec<u8>)], name: &str) -> Option<String> {
1965 headers
1966 .iter()
1967 .find(|(n, _)| n.eq_ignore_ascii_case(name))
1968 .and_then(|(_, v)| std::str::from_utf8(v).ok())
1969 .map(String::from)
1970}
1971
1972#[derive(Debug, Clone, PartialEq, Eq)]
1978pub enum LinkRel {
1979 Self_,
1981 Next,
1983 Prev,
1985 First,
1987 Last,
1989 Related,
1991 Alternate,
1993 Custom(String),
1995}
1996
1997impl fmt::Display for LinkRel {
1998 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1999 match self {
2000 Self::Self_ => write!(f, "self"),
2001 Self::Next => write!(f, "next"),
2002 Self::Prev => write!(f, "prev"),
2003 Self::First => write!(f, "first"),
2004 Self::Last => write!(f, "last"),
2005 Self::Related => write!(f, "related"),
2006 Self::Alternate => write!(f, "alternate"),
2007 Self::Custom(s) => write!(f, "{s}"),
2008 }
2009 }
2010}
2011
2012#[derive(Debug, Clone)]
2014pub struct Link {
2015 url: String,
2016 rel: LinkRel,
2017 title: Option<String>,
2018 media_type: Option<String>,
2019}
2020
2021impl Link {
2022 pub fn new(url: impl Into<String>, rel: LinkRel) -> Self {
2024 Self {
2025 url: url.into(),
2026 rel,
2027 title: None,
2028 media_type: None,
2029 }
2030 }
2031
2032 #[must_use]
2034 pub fn title(mut self, title: impl Into<String>) -> Self {
2035 self.title = Some(title.into());
2036 self
2037 }
2038
2039 #[must_use]
2041 pub fn media_type(mut self, media_type: impl Into<String>) -> Self {
2042 self.media_type = Some(media_type.into());
2043 self
2044 }
2045}
2046
2047impl fmt::Display for Link {
2048 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2049 write!(f, "<{}>; rel=\"{}\"", self.url, self.rel)?;
2050 if let Some(ref title) = self.title {
2051 write!(f, "; title=\"{title}\"")?;
2052 }
2053 if let Some(ref mt) = self.media_type {
2054 write!(f, "; type=\"{mt}\"")?;
2055 }
2056 Ok(())
2057 }
2058}
2059
2060#[derive(Debug, Clone, Default)]
2079pub struct LinkHeader {
2080 links: Vec<Link>,
2081}
2082
2083impl LinkHeader {
2084 #[must_use]
2086 pub fn new() -> Self {
2087 Self::default()
2088 }
2089
2090 #[must_use]
2092 pub fn link(mut self, url: impl Into<String>, rel: LinkRel) -> Self {
2093 self.links.push(Link::new(url, rel));
2094 self
2095 }
2096
2097 #[must_use]
2099 #[allow(clippy::should_implement_trait)]
2100 pub fn add(mut self, link: Link) -> Self {
2101 self.links.push(link);
2102 self
2103 }
2104
2105 #[must_use]
2110 pub fn paginate(self, base_url: &str, page: u64, per_page: u64, total: u64) -> Self {
2111 let last_page = if total == 0 {
2112 1
2113 } else {
2114 total.div_ceil(per_page)
2115 };
2116 let sep = if base_url.contains('?') { '&' } else { '?' };
2117
2118 let mut h = self.link(
2119 format!("{base_url}{sep}page={page}&per_page={per_page}"),
2120 LinkRel::Self_,
2121 );
2122 h = h.link(
2123 format!("{base_url}{sep}page=1&per_page={per_page}"),
2124 LinkRel::First,
2125 );
2126 h = h.link(
2127 format!("{base_url}{sep}page={last_page}&per_page={per_page}"),
2128 LinkRel::Last,
2129 );
2130 if page > 1 {
2131 h = h.link(
2132 format!("{base_url}{sep}page={}&per_page={per_page}", page - 1),
2133 LinkRel::Prev,
2134 );
2135 }
2136 if page < last_page {
2137 h = h.link(
2138 format!("{base_url}{sep}page={}&per_page={per_page}", page + 1),
2139 LinkRel::Next,
2140 );
2141 }
2142 h
2143 }
2144
2145 #[must_use]
2147 pub fn is_empty(&self) -> bool {
2148 self.links.is_empty()
2149 }
2150
2151 #[must_use]
2153 pub fn len(&self) -> usize {
2154 self.links.len()
2155 }
2156
2157 #[must_use]
2159 pub fn to_header_value(&self) -> String {
2160 self.to_string()
2161 }
2162
2163 pub fn apply(self, response: Response) -> Response {
2165 if self.is_empty() {
2166 return response;
2167 }
2168 response.header("link", self.to_string().into_bytes())
2169 }
2170}
2171
2172impl fmt::Display for LinkHeader {
2173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2174 for (i, link) in self.links.iter().enumerate() {
2175 if i > 0 {
2176 write!(f, ", ")?;
2177 }
2178 write!(f, "{link}")?;
2179 }
2180 Ok(())
2181 }
2182}
2183
2184#[cfg(test)]
2185mod tests {
2186 use super::*;
2187 use crate::error::HttpError;
2188
2189 #[test]
2190 fn response_remove_header_removes_all_instances_case_insensitive() {
2191 let resp = Response::ok()
2192 .header("X-Test", b"1".to_vec())
2193 .header("x-test", b"2".to_vec())
2194 .header("Other", b"3".to_vec())
2195 .remove_header("X-Test");
2196
2197 assert!(
2198 resp.headers()
2199 .iter()
2200 .all(|(n, _)| !n.eq_ignore_ascii_case("x-test"))
2201 );
2202 assert!(
2203 resp.headers()
2204 .iter()
2205 .any(|(n, _)| n.eq_ignore_ascii_case("other"))
2206 );
2207 }
2208
2209 #[test]
2210 fn outcome_ok_maps_to_response() {
2211 let response = Response::created();
2212 let mapped = outcome_to_response::<Response, HttpError>(Outcome::Ok(response));
2213 assert_eq!(mapped.status().as_u16(), 201);
2214 }
2215
2216 #[test]
2217 fn outcome_err_maps_to_response() {
2218 let mapped =
2219 outcome_to_response::<Response, HttpError>(Outcome::Err(HttpError::bad_request()));
2220 assert_eq!(mapped.status().as_u16(), 400);
2221 }
2222
2223 #[test]
2224 fn outcome_cancelled_timeout_maps_to_504() {
2225 let mapped =
2226 outcome_to_response::<Response, HttpError>(Outcome::Cancelled(CancelReason::timeout()));
2227 assert_eq!(mapped.status().as_u16(), 504);
2228 }
2229
2230 #[test]
2231 fn outcome_cancelled_user_maps_to_499() {
2232 let mapped = outcome_to_response::<Response, HttpError>(Outcome::Cancelled(
2233 CancelReason::user("client disconnected"),
2234 ));
2235 assert_eq!(mapped.status().as_u16(), 499);
2236 }
2237
2238 #[test]
2239 fn outcome_panicked_maps_to_500() {
2240 let mapped = outcome_to_response::<Response, HttpError>(Outcome::Panicked(
2241 PanicPayload::new("boom"),
2242 ));
2243 assert_eq!(mapped.status().as_u16(), 500);
2244 }
2245
2246 #[test]
2251 fn redirect_temporary_returns_307() {
2252 let redirect = Redirect::temporary("/new-location");
2253 let response = redirect.into_response();
2254 assert_eq!(response.status().as_u16(), 307);
2255 }
2256
2257 #[test]
2258 fn redirect_permanent_returns_308() {
2259 let redirect = Redirect::permanent("/moved");
2260 let response = redirect.into_response();
2261 assert_eq!(response.status().as_u16(), 308);
2262 }
2263
2264 #[test]
2265 fn redirect_see_other_returns_303() {
2266 let redirect = Redirect::see_other("/result");
2267 let response = redirect.into_response();
2268 assert_eq!(response.status().as_u16(), 303);
2269 }
2270
2271 #[test]
2272 fn redirect_moved_permanently_returns_301() {
2273 let redirect = Redirect::moved_permanently("/gone");
2274 let response = redirect.into_response();
2275 assert_eq!(response.status().as_u16(), 301);
2276 }
2277
2278 #[test]
2279 fn redirect_found_returns_302() {
2280 let redirect = Redirect::found("/elsewhere");
2281 let response = redirect.into_response();
2282 assert_eq!(response.status().as_u16(), 302);
2283 }
2284
2285 #[test]
2286 fn redirect_sets_location_header() {
2287 let redirect = Redirect::temporary("/target?query=1");
2288 let response = redirect.into_response();
2289
2290 let location = response
2291 .headers()
2292 .iter()
2293 .find(|(name, _)| name == "location")
2294 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2295
2296 assert_eq!(location, Some("/target?query=1".to_string()));
2297 }
2298
2299 #[test]
2300 fn redirect_location_accessor() {
2301 let redirect = Redirect::permanent("https://example.com/new");
2302 assert_eq!(redirect.location(), "https://example.com/new");
2303 }
2304
2305 #[test]
2306 fn redirect_status_accessor() {
2307 let redirect = Redirect::see_other("/done");
2308 assert_eq!(redirect.status().as_u16(), 303);
2309 }
2310
2311 #[test]
2316 fn html_response_has_correct_content_type() {
2317 let html = Html::new("<html><body>Hello</body></html>");
2318 let response = html.into_response();
2319
2320 let content_type = response
2321 .headers()
2322 .iter()
2323 .find(|(name, _)| name == "content-type")
2324 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2325
2326 assert_eq!(content_type, Some("text/html; charset=utf-8".to_string()));
2327 }
2328
2329 #[test]
2330 fn html_response_has_status_200() {
2331 let html = Html::new("<p>test</p>");
2332 let response = html.into_response();
2333 assert_eq!(response.status().as_u16(), 200);
2334 }
2335
2336 #[test]
2337 fn html_content_accessor() {
2338 let html = Html::new("<div>content</div>");
2339 assert_eq!(html.content(), "<div>content</div>");
2340 }
2341
2342 #[test]
2343 fn html_from_string() {
2344 let html: Html = "hello".into();
2345 assert_eq!(html.content(), "hello");
2346 }
2347
2348 #[test]
2353 fn text_response_has_correct_content_type() {
2354 let text = Text::new("Plain text content");
2355 let response = text.into_response();
2356
2357 let content_type = response
2358 .headers()
2359 .iter()
2360 .find(|(name, _)| name == "content-type")
2361 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2362
2363 assert_eq!(content_type, Some("text/plain; charset=utf-8".to_string()));
2364 }
2365
2366 #[test]
2367 fn text_response_has_status_200() {
2368 let text = Text::new("hello");
2369 let response = text.into_response();
2370 assert_eq!(response.status().as_u16(), 200);
2371 }
2372
2373 #[test]
2374 fn text_content_accessor() {
2375 let text = Text::new("my content");
2376 assert_eq!(text.content(), "my content");
2377 }
2378
2379 #[test]
2384 fn no_content_returns_204() {
2385 let response = NoContent.into_response();
2386 assert_eq!(response.status().as_u16(), 204);
2387 }
2388
2389 #[test]
2390 fn no_content_has_empty_body() {
2391 let response = NoContent.into_response();
2392 assert!(response.body_ref().is_empty());
2393 }
2394
2395 #[test]
2400 fn file_response_infers_png_content_type() {
2401 let file = FileResponse::new("/path/to/image.png");
2402 assert_eq!(file.path().to_str(), Some("/path/to/image.png"));
2404 }
2405
2406 #[test]
2407 fn file_response_download_as_sets_attachment() {
2408 let file = FileResponse::new("/data/report.csv").download_as("my-report.csv");
2409 let disposition = file.content_disposition();
2410 assert!(disposition.contains("attachment"));
2411 assert!(disposition.contains("my-report.csv"));
2412 }
2413
2414 #[test]
2415 fn file_response_inline_sets_inline() {
2416 let file = FileResponse::new("/image.png").inline();
2417 let disposition = file.content_disposition();
2418 assert_eq!(disposition, "inline");
2419 }
2420
2421 #[test]
2422 fn file_response_custom_content_type() {
2423 let temp_dir = std::env::temp_dir();
2425 let test_file = temp_dir.join("test_response_file.txt");
2426 std::fs::write(&test_file, b"test content").unwrap();
2427
2428 let file = FileResponse::new(&test_file).content_type("application/custom");
2429 let response = file.into_response();
2430
2431 let content_type = response
2432 .headers()
2433 .iter()
2434 .find(|(name, _)| name == "content-type")
2435 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2436
2437 assert_eq!(content_type, Some("application/custom".to_string()));
2438
2439 let _ = std::fs::remove_file(test_file);
2441 }
2442
2443 #[test]
2444 fn file_response_includes_accept_ranges_header() {
2445 let temp_dir = std::env::temp_dir();
2447 let test_file = temp_dir.join("test_accept_ranges.txt");
2448 std::fs::write(&test_file, b"test content for range support").unwrap();
2449
2450 let file = FileResponse::new(&test_file);
2451 let response = file.into_response();
2452
2453 let accept_ranges = response
2454 .headers()
2455 .iter()
2456 .find(|(name, _)| name == "accept-ranges")
2457 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2458
2459 assert_eq!(accept_ranges, Some("bytes".to_string()));
2460
2461 let _ = std::fs::remove_file(test_file);
2463 }
2464
2465 #[test]
2466 fn file_response_not_found_returns_404() {
2467 let file = FileResponse::new("/nonexistent/path/file.txt");
2468 let response = file.into_response();
2469 assert_eq!(response.status().as_u16(), 404);
2470 }
2471
2472 #[test]
2477 fn mime_type_for_common_extensions() {
2478 assert_eq!(mime_type_for_extension("html"), "text/html; charset=utf-8");
2479 assert_eq!(mime_type_for_extension("css"), "text/css; charset=utf-8");
2480 assert_eq!(
2481 mime_type_for_extension("js"),
2482 "text/javascript; charset=utf-8"
2483 );
2484 assert_eq!(mime_type_for_extension("json"), "application/json");
2485 assert_eq!(mime_type_for_extension("png"), "image/png");
2486 assert_eq!(mime_type_for_extension("jpg"), "image/jpeg");
2487 assert_eq!(mime_type_for_extension("pdf"), "application/pdf");
2488 assert_eq!(mime_type_for_extension("zip"), "application/zip");
2489 }
2490
2491 #[test]
2492 fn mime_type_case_insensitive() {
2493 assert_eq!(mime_type_for_extension("HTML"), "text/html; charset=utf-8");
2494 assert_eq!(mime_type_for_extension("PNG"), "image/png");
2495 assert_eq!(mime_type_for_extension("Json"), "application/json");
2496 }
2497
2498 #[test]
2499 fn mime_type_unknown_returns_octet_stream() {
2500 assert_eq!(
2501 mime_type_for_extension("unknown"),
2502 "application/octet-stream"
2503 );
2504 assert_eq!(mime_type_for_extension("xyz"), "application/octet-stream");
2505 }
2506
2507 #[test]
2512 fn status_code_see_other_is_303() {
2513 assert_eq!(StatusCode::SEE_OTHER.as_u16(), 303);
2514 }
2515
2516 #[test]
2517 fn status_code_see_other_canonical_reason() {
2518 assert_eq!(StatusCode::SEE_OTHER.canonical_reason(), "See Other");
2519 }
2520
2521 #[test]
2522 fn status_code_partial_content_is_206() {
2523 assert_eq!(StatusCode::PARTIAL_CONTENT.as_u16(), 206);
2524 }
2525
2526 #[test]
2527 fn status_code_partial_content_canonical_reason() {
2528 assert_eq!(
2529 StatusCode::PARTIAL_CONTENT.canonical_reason(),
2530 "Partial Content"
2531 );
2532 }
2533
2534 #[test]
2535 fn status_code_range_not_satisfiable_is_416() {
2536 assert_eq!(StatusCode::RANGE_NOT_SATISFIABLE.as_u16(), 416);
2537 }
2538
2539 #[test]
2540 fn status_code_range_not_satisfiable_canonical_reason() {
2541 assert_eq!(
2542 StatusCode::RANGE_NOT_SATISFIABLE.canonical_reason(),
2543 "Range Not Satisfiable"
2544 );
2545 }
2546
2547 #[test]
2548 fn response_partial_content_returns_206() {
2549 let response = Response::partial_content();
2550 assert_eq!(response.status().as_u16(), 206);
2551 }
2552
2553 #[test]
2554 fn response_range_not_satisfiable_returns_416() {
2555 let response = Response::range_not_satisfiable();
2556 assert_eq!(response.status().as_u16(), 416);
2557 }
2558
2559 #[test]
2564 fn response_set_cookie_adds_header() {
2565 let response = Response::ok().set_cookie(SetCookie::new("session", "abc123"));
2566
2567 let cookie_header = response
2568 .headers()
2569 .iter()
2570 .find(|(name, _)| name == "set-cookie")
2571 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2572
2573 assert!(cookie_header.is_some());
2574 let header_value = cookie_header.unwrap();
2575 assert!(header_value.contains("session=abc123"));
2576 }
2577
2578 #[test]
2579 fn response_set_cookie_with_attributes() {
2580 let response = Response::ok().set_cookie(
2581 SetCookie::new("session", "token123")
2582 .http_only(true)
2583 .secure(true)
2584 .same_site(SameSite::Strict)
2585 .max_age(3600)
2586 .path("/api"),
2587 );
2588
2589 let cookie_header = response
2590 .headers()
2591 .iter()
2592 .find(|(name, _)| name == "set-cookie")
2593 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2594 .unwrap();
2595
2596 assert!(cookie_header.contains("session=token123"));
2597 assert!(cookie_header.contains("HttpOnly"));
2598 assert!(cookie_header.contains("Secure"));
2599 assert!(cookie_header.contains("SameSite=Strict"));
2600 assert!(cookie_header.contains("Max-Age=3600"));
2601 assert!(cookie_header.contains("Path=/api"));
2602 }
2603
2604 #[test]
2605 fn response_set_multiple_cookies() {
2606 let response = Response::ok()
2607 .set_cookie(SetCookie::new("session", "abc"))
2608 .set_cookie(SetCookie::new("prefs", "dark"));
2609
2610 let cookie_headers: Vec<_> = response
2611 .headers()
2612 .iter()
2613 .filter(|(name, _)| name == "set-cookie")
2614 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2615 .collect();
2616
2617 assert_eq!(cookie_headers.len(), 2);
2618 assert!(cookie_headers.iter().any(|h| h.contains("session=abc")));
2619 assert!(cookie_headers.iter().any(|h| h.contains("prefs=dark")));
2620 }
2621
2622 #[test]
2623 fn response_delete_cookie_sets_max_age_zero() {
2624 let response = Response::ok().delete_cookie("session");
2625
2626 let cookie_header = response
2627 .headers()
2628 .iter()
2629 .find(|(name, _)| name == "set-cookie")
2630 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2631 .unwrap();
2632
2633 assert!(cookie_header.contains("session="));
2634 assert!(cookie_header.contains("Max-Age=0"));
2635 }
2636
2637 #[test]
2638 fn response_set_and_delete_cookies() {
2639 let response = Response::ok()
2641 .set_cookie(SetCookie::new("new_session", "xyz"))
2642 .delete_cookie("old_session");
2643
2644 let cookie_headers: Vec<_> = response
2645 .headers()
2646 .iter()
2647 .filter(|(name, _)| name == "set-cookie")
2648 .map(|(_, value)| String::from_utf8_lossy(value).to_string())
2649 .collect();
2650
2651 assert_eq!(cookie_headers.len(), 2);
2652 assert!(cookie_headers.iter().any(|h| h.contains("new_session=xyz")));
2653 assert!(
2654 cookie_headers
2655 .iter()
2656 .any(|h| h.contains("old_session=") && h.contains("Max-Age=0"))
2657 );
2658 }
2659
2660 #[test]
2665 fn binary_new_creates_from_vec() {
2666 let data = vec![0x01, 0x02, 0x03, 0x04];
2667 let binary = Binary::new(data.clone());
2668 assert_eq!(binary.data(), &data[..]);
2669 }
2670
2671 #[test]
2672 fn binary_new_creates_from_slice() {
2673 let data = [0xDE, 0xAD, 0xBE, 0xEF];
2674 let binary = Binary::new(&data[..]);
2675 assert_eq!(binary.data(), &data);
2676 }
2677
2678 #[test]
2679 fn binary_into_response_has_correct_content_type() {
2680 let binary = Binary::new(vec![1, 2, 3]);
2681 let response = binary.into_response();
2682
2683 let content_type = response
2684 .headers()
2685 .iter()
2686 .find(|(name, _)| name == "content-type")
2687 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2688
2689 assert_eq!(content_type, Some("application/octet-stream".to_string()));
2690 }
2691
2692 #[test]
2693 fn binary_into_response_has_status_200() {
2694 let binary = Binary::new(vec![1, 2, 3]);
2695 let response = binary.into_response();
2696 assert_eq!(response.status().as_u16(), 200);
2697 }
2698
2699 #[test]
2700 fn binary_into_response_has_correct_body() {
2701 let data = vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]; let binary = Binary::new(data.clone());
2703 let response = binary.into_response();
2704
2705 if let ResponseBody::Bytes(bytes) = response.body_ref() {
2706 assert_eq!(bytes, &data);
2707 } else {
2708 panic!("Expected Bytes body");
2709 }
2710 }
2711
2712 #[test]
2713 fn binary_with_content_type_returns_binary_with_type() {
2714 let data = vec![0x89, 0x50, 0x4E, 0x47]; let binary = Binary::new(data);
2716 let binary_typed = binary.with_content_type("image/png");
2717
2718 assert_eq!(binary_typed.content_type(), "image/png");
2719 }
2720
2721 #[test]
2722 fn binary_with_type_into_response_has_correct_content_type() {
2723 let data = vec![0xFF, 0xD8, 0xFF]; let binary = Binary::new(data).with_content_type("image/jpeg");
2725 let response = binary.into_response();
2726
2727 let content_type = response
2728 .headers()
2729 .iter()
2730 .find(|(name, _)| name == "content-type")
2731 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2732
2733 assert_eq!(content_type, Some("image/jpeg".to_string()));
2734 }
2735
2736 #[test]
2737 fn binary_with_type_into_response_has_correct_body() {
2738 let data = vec![0x25, 0x50, 0x44, 0x46]; let binary = Binary::new(data.clone()).with_content_type("application/pdf");
2740 let response = binary.into_response();
2741
2742 if let ResponseBody::Bytes(bytes) = response.body_ref() {
2743 assert_eq!(bytes, &data);
2744 } else {
2745 panic!("Expected Bytes body");
2746 }
2747 }
2748
2749 #[test]
2750 fn binary_with_type_data_accessor() {
2751 let data = vec![1, 2, 3, 4, 5];
2752 let binary = Binary::new(data.clone()).with_content_type("application/custom");
2753 assert_eq!(binary.data(), &data[..]);
2754 }
2755
2756 #[test]
2757 fn binary_with_type_status_200() {
2758 let binary = Binary::new(vec![0]).with_content_type("text/plain");
2759 let response = binary.into_response();
2760 assert_eq!(response.status().as_u16(), 200);
2761 }
2762
2763 #[test]
2768 fn response_model_config_default() {
2769 let config = ResponseModelConfig::new();
2770 assert!(config.include.is_none());
2771 assert!(config.exclude.is_none());
2772 assert!(!config.by_alias);
2773 assert!(!config.exclude_unset);
2774 assert!(!config.exclude_defaults);
2775 assert!(!config.exclude_none);
2776 }
2777
2778 #[test]
2779 fn response_model_config_include() {
2780 let fields: std::collections::HashSet<String> =
2781 ["id", "name"].iter().map(|s| (*s).to_string()).collect();
2782 let config = ResponseModelConfig::new().include(fields.clone());
2783 assert_eq!(config.include, Some(fields));
2784 }
2785
2786 #[test]
2787 fn response_model_config_exclude() {
2788 let fields: std::collections::HashSet<String> =
2789 ["password"].iter().map(|s| (*s).to_string()).collect();
2790 let config = ResponseModelConfig::new().exclude(fields.clone());
2791 assert_eq!(config.exclude, Some(fields));
2792 }
2793
2794 #[test]
2795 fn response_model_config_by_alias() {
2796 let config = ResponseModelConfig::new().by_alias(true);
2797 assert!(config.by_alias);
2798 }
2799
2800 #[test]
2801 fn response_model_config_exclude_none() {
2802 let config = ResponseModelConfig::new().exclude_none(true);
2803 assert!(config.exclude_none);
2804 }
2805
2806 #[test]
2807 fn response_model_config_exclude_unset() {
2808 let config = ResponseModelConfig::new().exclude_unset(true);
2809 assert!(config.exclude_unset);
2810 }
2811
2812 #[test]
2813 fn response_model_config_exclude_defaults() {
2814 let config = ResponseModelConfig::new().exclude_defaults(true);
2815 assert!(config.exclude_defaults);
2816 }
2817
2818 #[test]
2819 fn response_model_config_has_filtering() {
2820 let config = ResponseModelConfig::new();
2821 assert!(!config.has_filtering());
2822
2823 let config =
2824 ResponseModelConfig::new().include(["id"].iter().map(|s| (*s).to_string()).collect());
2825 assert!(config.has_filtering());
2826
2827 let config = ResponseModelConfig::new()
2828 .exclude(["password"].iter().map(|s| (*s).to_string()).collect());
2829 assert!(config.has_filtering());
2830
2831 let config = ResponseModelConfig::new().exclude_none(true);
2832 assert!(config.has_filtering());
2833 }
2834
2835 #[test]
2836 fn response_model_config_filter_json_include() {
2837 let config = ResponseModelConfig::new()
2838 .include(["id", "name"].iter().map(|s| (*s).to_string()).collect());
2839
2840 let value = serde_json::json!({
2841 "id": 1,
2842 "name": "Alice",
2843 "email": "alice@example.com",
2844 "password": "secret"
2845 });
2846
2847 let filtered = config.filter_json(value).unwrap();
2848 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2849 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2850 assert!(filtered.get("email").is_none());
2851 assert!(filtered.get("password").is_none());
2852 }
2853
2854 #[test]
2855 fn response_model_config_filter_json_exclude() {
2856 let config = ResponseModelConfig::new().exclude(
2857 ["password", "secret"]
2858 .iter()
2859 .map(|s| (*s).to_string())
2860 .collect(),
2861 );
2862
2863 let value = serde_json::json!({
2864 "id": 1,
2865 "name": "Alice",
2866 "password": "secret123",
2867 "secret": "hidden"
2868 });
2869
2870 let filtered = config.filter_json(value).unwrap();
2871 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2872 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2873 assert!(filtered.get("password").is_none());
2874 assert!(filtered.get("secret").is_none());
2875 }
2876
2877 #[test]
2878 fn response_model_config_filter_json_exclude_none() {
2879 let config = ResponseModelConfig::new().exclude_none(true);
2880
2881 let value = serde_json::json!({
2882 "id": 1,
2883 "name": "Alice",
2884 "middle_name": null,
2885 "nickname": null
2886 });
2887
2888 let filtered = config.filter_json(value).unwrap();
2889 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2890 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2891 assert!(filtered.get("middle_name").is_none());
2892 assert!(filtered.get("nickname").is_none());
2893 }
2894
2895 #[test]
2896 fn response_model_config_filter_json_combined() {
2897 let config = ResponseModelConfig::new()
2898 .include(
2899 ["id", "name", "email", "middle_name"]
2900 .iter()
2901 .map(|s| (*s).to_string())
2902 .collect(),
2903 )
2904 .exclude_none(true);
2905
2906 let value = serde_json::json!({
2907 "id": 1,
2908 "name": "Alice",
2909 "email": "alice@example.com",
2910 "middle_name": null,
2911 "password": "secret"
2912 });
2913
2914 let filtered = config.filter_json(value).unwrap();
2915 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2916 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2917 assert_eq!(
2918 filtered.get("email"),
2919 Some(&serde_json::json!("alice@example.com"))
2920 );
2921 assert!(filtered.get("middle_name").is_none()); assert!(filtered.get("password").is_none()); }
2924
2925 #[test]
2926 fn response_model_config_by_alias_requires_alias_metadata() {
2927 let config = ResponseModelConfig::new().by_alias(true);
2928 let value = serde_json::json!({"userId": 1, "name": "Alice"});
2929 assert!(config.filter_json(value).is_err());
2930 }
2931
2932 #[test]
2933 fn response_model_config_by_alias_normalizes_and_realiases() {
2934 static ALIASES: &[(&str, &str)] = &[("user_id", "userId")];
2935
2936 let config = ResponseModelConfig::new().with_aliases(ALIASES);
2938 let value = serde_json::json!({"userId": 1, "name": "Alice"});
2939 let filtered = config.filter_json(value).unwrap();
2940 assert_eq!(filtered.get("user_id"), Some(&serde_json::json!(1)));
2941 assert!(filtered.get("userId").is_none());
2942
2943 let config = ResponseModelConfig::new()
2945 .with_aliases(ALIASES)
2946 .by_alias(true);
2947 let value = serde_json::json!({"user_id": 1, "name": "Alice"});
2948 let filtered = config.filter_json(value).unwrap();
2949 assert_eq!(filtered.get("userId"), Some(&serde_json::json!(1)));
2950 assert!(filtered.get("user_id").is_none());
2951 }
2952
2953 #[test]
2954 fn response_model_config_exclude_defaults_requires_defaults_provider() {
2955 let config = ResponseModelConfig::new().exclude_defaults(true);
2956 let value = serde_json::json!({"active": false});
2957 assert!(config.filter_json(value).is_err());
2958 }
2959
2960 #[test]
2961 fn response_model_config_exclude_defaults_filters_matching_fields() {
2962 #[derive(Default, Serialize)]
2963 struct UserDefaults {
2964 active: bool,
2965 name: String,
2966 }
2967
2968 let config = ResponseModelConfig::new()
2970 .with_defaults_from::<UserDefaults>()
2971 .exclude_defaults(true);
2972 let value = serde_json::json!({"active": false, "name": "Alice"});
2973 let filtered = config.filter_json(value).unwrap();
2974 assert!(filtered.get("active").is_none());
2975 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2976 }
2977
2978 #[test]
2979 fn response_model_config_exclude_unset_requires_set_fields() {
2980 let config = ResponseModelConfig::new().exclude_unset(true);
2981 let value = serde_json::json!({"id": 1, "name": "Alice"});
2982 assert!(config.filter_json(value).is_err());
2983 }
2984
2985 #[test]
2986 fn response_model_config_exclude_unset_filters_not_set() {
2987 let set_fields: std::collections::HashSet<String> =
2988 ["id", "name"].iter().map(|s| (*s).to_string()).collect();
2989 let config = ResponseModelConfig::new()
2990 .with_set_fields(set_fields)
2991 .exclude_unset(true);
2992 let value = serde_json::json!({"id": 1, "name": "Alice", "email": "a@b.com"});
2993 let filtered = config.filter_json(value).unwrap();
2994 assert_eq!(filtered.get("id"), Some(&serde_json::json!(1)));
2995 assert_eq!(filtered.get("name"), Some(&serde_json::json!("Alice")));
2996 assert!(filtered.get("email").is_none());
2997 }
2998
2999 #[test]
3004 fn validated_response_serializes_struct() {
3005 #[derive(Serialize)]
3006 struct User {
3007 id: i64,
3008 name: String,
3009 }
3010
3011 let user = User {
3012 id: 1,
3013 name: "Alice".to_string(),
3014 };
3015
3016 let response = ValidatedResponse::new(user).into_response();
3017 assert_eq!(response.status().as_u16(), 200);
3018
3019 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3020 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3021 assert_eq!(parsed["id"], 1);
3022 assert_eq!(parsed["name"], "Alice");
3023 } else {
3024 panic!("Expected Bytes body");
3025 }
3026 }
3027
3028 #[test]
3029 fn validated_response_excludes_fields() {
3030 #[derive(Serialize)]
3031 struct User {
3032 id: i64,
3033 name: String,
3034 password: String,
3035 }
3036
3037 let user = User {
3038 id: 1,
3039 name: "Alice".to_string(),
3040 password: "secret123".to_string(),
3041 };
3042
3043 let response = ValidatedResponse::new(user)
3044 .with_config(
3045 ResponseModelConfig::new()
3046 .exclude(["password"].iter().map(|s| (*s).to_string()).collect()),
3047 )
3048 .into_response();
3049
3050 assert_eq!(response.status().as_u16(), 200);
3051
3052 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3053 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3054 assert_eq!(parsed["id"], 1);
3055 assert_eq!(parsed["name"], "Alice");
3056 assert!(parsed.get("password").is_none());
3057 } else {
3058 panic!("Expected Bytes body");
3059 }
3060 }
3061
3062 #[test]
3063 fn validated_response_includes_fields() {
3064 #[derive(Serialize)]
3065 struct User {
3066 id: i64,
3067 name: String,
3068 email: String,
3069 password: String,
3070 }
3071
3072 let user = User {
3073 id: 1,
3074 name: "Alice".to_string(),
3075 email: "alice@example.com".to_string(),
3076 password: "secret123".to_string(),
3077 };
3078
3079 let response = ValidatedResponse::new(user)
3080 .with_config(
3081 ResponseModelConfig::new()
3082 .include(["id", "name"].iter().map(|s| (*s).to_string()).collect()),
3083 )
3084 .into_response();
3085
3086 assert_eq!(response.status().as_u16(), 200);
3087
3088 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3089 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3090 assert_eq!(parsed["id"], 1);
3091 assert_eq!(parsed["name"], "Alice");
3092 assert!(parsed.get("email").is_none());
3093 assert!(parsed.get("password").is_none());
3094 } else {
3095 panic!("Expected Bytes body");
3096 }
3097 }
3098
3099 #[test]
3100 fn validated_response_exclude_none_values() {
3101 #[derive(Serialize)]
3102 struct User {
3103 id: i64,
3104 name: String,
3105 nickname: Option<String>,
3106 }
3107
3108 let user = User {
3109 id: 1,
3110 name: "Alice".to_string(),
3111 nickname: None,
3112 };
3113
3114 let response = ValidatedResponse::new(user)
3115 .with_config(ResponseModelConfig::new().exclude_none(true))
3116 .into_response();
3117
3118 assert_eq!(response.status().as_u16(), 200);
3119
3120 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3121 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3122 assert_eq!(parsed["id"], 1);
3123 assert_eq!(parsed["name"], "Alice");
3124 assert!(parsed.get("nickname").is_none());
3125 } else {
3126 panic!("Expected Bytes body");
3127 }
3128 }
3129
3130 #[test]
3131 fn validated_response_content_type_is_json() {
3132 #[derive(Serialize)]
3133 struct Data {
3134 value: i32,
3135 }
3136
3137 let response = ValidatedResponse::new(Data { value: 42 }).into_response();
3138
3139 let content_type = response
3140 .headers()
3141 .iter()
3142 .find(|(name, _)| name == "content-type")
3143 .map(|(_, value)| String::from_utf8_lossy(value).to_string());
3144
3145 assert_eq!(content_type, Some("application/json".to_string()));
3146 }
3147
3148 #[test]
3153 fn exclude_fields_helper() {
3154 #[derive(Serialize)]
3155 struct User {
3156 id: i64,
3157 name: String,
3158 password: String,
3159 }
3160
3161 let user = User {
3162 id: 1,
3163 name: "Alice".to_string(),
3164 password: "secret".to_string(),
3165 };
3166
3167 let response = exclude_fields(user, &["password"]).into_response();
3168
3169 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3170 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3171 assert!(parsed.get("id").is_some());
3172 assert!(parsed.get("name").is_some());
3173 assert!(parsed.get("password").is_none());
3174 } else {
3175 panic!("Expected Bytes body");
3176 }
3177 }
3178
3179 #[test]
3180 fn include_fields_helper() {
3181 #[derive(Serialize)]
3182 struct User {
3183 id: i64,
3184 name: String,
3185 email: String,
3186 password: String,
3187 }
3188
3189 let user = User {
3190 id: 1,
3191 name: "Alice".to_string(),
3192 email: "alice@example.com".to_string(),
3193 password: "secret".to_string(),
3194 };
3195
3196 let response = include_fields(user, &["id", "name"]).into_response();
3197
3198 if let ResponseBody::Bytes(bytes) = response.body_ref() {
3199 let parsed: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3200 assert!(parsed.get("id").is_some());
3201 assert!(parsed.get("name").is_some());
3202 assert!(parsed.get("email").is_none());
3203 assert!(parsed.get("password").is_none());
3204 } else {
3205 panic!("Expected Bytes body");
3206 }
3207 }
3208
3209 #[test]
3214 fn status_code_precondition_failed() {
3215 assert_eq!(StatusCode::PRECONDITION_FAILED.as_u16(), 412);
3216 assert_eq!(
3217 StatusCode::PRECONDITION_FAILED.canonical_reason(),
3218 "Precondition Failed"
3219 );
3220 }
3221
3222 #[test]
3223 fn response_not_modified_status() {
3224 let resp = Response::not_modified();
3225 assert_eq!(resp.status().as_u16(), 304);
3226 }
3227
3228 #[test]
3229 fn response_precondition_failed_status() {
3230 let resp = Response::precondition_failed();
3231 assert_eq!(resp.status().as_u16(), 412);
3232 }
3233
3234 #[test]
3235 fn response_with_etag() {
3236 let resp = Response::ok().with_etag("\"abc123\"");
3237 let etag = resp
3238 .headers()
3239 .iter()
3240 .find(|(n, _)| n == "ETag")
3241 .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3242 assert_eq!(etag, Some("\"abc123\"".to_string()));
3243 }
3244
3245 #[test]
3246 fn response_with_weak_etag() {
3247 let resp = Response::ok().with_weak_etag("\"abc123\"");
3248 let etag = resp
3249 .headers()
3250 .iter()
3251 .find(|(n, _)| n == "ETag")
3252 .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3253 assert_eq!(etag, Some("W/\"abc123\"".to_string()));
3254 }
3255
3256 #[test]
3257 fn response_with_weak_etag_already_prefixed() {
3258 let resp = Response::ok().with_weak_etag("W/\"abc123\"");
3259 let etag = resp
3260 .headers()
3261 .iter()
3262 .find(|(n, _)| n == "ETag")
3263 .map(|(_, v)| String::from_utf8_lossy(v).to_string());
3264 assert_eq!(etag, Some("W/\"abc123\"".to_string()));
3265 }
3266
3267 #[test]
3268 fn check_if_none_match_exact() {
3269 assert!(!check_if_none_match("\"abc\"", "\"abc\""));
3271 }
3272
3273 #[test]
3274 fn check_if_none_match_no_match() {
3275 assert!(check_if_none_match("\"abc\"", "\"def\""));
3277 }
3278
3279 #[test]
3280 fn check_if_none_match_wildcard() {
3281 assert!(!check_if_none_match("*", "\"anything\""));
3282 }
3283
3284 #[test]
3285 fn check_if_none_match_multiple_etags() {
3286 assert!(!check_if_none_match("\"aaa\", \"bbb\", \"ccc\"", "\"bbb\""));
3288 assert!(check_if_none_match("\"aaa\", \"bbb\"", "\"ccc\""));
3290 }
3291
3292 #[test]
3293 fn check_if_none_match_weak_comparison() {
3294 assert!(!check_if_none_match("W/\"abc\"", "\"abc\""));
3296 assert!(!check_if_none_match("\"abc\"", "W/\"abc\""));
3297 assert!(!check_if_none_match("W/\"abc\"", "W/\"abc\""));
3298 }
3299
3300 #[test]
3301 fn check_if_match_exact() {
3302 assert!(check_if_match("\"abc\"", "\"abc\""));
3304 }
3305
3306 #[test]
3307 fn check_if_match_no_match() {
3308 assert!(!check_if_match("\"abc\"", "\"def\""));
3310 }
3311
3312 #[test]
3313 fn check_if_match_wildcard() {
3314 assert!(check_if_match("*", "\"anything\""));
3315 }
3316
3317 #[test]
3318 fn check_if_match_weak_etag_fails() {
3319 assert!(!check_if_match("W/\"abc\"", "\"abc\""));
3321 assert!(!check_if_match("\"abc\"", "W/\"abc\""));
3322 }
3323
3324 #[test]
3325 fn check_if_match_multiple_etags() {
3326 assert!(check_if_match("\"aaa\", \"bbb\"", "\"bbb\""));
3327 assert!(!check_if_match("\"aaa\", \"bbb\"", "\"ccc\""));
3328 }
3329
3330 #[test]
3331 fn apply_conditional_get_304() {
3332 use crate::request::Method;
3333
3334 let headers = vec![("If-None-Match".to_string(), b"\"abc123\"".to_vec())];
3335 let response = Response::ok().with_etag("\"abc123\"");
3336 let result = apply_conditional(&headers, Method::Get, response);
3337 assert_eq!(result.status().as_u16(), 304);
3338 }
3339
3340 #[test]
3341 fn apply_conditional_get_no_match_200() {
3342 use crate::request::Method;
3343
3344 let headers = vec![("If-None-Match".to_string(), b"\"old\"".to_vec())];
3345 let response = Response::ok().with_etag("\"new\"");
3346 let result = apply_conditional(&headers, Method::Get, response);
3347 assert_eq!(result.status().as_u16(), 200);
3348 }
3349
3350 #[test]
3351 fn apply_conditional_put_412() {
3352 use crate::request::Method;
3353
3354 let headers = vec![("If-Match".to_string(), b"\"old\"".to_vec())];
3355 let response = Response::ok().with_etag("\"new\"");
3356 let result = apply_conditional(&headers, Method::Put, response);
3357 assert_eq!(result.status().as_u16(), 412);
3358 }
3359
3360 #[test]
3361 fn apply_conditional_put_match_200() {
3362 use crate::request::Method;
3363
3364 let headers = vec![("If-Match".to_string(), b"\"current\"".to_vec())];
3365 let response = Response::ok().with_etag("\"current\"");
3366 let result = apply_conditional(&headers, Method::Put, response);
3367 assert_eq!(result.status().as_u16(), 200);
3368 }
3369
3370 #[test]
3371 fn apply_conditional_no_etag_passthrough() {
3372 use crate::request::Method;
3373
3374 let headers = vec![("If-None-Match".to_string(), b"\"abc\"".to_vec())];
3375 let response = Response::ok(); let result = apply_conditional(&headers, Method::Get, response);
3377 assert_eq!(result.status().as_u16(), 200);
3378 }
3379
3380 #[test]
3385 fn link_header_single() {
3386 let h = LinkHeader::new().link("https://example.com/next", LinkRel::Next);
3387 assert_eq!(h.to_string(), r#"<https://example.com/next>; rel="next""#);
3388 }
3389
3390 #[test]
3391 fn link_header_multiple() {
3392 let h = LinkHeader::new()
3393 .link("/page/2", LinkRel::Next)
3394 .link("/page/0", LinkRel::Prev);
3395 let s = h.to_string();
3396 assert!(s.contains(r#"</page/2>; rel="next""#));
3397 assert!(s.contains(r#"</page/0>; rel="prev""#));
3398 assert!(s.contains(", "));
3399 }
3400
3401 #[test]
3402 fn link_with_title_and_type() {
3403 let link = Link::new("https://api.example.com", LinkRel::Related)
3404 .title("API Docs")
3405 .media_type("text/html");
3406 let s = link.to_string();
3407 assert!(s.contains(r#"title="API Docs""#));
3408 assert!(s.contains(r#"type="text/html""#));
3409 }
3410
3411 #[test]
3412 fn link_header_custom_rel() {
3413 let h = LinkHeader::new().link("/schema", LinkRel::Custom("describedby".to_string()));
3414 assert!(h.to_string().contains(r#"rel="describedby""#));
3415 }
3416
3417 #[test]
3418 fn link_header_paginate_first_page() {
3419 let h = LinkHeader::new().paginate("/users", 1, 10, 50);
3420 let s = h.to_string();
3421 assert!(s.contains(r#"rel="self""#));
3422 assert!(s.contains(r#"rel="first""#));
3423 assert!(s.contains(r#"rel="last""#));
3424 assert!(s.contains(r#"rel="next""#));
3425 assert!(!s.contains(r#"rel="prev""#)); assert!(s.contains("page=5")); }
3428
3429 #[test]
3430 fn link_header_paginate_middle_page() {
3431 let h = LinkHeader::new().paginate("/users", 3, 10, 50);
3432 let s = h.to_string();
3433 assert!(s.contains(r#"rel="prev""#));
3434 assert!(s.contains(r#"rel="next""#));
3435 assert!(s.contains("page=2")); assert!(s.contains("page=4")); }
3438
3439 #[test]
3440 fn link_header_paginate_last_page() {
3441 let h = LinkHeader::new().paginate("/users", 5, 10, 50);
3442 let s = h.to_string();
3443 assert!(s.contains(r#"rel="prev""#));
3444 assert!(!s.contains(r#"rel="next""#)); }
3446
3447 #[test]
3448 fn link_header_paginate_with_existing_query() {
3449 let h = LinkHeader::new().paginate("/users?sort=name", 1, 10, 20);
3450 let s = h.to_string();
3451 assert!(s.contains("sort=name&page="));
3452 }
3453
3454 #[test]
3455 fn link_header_empty() {
3456 let h = LinkHeader::new();
3457 assert!(h.is_empty());
3458 assert_eq!(h.len(), 0);
3459 assert_eq!(h.to_string(), "");
3460 }
3461
3462 #[test]
3463 fn link_header_apply_to_response() {
3464 let h = LinkHeader::new().link("/next", LinkRel::Next);
3465 let response = h.apply(Response::ok());
3466 let link_hdr = response
3467 .headers()
3468 .iter()
3469 .find(|(n, _)| n == "link")
3470 .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string());
3471 assert!(link_hdr.unwrap().contains("rel=\"next\""));
3472 }
3473
3474 #[test]
3475 fn link_header_apply_empty_noop() {
3476 let h = LinkHeader::new();
3477 let response = h.apply(Response::ok());
3478 let has_link = response.headers().iter().any(|(n, _)| n == "link");
3479 assert!(!has_link);
3480 }
3481
3482 #[test]
3483 fn link_rel_display() {
3484 assert_eq!(LinkRel::Self_.to_string(), "self");
3485 assert_eq!(LinkRel::Next.to_string(), "next");
3486 assert_eq!(LinkRel::Prev.to_string(), "prev");
3487 assert_eq!(LinkRel::First.to_string(), "first");
3488 assert_eq!(LinkRel::Last.to_string(), "last");
3489 assert_eq!(LinkRel::Related.to_string(), "related");
3490 assert_eq!(LinkRel::Alternate.to_string(), "alternate");
3491 }
3492}