1use crate::{Error, HttpRequest, HttpResponse};
32use bytes::Bytes;
33use serde::Serialize;
34use std::cmp::Ordering;
35use std::collections::HashMap;
36use std::fmt;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct MediaType {
45 pub type_: String,
47 pub subtype: String,
49 pub params: HashMap<String, String>,
51}
52
53impl MediaType {
54 pub fn new(type_: impl Into<String>, subtype: impl Into<String>) -> Self {
56 Self {
57 type_: type_.into(),
58 subtype: subtype.into(),
59 params: HashMap::new(),
60 }
61 }
62
63 pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
65 self.params.insert(key.into(), value.into());
66 self
67 }
68
69 pub fn json() -> Self {
71 Self::new("application", "json")
72 }
73
74 pub fn html() -> Self {
76 Self::new("text", "html")
77 }
78
79 pub fn plain_text() -> Self {
81 Self::new("text", "plain")
82 }
83
84 pub fn xml() -> Self {
86 Self::new("application", "xml")
87 }
88
89 pub fn text_xml() -> Self {
91 Self::new("text", "xml")
92 }
93
94 pub fn form_urlencoded() -> Self {
96 Self::new("application", "x-www-form-urlencoded")
97 }
98
99 pub fn multipart_form_data() -> Self {
101 Self::new("multipart", "form-data")
102 }
103
104 pub fn octet_stream() -> Self {
106 Self::new("application", "octet-stream")
107 }
108
109 pub fn any() -> Self {
111 Self::new("*", "*")
112 }
113
114 pub fn parse(s: &str) -> Option<Self> {
116 let s = s.trim();
117 let mut parts = s.split(';');
118
119 let type_subtype = parts.next()?.trim();
120 let mut type_parts = type_subtype.splitn(2, '/');
121
122 let type_ = type_parts.next()?.trim().to_lowercase();
123 let subtype = type_parts.next()?.trim().to_lowercase();
124
125 let mut params = HashMap::new();
126 for param in parts {
127 let param = param.trim();
128 if let Some((key, value)) = param.split_once('=') {
129 let key = key.trim().to_lowercase();
130 let value = value.trim().trim_matches('"').to_string();
131 if key != "q" {
133 params.insert(key, value);
134 }
135 }
136 }
137
138 Some(Self {
139 type_,
140 subtype,
141 params,
142 })
143 }
144
145 pub fn matches(&self, other: &MediaType) -> bool {
147 let type_matches = self.type_ == "*" || other.type_ == "*" || self.type_ == other.type_;
148 let subtype_matches =
149 self.subtype == "*" || other.subtype == "*" || self.subtype == other.subtype;
150 type_matches && subtype_matches
151 }
152
153 pub fn is_any(&self) -> bool {
155 self.type_ == "*" && self.subtype == "*"
156 }
157
158 pub fn is_type_wildcard(&self) -> bool {
160 self.type_ == "*"
161 }
162
163 pub fn is_subtype_wildcard(&self) -> bool {
165 self.subtype == "*"
166 }
167
168 pub fn mime_type(&self) -> String {
170 format!("{}/{}", self.type_, self.subtype)
171 }
172
173 pub fn to_header_value(&self) -> String {
175 let mut result = self.mime_type();
176 for (key, value) in &self.params {
177 result.push_str(&format!("; {}={}", key, value));
178 }
179 result
180 }
181}
182
183impl fmt::Display for MediaType {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 write!(f, "{}", self.to_header_value())
186 }
187}
188
189fn find_quality_param(s: &str) -> Option<usize> {
195 s.as_bytes()
196 .windows(3)
197 .position(|w| w[0] == b';' && w[1].eq_ignore_ascii_case(&b'q') && w[2] == b'=')
198}
199
200#[derive(Debug, Clone)]
206pub struct Accept {
207 pub media_types: Vec<(MediaType, f32)>,
209}
210
211impl Default for Accept {
212 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl Accept {
219 pub fn new() -> Self {
221 Self {
222 media_types: vec![(MediaType::any(), 1.0)],
223 }
224 }
225
226 pub fn parse(header: &str) -> Self {
237 let mut media_types: Vec<(MediaType, f32)> = header
238 .split(',')
239 .filter_map(|part| {
240 let part = part.trim();
241 if part.is_empty() {
242 return None;
243 }
244
245 let (media_part, quality) = Self::extract_quality(part);
247
248 MediaType::parse(media_part).map(|mt| (mt, quality))
249 })
250 .collect();
251
252 media_types.sort_by(|a, b| {
254 match b.1.partial_cmp(&a.1) {
256 Some(Ordering::Equal) | None => {}
257 Some(ord) => return ord,
258 }
259
260 let a_specificity = Self::specificity(&a.0);
262 let b_specificity = Self::specificity(&b.0);
263 b_specificity.cmp(&a_specificity)
264 });
265
266 Self { media_types }
267 }
268
269 fn extract_quality(s: &str) -> (&str, f32) {
271 if let Some(q_pos) = find_quality_param(s) {
273 let media_part = &s[..q_pos];
274 let q_part = &s[q_pos + 3..];
275
276 let quality = q_part
278 .split(';')
279 .next()
280 .and_then(|q| q.trim().parse::<f32>().ok())
281 .unwrap_or(1.0)
282 .clamp(0.0, 1.0);
283
284 (media_part, quality)
285 } else {
286 (s, 1.0)
287 }
288 }
289
290 fn specificity(mt: &MediaType) -> u8 {
292 let mut score = 0u8;
293 if mt.type_ != "*" {
294 score += 2;
295 }
296 if mt.subtype != "*" {
297 score += 1;
298 }
299 score
300 }
301
302 pub fn accepts(&self, media_type: &MediaType) -> bool {
304 self.quality_for(media_type) > 0.0
305 }
306
307 pub fn quality_for(&self, media_type: &MediaType) -> f32 {
309 for (mt, quality) in &self.media_types {
310 if mt.matches(media_type) {
311 return *quality;
312 }
313 }
314 0.0
315 }
316
317 pub fn preferred(&self) -> Option<&MediaType> {
319 self.media_types.first().map(|(mt, _)| mt)
320 }
321
322 pub fn prefers_json(&self) -> bool {
324 self.quality_for(&MediaType::json()) > self.quality_for(&MediaType::html())
325 }
326
327 pub fn prefers_html(&self) -> bool {
329 self.quality_for(&MediaType::html()) > self.quality_for(&MediaType::json())
330 }
331}
332
333pub fn negotiate_media_type<'a>(
338 accept: &Accept,
339 available: &'a [MediaType],
340) -> Option<&'a MediaType> {
341 let mut best: Option<(&'a MediaType, f32, u8)> = None;
342
343 for available_mt in available {
344 let quality = accept.quality_for(available_mt);
345 if quality > 0.0 {
346 let specificity = Accept::specificity(available_mt);
347 match &best {
348 None => best = Some((available_mt, quality, specificity)),
349 Some((_, best_q, best_s)) => {
350 if quality > *best_q || (quality == *best_q && specificity > *best_s) {
351 best = Some((available_mt, quality, specificity));
352 }
353 }
354 }
355 }
356 }
357
358 best.map(|(mt, _, _)| mt)
359}
360
361#[derive(Debug, Clone, PartialEq)]
367pub struct LanguageTag {
368 pub primary: String,
370 pub subtag: Option<String>,
372}
373
374impl LanguageTag {
375 pub fn new(primary: impl Into<String>) -> Self {
377 Self {
378 primary: primary.into().to_lowercase(),
379 subtag: None,
380 }
381 }
382
383 pub fn with_subtag(primary: impl Into<String>, subtag: impl Into<String>) -> Self {
385 Self {
386 primary: primary.into().to_lowercase(),
387 subtag: Some(subtag.into().to_uppercase()),
388 }
389 }
390
391 pub fn parse(s: &str) -> Option<Self> {
393 let s = s.trim();
394 if s.is_empty() || s == "*" {
395 return Some(Self::new("*"));
396 }
397
398 let mut parts = s.splitn(2, '-');
399 let primary = parts.next()?.trim().to_lowercase();
400 let subtag = parts.next().map(|s| s.trim().to_uppercase());
401
402 Some(Self { primary, subtag })
403 }
404
405 pub fn matches(&self, other: &LanguageTag) -> bool {
407 if self.primary == "*" || other.primary == "*" {
408 return true;
409 }
410 if self.primary != other.primary {
411 return false;
412 }
413 match (&self.subtag, &other.subtag) {
415 (Some(a), Some(b)) => a == b,
416 (None, _) => true, (Some(_), None) => false, }
419 }
420}
421
422impl fmt::Display for LanguageTag {
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 match &self.subtag {
425 Some(sub) => write!(f, "{}-{}", self.primary, sub),
426 None => write!(f, "{}", self.primary),
427 }
428 }
429}
430
431#[derive(Debug, Clone, Default)]
433pub struct AcceptLanguage {
434 pub languages: Vec<(LanguageTag, f32)>,
436}
437
438impl AcceptLanguage {
439 pub fn parse(header: &str) -> Self {
450 let mut languages: Vec<(LanguageTag, f32)> = header
451 .split(',')
452 .filter_map(|part| {
453 let part = part.trim();
454 if part.is_empty() {
455 return None;
456 }
457
458 let (lang_part, quality) = Self::extract_quality(part);
459 LanguageTag::parse(lang_part).map(|lt| (lt, quality))
460 })
461 .collect();
462
463 languages.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
465
466 Self { languages }
467 }
468
469 fn extract_quality(s: &str) -> (&str, f32) {
470 if let Some(q_pos) = find_quality_param(s) {
471 let lang_part = &s[..q_pos];
472 let q_part = &s[q_pos + 3..];
473
474 let quality = q_part.trim().parse::<f32>().unwrap_or(1.0).clamp(0.0, 1.0);
475
476 (lang_part, quality)
477 } else {
478 (s, 1.0)
479 }
480 }
481
482 pub fn quality_for(&self, language: &LanguageTag) -> f32 {
484 for (lt, quality) in &self.languages {
485 if lt.matches(language) {
486 return *quality;
487 }
488 }
489 0.0
490 }
491
492 pub fn preferred(&self) -> Option<&LanguageTag> {
494 self.languages.first().map(|(lt, _)| lt)
495 }
496}
497
498pub fn negotiate_language<'a>(
500 accept: &AcceptLanguage,
501 available: &'a [LanguageTag],
502) -> Option<&'a LanguageTag> {
503 let mut best: Option<(&'a LanguageTag, f32)> = None;
504
505 for available_lt in available {
506 let quality = accept.quality_for(available_lt);
507 if quality > 0.0 {
508 match &best {
509 None => best = Some((available_lt, quality)),
510 Some((_, best_q)) if quality > *best_q => {
511 best = Some((available_lt, quality));
512 }
513 _ => {}
514 }
515 }
516 }
517
518 best.map(|(lt, _)| lt)
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
527pub enum Encoding {
528 Gzip,
530 Deflate,
532 Brotli,
534 Zstd,
536 Identity,
538}
539
540impl Encoding {
541 pub fn parse(s: &str) -> Option<Self> {
543 match s.trim().to_lowercase().as_str() {
544 "gzip" | "x-gzip" => Some(Self::Gzip),
545 "deflate" => Some(Self::Deflate),
546 "br" => Some(Self::Brotli),
547 "zstd" => Some(Self::Zstd),
548 "identity" => Some(Self::Identity),
549 _ => None,
550 }
551 }
552
553 pub fn to_header_value(&self) -> &'static str {
555 match self {
556 Self::Gzip => "gzip",
557 Self::Deflate => "deflate",
558 Self::Brotli => "br",
559 Self::Zstd => "zstd",
560 Self::Identity => "identity",
561 }
562 }
563}
564
565impl fmt::Display for Encoding {
566 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
567 write!(f, "{}", self.to_header_value())
568 }
569}
570
571#[derive(Debug, Clone, Default)]
573pub struct AcceptEncoding {
574 pub encodings: Vec<(Encoding, f32)>,
576}
577
578impl AcceptEncoding {
579 pub fn parse(header: &str) -> Self {
590 let mut encodings: Vec<(Encoding, f32)> = header
591 .split(',')
592 .filter_map(|part| {
593 let part = part.trim();
594 if part.is_empty() {
595 return None;
596 }
597
598 let (enc_part, quality) = Self::extract_quality(part);
599 Encoding::parse(enc_part).map(|enc| (enc, quality))
600 })
601 .collect();
602
603 encodings.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
605
606 Self { encodings }
607 }
608
609 fn extract_quality(s: &str) -> (&str, f32) {
610 if let Some(q_pos) = find_quality_param(s) {
611 let enc_part = &s[..q_pos];
612 let q_part = &s[q_pos + 3..];
613
614 let quality = q_part.trim().parse::<f32>().unwrap_or(1.0).clamp(0.0, 1.0);
615
616 (enc_part, quality)
617 } else {
618 (s, 1.0)
619 }
620 }
621
622 pub fn quality_for(&self, encoding: Encoding) -> f32 {
624 for (enc, quality) in &self.encodings {
625 if *enc == encoding {
626 return *quality;
627 }
628 }
629 0.0
630 }
631
632 pub fn preferred(&self) -> Option<Encoding> {
634 self.encodings.first().map(|(enc, _)| *enc)
635 }
636
637 pub fn accepts(&self, encoding: Encoding) -> bool {
639 self.quality_for(encoding) > 0.0
640 }
641}
642
643pub fn negotiate_encoding(accept: &AcceptEncoding, available: &[Encoding]) -> Option<Encoding> {
645 let mut best: Option<(Encoding, f32)> = None;
646
647 for &enc in available {
648 let quality = accept.quality_for(enc);
649 if quality > 0.0 {
650 match &best {
651 None => best = Some((enc, quality)),
652 Some((_, best_q)) if quality > *best_q => {
653 best = Some((enc, quality));
654 }
655 _ => {}
656 }
657 }
658 }
659
660 best.map(|(enc, _)| enc)
661}
662
663#[derive(Debug, Clone, Default)]
669pub struct AcceptCharset {
670 pub charsets: Vec<(String, f32)>,
672}
673
674impl AcceptCharset {
675 pub fn parse(header: &str) -> Self {
677 let mut charsets: Vec<(String, f32)> = header
678 .split(',')
679 .filter_map(|part| {
680 let part = part.trim();
681 if part.is_empty() {
682 return None;
683 }
684
685 let (charset_part, quality) = Self::extract_quality(part);
686 Some((charset_part.trim().to_lowercase(), quality))
687 })
688 .collect();
689
690 charsets.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
692
693 Self { charsets }
694 }
695
696 fn extract_quality(s: &str) -> (&str, f32) {
697 if let Some(q_pos) = find_quality_param(s) {
698 let charset_part = &s[..q_pos];
699 let q_part = &s[q_pos + 3..];
700
701 let quality = q_part.trim().parse::<f32>().unwrap_or(1.0).clamp(0.0, 1.0);
702
703 (charset_part, quality)
704 } else {
705 (s, 1.0)
706 }
707 }
708
709 pub fn quality_for(&self, charset: &str) -> f32 {
711 let charset = charset.to_lowercase();
712 for (cs, quality) in &self.charsets {
713 if cs == &charset || cs == "*" {
714 return *quality;
715 }
716 }
717 if charset == "utf-8" {
719 return 1.0;
720 }
721 0.0
722 }
723
724 pub fn preferred(&self) -> Option<&str> {
726 self.charsets.first().map(|(cs, _)| cs.as_str())
727 }
728}
729
730impl HttpRequest {
736 pub fn accept(&self) -> Accept {
738 self.headers
739 .get("Accept")
740 .or_else(|| self.headers.get("accept"))
741 .map(Accept::parse)
742 .unwrap_or_default()
743 }
744
745 pub fn accept_language(&self) -> AcceptLanguage {
747 self.headers
748 .get("Accept-Language")
749 .or_else(|| self.headers.get("accept-language"))
750 .map(AcceptLanguage::parse)
751 .unwrap_or_default()
752 }
753
754 pub fn accept_encoding(&self) -> AcceptEncoding {
756 self.headers
757 .get("Accept-Encoding")
758 .or_else(|| self.headers.get("accept-encoding"))
759 .map(AcceptEncoding::parse)
760 .unwrap_or_default()
761 }
762
763 pub fn accept_charset(&self) -> AcceptCharset {
765 self.headers
766 .get("Accept-Charset")
767 .or_else(|| self.headers.get("accept-charset"))
768 .map(AcceptCharset::parse)
769 .unwrap_or_default()
770 }
771
772 pub fn accepts(&self, media_type: &MediaType) -> bool {
774 self.accept().accepts(media_type)
775 }
776
777 pub fn prefers_json(&self) -> bool {
779 self.accept().prefers_json()
780 }
781
782 pub fn prefers_html(&self) -> bool {
784 self.accept().prefers_html()
785 }
786
787 pub fn negotiate_media_type<'a>(&self, available: &'a [MediaType]) -> Option<&'a MediaType> {
789 negotiate_media_type(&self.accept(), available)
790 }
791
792 pub fn negotiate_language<'a>(&self, available: &'a [LanguageTag]) -> Option<&'a LanguageTag> {
794 negotiate_language(&self.accept_language(), available)
795 }
796
797 pub fn negotiate_encoding(&self, available: &[Encoding]) -> Option<Encoding> {
799 negotiate_encoding(&self.accept_encoding(), available)
800 }
801}
802
803pub struct ContentNegotiator<J, H, T, X>
822where
823 J: FnOnce() -> serde_json::Value,
824 H: FnOnce() -> String,
825 T: FnOnce() -> String,
826 X: FnOnce() -> String,
827{
828 json_fn: Option<J>,
829 html_fn: Option<H>,
830 text_fn: Option<T>,
831 xml_fn: Option<X>,
832 default_media_type: MediaType,
833}
834
835impl ContentNegotiator<fn() -> serde_json::Value, fn() -> String, fn() -> String, fn() -> String> {
836 pub fn new() -> Self {
838 Self {
839 json_fn: None,
840 html_fn: None,
841 text_fn: None,
842 xml_fn: None,
843 default_media_type: MediaType::json(),
844 }
845 }
846}
847
848impl<J, H, T, X> ContentNegotiator<J, H, T, X>
849where
850 J: FnOnce() -> serde_json::Value,
851 H: FnOnce() -> String,
852 T: FnOnce() -> String,
853 X: FnOnce() -> String,
854{
855 pub fn json<NJ: FnOnce() -> serde_json::Value>(self, f: NJ) -> ContentNegotiator<NJ, H, T, X> {
857 ContentNegotiator {
858 json_fn: Some(f),
859 html_fn: self.html_fn,
860 text_fn: self.text_fn,
861 xml_fn: self.xml_fn,
862 default_media_type: self.default_media_type,
863 }
864 }
865
866 pub fn html<NH: FnOnce() -> String>(self, f: NH) -> ContentNegotiator<J, NH, T, X> {
868 ContentNegotiator {
869 json_fn: self.json_fn,
870 html_fn: Some(f),
871 text_fn: self.text_fn,
872 xml_fn: self.xml_fn,
873 default_media_type: self.default_media_type,
874 }
875 }
876
877 pub fn plain_text<NT: FnOnce() -> String>(self, f: NT) -> ContentNegotiator<J, H, NT, X> {
879 ContentNegotiator {
880 json_fn: self.json_fn,
881 html_fn: self.html_fn,
882 text_fn: Some(f),
883 xml_fn: self.xml_fn,
884 default_media_type: self.default_media_type,
885 }
886 }
887
888 pub fn xml<NX: FnOnce() -> String>(self, f: NX) -> ContentNegotiator<J, H, T, NX> {
890 ContentNegotiator {
891 json_fn: self.json_fn,
892 html_fn: self.html_fn,
893 text_fn: self.text_fn,
894 xml_fn: Some(f),
895 default_media_type: self.default_media_type,
896 }
897 }
898
899 pub fn default_to(mut self, media_type: MediaType) -> Self {
901 self.default_media_type = media_type;
902 self
903 }
904
905 pub fn negotiate(self, request: &HttpRequest) -> Result<HttpResponse, Error> {
907 let accept = request.accept();
908
909 let mut available = Vec::new();
911 if self.json_fn.is_some() {
912 available.push(MediaType::json());
913 }
914 if self.html_fn.is_some() {
915 available.push(MediaType::html());
916 }
917 if self.text_fn.is_some() {
918 available.push(MediaType::plain_text());
919 }
920 if self.xml_fn.is_some() {
921 available.push(MediaType::xml());
922 }
923
924 if available.is_empty() {
926 return Err(Error::Internal(
927 "No response formats configured".to_string(),
928 ));
929 }
930
931 let best = negotiate_media_type(&accept, &available)
933 .cloned()
934 .unwrap_or_else(|| self.default_media_type.clone());
935
936 let mut response = HttpResponse::ok();
938
939 if best.matches(&MediaType::json()) {
940 if let Some(f) = self.json_fn {
941 let value = f();
942 let body =
943 serde_json::to_vec(&value).map_err(|e| Error::Serialization(e.to_string()))?;
944 response.body = Bytes::from(body);
945 response
946 .headers
947 .insert("Content-Type".to_string(), "application/json".to_string());
948 }
949 } else if best.matches(&MediaType::html()) {
950 if let Some(f) = self.html_fn {
951 let html = f();
952 response.body = Bytes::from(html.into_bytes());
953 response.headers.insert(
954 "Content-Type".to_string(),
955 "text/html; charset=utf-8".to_string(),
956 );
957 }
958 } else if best.matches(&MediaType::plain_text()) {
959 if let Some(f) = self.text_fn {
960 let text = f();
961 response.body = Bytes::from(text.into_bytes());
962 response.headers.insert(
963 "Content-Type".to_string(),
964 "text/plain; charset=utf-8".to_string(),
965 );
966 }
967 } else if best.matches(&MediaType::xml()) {
968 if let Some(f) = self.xml_fn {
969 let xml = f();
970 response.body = Bytes::from(xml.into_bytes());
971 response.headers.insert(
972 "Content-Type".to_string(),
973 "application/xml; charset=utf-8".to_string(),
974 );
975 }
976 } else {
977 return Err(Error::NotAcceptable(format!(
978 "Cannot produce response in requested format: {}",
979 best
980 )));
981 }
982
983 response
985 .headers
986 .insert("Vary".to_string(), "Accept".to_string());
987
988 Ok(response)
989 }
990}
991
992impl Default
993 for ContentNegotiator<fn() -> serde_json::Value, fn() -> String, fn() -> String, fn() -> String>
994{
995 fn default() -> Self {
996 Self::new()
997 }
998}
999
1000pub fn respond_with<T: Serialize>(request: &HttpRequest, data: &T) -> Result<HttpResponse, Error> {
1017 let accept = request.accept();
1018
1019 let mut response = HttpResponse::ok();
1020
1021 if accept.prefers_html() {
1022 let json =
1024 serde_json::to_string_pretty(data).map_err(|e| Error::Serialization(e.to_string()))?;
1025 let html = format!(
1026 "<!DOCTYPE html><html><body><pre>{}</pre></body></html>",
1027 html_escape(&json)
1028 );
1029 response.body = Bytes::from(html.into_bytes());
1030 response.headers.insert(
1031 "Content-Type".to_string(),
1032 "text/html; charset=utf-8".to_string(),
1033 );
1034 } else {
1035 response.body =
1037 Bytes::from(serde_json::to_vec(data).map_err(|e| Error::Serialization(e.to_string()))?);
1038 response
1039 .headers
1040 .insert("Content-Type".to_string(), "application/json".to_string());
1041 }
1042
1043 response
1044 .headers
1045 .insert("Vary".to_string(), "Accept".to_string());
1046
1047 Ok(response)
1048}
1049
1050fn html_escape(s: &str) -> String {
1052 s.replace('&', "&")
1053 .replace('<', "<")
1054 .replace('>', ">")
1055 .replace('"', """)
1056 .replace('\'', "'")
1057}
1058
1059#[cfg(test)]
1064mod tests {
1065 use super::*;
1066
1067 #[test]
1068 fn test_media_type_parse() {
1069 let mt = MediaType::parse("application/json").unwrap();
1070 assert_eq!(mt.type_, "application");
1071 assert_eq!(mt.subtype, "json");
1072 }
1073
1074 #[test]
1075 fn test_media_type_with_params() {
1076 let mt = MediaType::parse("text/html; charset=utf-8").unwrap();
1077 assert_eq!(mt.type_, "text");
1078 assert_eq!(mt.subtype, "html");
1079 assert_eq!(mt.params.get("charset").map(String::as_str), Some("utf-8"));
1080 }
1081
1082 #[test]
1083 fn test_media_type_matches() {
1084 let json = MediaType::json();
1085 let any = MediaType::any();
1086 let html = MediaType::html();
1087
1088 assert!(any.matches(&json));
1089 assert!(json.matches(&any));
1090 assert!(!json.matches(&html));
1091 }
1092
1093 #[test]
1094 fn test_accept_parse() {
1095 let accept = Accept::parse("application/json, text/html;q=0.9, */*;q=0.1");
1096 assert_eq!(accept.media_types.len(), 3);
1097
1098 assert_eq!(accept.media_types[0].0.subtype, "json");
1100 assert_eq!(accept.media_types[0].1, 1.0);
1101
1102 assert_eq!(accept.media_types[1].0.subtype, "html");
1104 assert_eq!(accept.media_types[1].1, 0.9);
1105 }
1106
1107 #[test]
1108 fn test_accept_quality_for() {
1109 let accept = Accept::parse("application/json, text/html;q=0.9");
1110
1111 assert_eq!(accept.quality_for(&MediaType::json()), 1.0);
1112 assert_eq!(accept.quality_for(&MediaType::html()), 0.9);
1113 assert_eq!(accept.quality_for(&MediaType::xml()), 0.0);
1114 }
1115
1116 #[test]
1117 fn test_extract_quality_case_insensitive() {
1118 let accept = Accept::parse("text/html;Q=0.8");
1120 assert_eq!(accept.quality_for(&MediaType::html()), 0.8);
1121 }
1122
1123 #[test]
1124 fn test_extract_quality_non_ascii_no_panic() {
1125 let accept = Accept::parse("application/json\u{212A}\u{212A};q=0.5");
1129 assert_eq!(accept.media_types.len(), 1);
1130 assert_eq!(accept.media_types[0].1, 0.5);
1131
1132 let accept_lang = AcceptLanguage::parse("en\u{212A}\u{212A};q=0.5");
1133 assert_eq!(accept_lang.languages[0].1, 0.5);
1134
1135 let accept_charset = AcceptCharset::parse("utf\u{212A}\u{212A};q=0.5");
1136 assert_eq!(accept_charset.charsets[0].1, 0.5);
1137
1138 let _ = AcceptEncoding::parse("gzip\u{212A}\u{212A};q=0.5");
1140 }
1141
1142 #[test]
1143 fn test_accept_prefers_json() {
1144 let accept = Accept::parse("application/json, text/html;q=0.9");
1145 assert!(accept.prefers_json());
1146 assert!(!accept.prefers_html());
1147 }
1148
1149 #[test]
1150 fn test_accept_prefers_html() {
1151 let accept = Accept::parse("text/html, application/json;q=0.9");
1152 assert!(accept.prefers_html());
1153 assert!(!accept.prefers_json());
1154 }
1155
1156 #[test]
1157 fn test_negotiate_media_type() {
1158 let accept = Accept::parse("application/json, text/html;q=0.9");
1159 let available = vec![MediaType::html(), MediaType::json()];
1160
1161 let best = negotiate_media_type(&accept, &available);
1162 assert_eq!(best, Some(&MediaType::json()));
1163 }
1164
1165 #[test]
1166 fn test_language_tag_parse() {
1167 let tag = LanguageTag::parse("en-US").unwrap();
1168 assert_eq!(tag.primary, "en");
1169 assert_eq!(tag.subtag, Some("US".to_string()));
1170 }
1171
1172 #[test]
1173 fn test_language_tag_matches() {
1174 let en = LanguageTag::new("en");
1175 let en_us = LanguageTag::with_subtag("en", "US");
1176 let fr = LanguageTag::new("fr");
1177
1178 assert!(en.matches(&en_us)); assert!(!en_us.matches(&en)); assert!(!en.matches(&fr));
1181 }
1182
1183 #[test]
1184 fn test_accept_language_parse() {
1185 let accept = AcceptLanguage::parse("en-US, en;q=0.9, fr;q=0.8");
1186 assert_eq!(accept.languages.len(), 3);
1187 assert_eq!(accept.languages[0].0.primary, "en");
1188 }
1189
1190 #[test]
1191 fn test_encoding_parse() {
1192 assert_eq!(Encoding::parse("gzip"), Some(Encoding::Gzip));
1193 assert_eq!(Encoding::parse("br"), Some(Encoding::Brotli));
1194 assert_eq!(Encoding::parse("deflate"), Some(Encoding::Deflate));
1195 }
1196
1197 #[test]
1198 fn test_accept_encoding_parse() {
1199 let accept = AcceptEncoding::parse("gzip, deflate, br;q=0.9");
1200 assert_eq!(accept.encodings.len(), 3);
1201 }
1202
1203 #[test]
1204 fn test_accept_charset_parse() {
1205 let accept = AcceptCharset::parse("utf-8, iso-8859-1;q=0.8");
1206 assert_eq!(accept.charsets.len(), 2);
1207 assert_eq!(accept.quality_for("utf-8"), 1.0);
1208 }
1209
1210 #[test]
1211 fn test_http_request_accept() {
1212 let mut request = HttpRequest::new("GET", "/".to_string());
1213 request
1214 .headers
1215 .insert("Accept", "application/json".to_string());
1216
1217 let accept = request.accept();
1218 assert!(accept.accepts(&MediaType::json()));
1219 }
1220
1221 #[test]
1222 fn test_http_request_prefers_json() {
1223 let mut request = HttpRequest::new("GET", "/".to_string());
1224 request
1225 .headers
1226 .insert("Accept", "application/json, text/html;q=0.9".to_string());
1227
1228 assert!(request.prefers_json());
1229 assert!(!request.prefers_html());
1230 }
1231}