actix_http/header/shared/quality.rs
1use std::fmt;
2
3use derive_more::{Display, Error};
4
5const MAX_QUALITY_INT: u16 = 1000;
6const MAX_QUALITY_FLOAT: f32 = 1.0;
7
8/// Represents a quality used in q-factor values.
9///
10/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
11///
12/// # Implementation notes
13/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
14/// This means there are 1001 possible values. Since floating point numbers are not exact and the
15/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
16/// the quality internally.
17///
18/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
19///
20/// # Examples
21/// ```
22/// use actix_http::header::{Quality, q};
23/// assert_eq!(q(1.0), Quality::MAX);
24///
25/// assert_eq!(q(0.42).to_string(), "0.42");
26/// assert_eq!(q(1.0).to_string(), "1");
27/// assert_eq!(Quality::MIN.to_string(), "0.001");
28/// assert_eq!(Quality::ZERO.to_string(), "0");
29/// ```
30///
31/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
33pub struct Quality(pub(super) u16);
34
35impl Quality {
36 /// The maximum quality value, equivalent to `q=1.0`.
37 pub const MAX: Quality = Quality(MAX_QUALITY_INT);
38
39 /// The minimum, non-zero quality value, equivalent to `q=0.001`.
40 pub const MIN: Quality = Quality(1);
41
42 /// The zero quality value, equivalent to `q=0.0`.
43 pub const ZERO: Quality = Quality(0);
44
45 /// Converts a float in the range 0.0–1.0 to a `Quality`.
46 ///
47 /// Intentionally private. External uses should rely on the `TryFrom` impl.
48 ///
49 /// # Panics
50 /// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
51 fn from_f32(value: f32) -> Self {
52 // Check that `value` is within range should be done before calling this method.
53 // Just in case, this debug_assert should catch if we were forgetful.
54 debug_assert!(
55 (0.0..=MAX_QUALITY_FLOAT).contains(&value),
56 "q value must be between 0.0 and 1.0"
57 );
58
59 Quality((value * MAX_QUALITY_INT as f32) as u16)
60 }
61}
62
63/// The default value is [`Quality::MAX`].
64impl Default for Quality {
65 fn default() -> Quality {
66 Quality::MAX
67 }
68}
69
70impl fmt::Display for Quality {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self.0 {
73 0 => f.write_str("0"),
74 MAX_QUALITY_INT => f.write_str("1"),
75
76 // some number in the range 1–999
77 x => {
78 f.write_str("0.")?;
79
80 // This implementation avoids string allocation for removing trailing zeroes.
81 // In benchmarks it is twice as fast as approach using something like
82 // `format!("{}").trim_end_matches('0')` for non-fast-path quality values.
83
84 if x < 10 {
85 // x in is range 1–9
86
87 f.write_str("00")?;
88
89 // 0 is already handled so it's not possible to have a trailing 0 in this range
90 // we can just write the integer
91 itoa_fmt(f, x)
92 } else if x < 100 {
93 // x in is range 10–99
94
95 f.write_str("0")?;
96
97 if x % 10 == 0 {
98 // trailing 0, divide by 10 and write
99 itoa_fmt(f, x / 10)
100 } else {
101 itoa_fmt(f, x)
102 }
103 } else {
104 // x is in range 100–999
105
106 if x % 100 == 0 {
107 // two trailing 0s, divide by 100 and write
108 itoa_fmt(f, x / 100)
109 } else if x % 10 == 0 {
110 // one trailing 0, divide by 10 and write
111 itoa_fmt(f, x / 10)
112 } else {
113 itoa_fmt(f, x)
114 }
115 }
116 }
117 }
118 }
119}
120
121/// Write integer to a `fmt::Write`.
122pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
123 let mut buf = itoa::Buffer::new();
124 wr.write_str(buf.format(value))
125}
126
127#[derive(Debug, Clone, Display, Error)]
128#[display("quality out of bounds")]
129#[non_exhaustive]
130pub struct QualityOutOfBounds;
131
132impl TryFrom<f32> for Quality {
133 type Error = QualityOutOfBounds;
134
135 #[inline]
136 fn try_from(value: f32) -> Result<Self, Self::Error> {
137 if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
138 Ok(Quality::from_f32(value))
139 } else {
140 Err(QualityOutOfBounds)
141 }
142 }
143}
144
145/// Convenience function to create a [`Quality`] from an `f32` (0.0–1.0).
146///
147/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
148///
149/// # Panics
150/// Panics if value is out of range.
151///
152/// # Examples
153/// ```
154/// # use actix_http::header::{q, Quality};
155/// let q1 = q(1.0);
156/// assert_eq!(q1, Quality::MAX);
157///
158/// let q2 = q(0.001);
159/// assert_eq!(q2, Quality::MIN);
160///
161/// let q3 = q(0.0);
162/// assert_eq!(q3, Quality::ZERO);
163///
164/// let q4 = q(0.42);
165/// ```
166///
167/// An out-of-range `f32` quality will panic.
168/// ```should_panic
169/// # use actix_http::header::q;
170/// let _q2 = q(1.42);
171/// ```
172#[inline]
173pub fn q<T>(quality: T) -> Quality
174where
175 T: TryInto<Quality>,
176 T::Error: fmt::Debug,
177{
178 quality.try_into().expect("quality value was out of bounds")
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn q_helper() {
187 assert_eq!(q(0.5), Quality(500));
188 }
189
190 #[test]
191 fn display_output() {
192 assert_eq!(Quality::ZERO.to_string(), "0");
193 assert_eq!(Quality::MIN.to_string(), "0.001");
194 assert_eq!(Quality::MAX.to_string(), "1");
195
196 assert_eq!(q(0.0).to_string(), "0");
197 assert_eq!(q(1.0).to_string(), "1");
198 assert_eq!(q(0.001).to_string(), "0.001");
199 assert_eq!(q(0.5).to_string(), "0.5");
200 assert_eq!(q(0.22).to_string(), "0.22");
201 assert_eq!(q(0.123).to_string(), "0.123");
202 assert_eq!(q(0.999).to_string(), "0.999");
203
204 for x in 0..=1000 {
205 // if trailing zeroes are handled correctly, we would not expect the serialized length
206 // to ever exceed "0." + 3 decimal places = 5 in length
207 assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
208 }
209 }
210
211 #[test]
212 #[should_panic]
213 fn negative_quality() {
214 q(-1.0);
215 }
216
217 #[test]
218 #[should_panic]
219 fn quality_out_of_bounds() {
220 q(2.0);
221 }
222}