accept_header/
media_type.rs1use mime::Mime;
2use snafu::{ensure, ResultExt};
3
4use crate::{error::*, MediaType};
5use std::{cmp::Ordering, fmt, str::FromStr};
6
7impl FromStr for MediaType {
8 type Err = Error;
9
10 fn from_str(s: &str) -> Result<Self, Self::Err> {
11 let mut parts = s.split(';');
12 let v = parts.next().unwrap().trim();
13 let mime = v.parse().context(MediaTypeSnafu { value: v })?;
14 let Some(v) = parts
15 .next()
16 .map(|s| s.trim())
17 .and_then(|s| s.strip_prefix("q=")) else {
18 return Ok(MediaType { mime, weight: None });
19 };
20
21 let weight: f32 = v.trim().parse().context(ParseWeightSnafu { value: v })?;
22 ensure!(
23 (0.0..=1.0).contains(&weight),
24 WeightRangeSnafu { value: weight }
25 );
26
27 Ok(MediaType {
28 mime,
29 weight: Some(weight),
30 })
31 }
32}
33
34impl From<Mime> for MediaType {
35 fn from(mime: Mime) -> Self {
36 Self { mime, weight: None }
37 }
38}
39
40impl From<MediaType> for Mime {
41 fn from(media_type: MediaType) -> Self {
42 media_type.mime
43 }
44}
45
46impl PartialEq<Mime> for MediaType {
47 fn eq(&self, other: &Mime) -> bool {
48 self.mime == *other
49 }
50}
51
52impl PartialEq<Mime> for &MediaType {
53 fn eq(&self, other: &Mime) -> bool {
54 self.mime == *other
55 }
56}
57
58impl PartialOrd for MediaType {
59 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
60 let aweight = self.weight.unwrap_or(1.0);
61 let bweight = other.weight.unwrap_or(1.0);
62 match aweight.partial_cmp(&bweight) {
63 Some(Ordering::Equal) => match (self.mime.type_(), other.mime.type_()) {
64 (mime::STAR, mime::STAR) => match (self.mime.subtype(), other.mime.subtype()) {
65 (mime::STAR, mime::STAR) => Some(Ordering::Equal),
66 (mime::STAR, _) => Some(Ordering::Less),
67 (_, mime::STAR) => Some(Ordering::Greater),
68 (_, _) => None,
69 },
70 (mime::STAR, _) => Some(Ordering::Less),
71 (_, mime::STAR) => Some(Ordering::Greater),
72 (_, _) => None,
73 },
74 v => v,
75 }
76 }
77}
78
79impl fmt::Display for MediaType {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}", self.mime)?;
82
83 if let Some(weight) = self.weight {
84 write!(f, ";q={weight}")?;
85 }
86
87 Ok(())
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn valid_media_type_should_be_parsed() {
97 let t1: MediaType = "text/html; q= 0.5 ".parse().unwrap();
98 let t2: MediaType = "application/json".parse().unwrap();
99 let t3 = "*/*; q=0.7".parse::<MediaType>().unwrap();
100 assert_eq!(t1.mime, Mime::from_str("text/html").unwrap());
101 assert_eq!(t1.mime.type_(), mime::TEXT);
102 assert_eq!(t1.mime.subtype(), mime::HTML);
103 assert_eq!(t2.mime.type_(), mime::APPLICATION);
104 assert_eq!(t2.mime.subtype(), mime::JSON);
105 assert_eq!(t1.weight, Some(0.5));
106 assert_eq!(t2.weight, None);
107 assert_eq!(t3.mime.type_(), mime::STAR);
108 }
109
110 #[test]
111 fn invalid_media_type_should_be_rejected() {
112 let t1 = "text/html; q=-0.5".parse::<MediaType>();
113 let t2 = "text/html; q=1.5".parse::<MediaType>();
114 let t3 = "text/html; q=abcd".parse::<MediaType>();
115
116 assert!(t1.is_err());
117 assert_eq!(
118 t2.unwrap_err().to_string(),
119 "Weight should be 0.0-1.0. Got 1.5"
120 );
121 assert_eq!(t3.unwrap_err().to_string(), "Invalid weight: abcd");
122 }
123
124 #[test]
125 fn media_type_should_be_comparable() {
126 let t1: MediaType = "text/html; q= 0.5 ".parse().unwrap();
127 let t2: MediaType = "application/json".parse().unwrap();
128 let t3: MediaType = "text/html".parse().unwrap();
129 assert!(t1 < t2);
130 assert!(t1 < t3);
131 assert!(t2 != t3);
132 }
133
134 #[test]
135 fn media_type_to_string_should_work() {
136 let t1: MediaType = "text/html; q= 0.5 ".parse().unwrap();
137 let t2: MediaType = "application/json".parse().unwrap();
138 let t3: MediaType = "text/html".parse().unwrap();
139 assert_eq!(t1.to_string(), "text/html;q=0.5");
140 assert_eq!(t2.to_string(), "application/json");
141 assert_eq!(t3.to_string(), "text/html");
142 }
143}