http_types/transfer/
encoding_proposal.rs

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