typed_headers/impls/
quality.rs

1use std::fmt;
2use std::slice;
3use std::str::{self, FromStr};
4
5/// A value paired with its "quality" as defined in [RFC7231].
6///
7/// Quality items are used in content negotiation headers such as `Accept` and `Accept-Encoding`.
8///
9/// [RFC7231]: https://tools.ietf.org/html/rfc7231#section-5.3
10#[derive(Debug, Clone, PartialEq)]
11pub struct QualityItem<T> {
12    pub item: T,
13    pub quality: Quality,
14}
15
16impl<T> QualityItem<T> {
17    /// Creates a new quality item.
18    pub fn new(item: T, quality: Quality) -> QualityItem<T> {
19        QualityItem { item, quality }
20    }
21}
22
23impl<T> fmt::Display for QualityItem<T>
24where
25    T: fmt::Display,
26{
27    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
28        fmt::Display::fmt(&self.item, fmt)?;
29        match self.quality.0 {
30            1000 => Ok(()),
31            0 => fmt.write_str("; q=0"),
32            mut x => {
33                fmt.write_str("; q=0.")?;
34                let mut digits = *b"000";
35                digits[2] = (x % 10) as u8 + b'0';
36                x /= 10;
37                digits[1] = (x % 10) as u8 + b'0';
38                x /= 10;
39                digits[0] = (x % 10) as u8 + b'0';
40
41                let s = unsafe { str::from_utf8_unchecked(&digits[..]) };
42                fmt.write_str(s.trim_end_matches('0'))
43            }
44        }
45    }
46}
47
48impl<T> FromStr for QualityItem<T>
49where
50    T: FromStr,
51{
52    type Err = T::Err;
53
54    fn from_str(mut s: &str) -> Result<QualityItem<T>, T::Err> {
55        let quality = match WeightParser::parse(s) {
56            Some((remaining, quality)) => {
57                s = &s[..remaining];
58                quality
59            }
60            None => Quality(1000),
61        };
62
63        let item = s.parse()?;
64
65        Ok(QualityItem { item, quality })
66    }
67}
68
69struct WeightParser<'a>(slice::Iter<'a, u8>);
70
71impl<'a> WeightParser<'a> {
72    fn parse(s: &'a str) -> Option<(usize, Quality)> {
73        let mut parser = WeightParser(s.as_bytes().iter());
74        let qvalue = parser.qvalue()?;
75        parser.eat(b'=')?;
76        parser.eat(b'q').or_else(|| parser.eat(b'Q'))?;
77        parser.ows();
78        parser.eat(b';')?;
79        parser.ows();
80        let remaining = parser.0.as_slice().len();
81        Some((remaining, Quality(qvalue)))
82    }
83
84    fn qvalue(&mut self) -> Option<u16> {
85        let mut qvalue = match self.digit() {
86            Some(v @ 0) | Some(v @ 1) if self.peek() == Some(b'=') => return Some(v * 1000),
87            Some(v) => v,
88            None if self.peek() == Some(b'.') => 0,
89            None => return None,
90        };
91
92        match self.digit() {
93            Some(digit1) => match self.digit() {
94                Some(digit2) => qvalue += digit1 * 10 + digit2 * 100,
95                None => {
96                    qvalue *= 10;
97                    qvalue += digit1 * 100;
98                }
99            },
100            None => qvalue *= 100,
101        }
102
103        self.eat(b'.')?;
104
105        match self.peek()? {
106            b'0' => {
107                self.next();
108                Some(qvalue)
109            }
110            b'1' if qvalue == 0 => {
111                self.next();
112                Some(1000)
113            }
114            _ => None,
115        }
116    }
117
118    fn digit(&mut self) -> Option<u16> {
119        match self.peek()? {
120            v @ b'0'..=b'9' => {
121                self.next();
122                Some((v - b'0') as u16)
123            }
124            _ => None,
125        }
126    }
127
128    fn ows(&mut self) {
129        loop {
130            match self.peek() {
131                Some(b' ') | Some(b'\t') => {
132                    self.next();
133                }
134                _ => break,
135            }
136        }
137    }
138
139    fn peek(&self) -> Option<u8> {
140        self.0.clone().next_back().cloned()
141    }
142
143    fn next(&mut self) -> Option<u8> {
144        self.0.next_back().cloned()
145    }
146
147    fn eat(&mut self, value: u8) -> Option<()> {
148        if self.peek() == Some(value) {
149            self.next();
150            Some(())
151        } else {
152            None
153        }
154    }
155}
156
157/// A quality value, as specified in [RFC7231].
158///
159/// Quality values are decimal numbers between 0 and 1 (inclusive) with up to 3 fractional digits of precision.
160///
161/// [RFC7231]: https://tools.ietf.org/html/rfc7231#section-5.3.1
162#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
163pub struct Quality(u16);
164
165impl Quality {
166    /// Creates a quality value from a value between 0 and 1000 inclusive.
167    ///
168    /// This is semantically divided by 1000 to produce a value between 0 and 1.
169    ///
170    /// # Panics
171    ///
172    /// Panics if the value is greater than 1000.
173    pub fn from_u16(quality: u16) -> Quality {
174        assert!(quality <= 1000);
175        Quality(quality)
176    }
177
178    /// Returns the quality multiplied by 1000 as an integer.
179    pub fn as_u16(&self) -> u16 {
180        self.0
181    }
182}
183
184#[cfg(test)]
185mod test {
186    use super::*;
187    use crate::Error;
188
189    #[derive(Debug, Clone, PartialEq)]
190    struct Item;
191
192    impl fmt::Display for Item {
193        fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
194            fmt.write_str("item")
195        }
196    }
197
198    impl FromStr for Item {
199        type Err = Error;
200
201        fn from_str(s: &str) -> Result<Item, Error> {
202            if s == "item" {
203                Ok(Item)
204            } else {
205                Err(Error::invalid_value())
206            }
207        }
208    }
209
210    fn qitem(quality: u16) -> QualityItem<Item> {
211        QualityItem {
212            item: Item,
213            quality: Quality(quality),
214        }
215    }
216
217    #[test]
218    fn parse_ok() {
219        assert_eq!(qitem(1000), "item".parse().unwrap());
220        assert_eq!(qitem(1000), "item; q=1".parse().unwrap());
221        assert_eq!(qitem(1000), "item; Q=1".parse().unwrap());
222        assert_eq!(qitem(1000), "item ;q=1".parse().unwrap());
223        assert_eq!(qitem(1000), "item; q=1.".parse().unwrap());
224        assert_eq!(qitem(1000), "item; q=1.0".parse().unwrap());
225        assert_eq!(qitem(1000), "item; q=1.00".parse().unwrap());
226        assert_eq!(qitem(1000), "item; q=1.000".parse().unwrap());
227
228        assert_eq!(qitem(0), "item; q=0".parse().unwrap());
229        assert_eq!(qitem(0), "item; q=0.".parse().unwrap());
230        assert_eq!(qitem(0), "item; q=0.0".parse().unwrap());
231        assert_eq!(qitem(0), "item; q=0.00".parse().unwrap());
232        assert_eq!(qitem(0), "item; q=0.000".parse().unwrap());
233
234        assert_eq!(qitem(100), "item; q=0.1".parse().unwrap());
235        assert_eq!(qitem(100), "item; q=0.10".parse().unwrap());
236        assert_eq!(qitem(100), "item; q=0.100".parse().unwrap());
237        assert_eq!(qitem(120), "item; q=0.12".parse().unwrap());
238        assert_eq!(qitem(120), "item; q=0.120".parse().unwrap());
239        assert_eq!(qitem(123), "item; q=0.123".parse().unwrap());
240    }
241
242    #[test]
243    fn parse_err() {
244        assert!("item; q=".parse::<QualityItem<Item>>().is_err());
245        assert!("item; q=.1".parse::<QualityItem<Item>>().is_err());
246        assert!("item; q=1.1".parse::<QualityItem<Item>>().is_err());
247        assert!("item; q=1.01".parse::<QualityItem<Item>>().is_err());
248        assert!("item; q=1.001".parse::<QualityItem<Item>>().is_err());
249        assert!("item; q=0.0001".parse::<QualityItem<Item>>().is_err());
250    }
251
252    #[test]
253    fn display() {
254        assert_eq!(qitem(1000).to_string(), "item");
255        assert_eq!(qitem(0).to_string(), "item; q=0");
256        assert_eq!(qitem(1).to_string(), "item; q=0.001");
257        assert_eq!(qitem(10).to_string(), "item; q=0.01");
258        assert_eq!(qitem(100).to_string(), "item; q=0.1");
259    }
260}