http_types/content/
media_type_proposal.rs

1use crate::ensure;
2use crate::headers::HeaderValue;
3use crate::Mime;
4
5use std::ops::{Deref, DerefMut};
6use std::{
7    cmp::{Ordering, PartialEq},
8    str::FromStr,
9};
10
11/// A proposed Media Type for the `Accept` header.
12#[derive(Debug, Clone, PartialEq)]
13pub struct MediaTypeProposal {
14    /// The proposed media_type.
15    pub(crate) media_type: Mime,
16
17    /// The weight of the proposal.
18    ///
19    /// This is a number between 0.0 and 1.0, and is max 3 decimal points.
20    weight: Option<f32>,
21}
22
23impl MediaTypeProposal {
24    /// Create a new instance of `MediaTypeProposal`.
25    pub fn new(media_type: impl Into<Mime>, weight: Option<f32>) -> crate::Result<Self> {
26        if let Some(weight) = weight {
27            ensure!(
28                weight.is_sign_positive() && weight <= 1.0,
29                "MediaTypeProposal should have a weight between 0.0 and 1.0"
30            )
31        }
32
33        Ok(Self {
34            media_type: media_type.into(),
35            weight,
36        })
37    }
38
39    /// Get the proposed media_type.
40    pub fn media_type(&self) -> &Mime {
41        &self.media_type
42    }
43
44    /// Get the weight of the proposal.
45    pub fn weight(&self) -> Option<f32> {
46        self.weight
47    }
48
49    /// Parse a string into a media type proposal.
50    ///
51    /// Because `;` and `q=0.0` are all valid values for in use in a media type,
52    /// we have to parse the full string to the media type first, and then see if
53    /// a `q` value has been set.
54    pub(crate) fn from_str(s: &str) -> crate::Result<Self> {
55        let mut media_type = Mime::from_str(s)?;
56        let weight = media_type
57            .remove_param("q")
58            .map(|param| param.as_str().parse())
59            .transpose()?;
60        Self::new(media_type, weight)
61    }
62}
63
64impl From<Mime> for MediaTypeProposal {
65    fn from(media_type: Mime) -> Self {
66        Self {
67            media_type,
68            weight: None,
69        }
70    }
71}
72
73impl From<MediaTypeProposal> for Mime {
74    fn from(accept: MediaTypeProposal) -> Self {
75        accept.media_type
76    }
77}
78
79impl PartialEq<Mime> for MediaTypeProposal {
80    fn eq(&self, other: &Mime) -> bool {
81        self.media_type == *other
82    }
83}
84
85impl PartialEq<Mime> for &MediaTypeProposal {
86    fn eq(&self, other: &Mime) -> bool {
87        self.media_type == *other
88    }
89}
90
91impl Deref for MediaTypeProposal {
92    type Target = Mime;
93    fn deref(&self) -> &Self::Target {
94        &self.media_type
95    }
96}
97
98impl DerefMut for MediaTypeProposal {
99    fn deref_mut(&mut self) -> &mut Self::Target {
100        &mut self.media_type
101    }
102}
103
104// NOTE: For Accept-Encoding Firefox sends the values: `gzip, deflate, br`. This means
105// when parsing media_types we should choose the last value in the list under
106// equal weights. This impl doesn't know which value was passed later, so that
107// behavior needs to be handled separately.
108//
109// NOTE: This comparison does not include a notion of `*` (any value is valid).
110// that needs to be handled separately.
111impl PartialOrd for MediaTypeProposal {
112    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
113        match (self.weight, other.weight) {
114            (Some(left), Some(right)) => left.partial_cmp(&right),
115            (Some(_), None) => Some(Ordering::Greater),
116            (None, Some(_)) => Some(Ordering::Less),
117            (None, None) => None,
118        }
119    }
120}
121
122impl From<MediaTypeProposal> for HeaderValue {
123    fn from(entry: MediaTypeProposal) -> HeaderValue {
124        let s = match entry.weight {
125            Some(weight) => format!("{};q={:.3}", entry.media_type, weight),
126            None => entry.media_type.to_string(),
127        };
128        unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) }
129    }
130}
131
132#[cfg(test)]
133mod test {
134    use super::*;
135    use crate::mime;
136
137    #[test]
138    fn smoke() {
139        let _ = MediaTypeProposal::new(mime::JSON, Some(0.0)).unwrap();
140        let _ = MediaTypeProposal::new(mime::XML, Some(0.5)).unwrap();
141        let _ = MediaTypeProposal::new(mime::HTML, Some(1.0)).unwrap();
142    }
143
144    #[test]
145    fn error_code_500() {
146        let err = MediaTypeProposal::new(mime::JSON, Some(1.1)).unwrap_err();
147        assert_eq!(err.status(), 500);
148
149        let err = MediaTypeProposal::new(mime::XML, Some(-0.1)).unwrap_err();
150        assert_eq!(err.status(), 500);
151
152        let err = MediaTypeProposal::new(mime::HTML, Some(-0.0)).unwrap_err();
153        assert_eq!(err.status(), 500);
154    }
155}