accept_header/
media_type.rs

1use 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}