http_types/content/
encoding_proposal.rs

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