rocket_http_community/header/
accept.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::ops::Deref;
4use std::str::FromStr;
5
6use crate::parse::parse_accept;
7use crate::{Header, MediaType};
8
9/// The HTTP Accept header.
10///
11/// An `Accept` header is composed of zero or more media types, each of which
12/// may have an optional quality value (a [`QMediaType`]). The header is sent by
13/// an HTTP client to describe the formats it accepts as well as the order in
14/// which it prefers different formats.
15///
16/// # Usage
17///
18/// The Accept header of an incoming request can be retrieved via the
19/// [`Request::accept()`] method. The [`preferred()`] method can be used to
20/// retrieve the client's preferred media type.
21///
22/// [`Request::accept()`]: rocket::Request::accept()
23/// [`preferred()`]: Accept::preferred()
24///
25/// An `Accept` type with a single, common media type can be easily constructed
26/// via provided associated constants.
27///
28/// ## Example
29///
30/// Construct an `Accept` header with a single `application/json` media type:
31///
32/// ```rust
33/// # extern crate rocket;
34/// use rocket::http::Accept;
35///
36/// # #[allow(unused_variables)]
37/// let accept_json = Accept::JSON;
38/// ```
39///
40/// # Header
41///
42/// `Accept` implements `Into<Header>`. As such, it can be used in any context
43/// where an `Into<Header>` is expected:
44///
45/// ```rust
46/// # extern crate rocket;
47/// use rocket::http::Accept;
48/// use rocket::response::Response;
49///
50/// let response = Response::build().header(Accept::JSON).finalize();
51/// ```
52#[derive(Debug, Clone)]
53pub struct Accept(pub(crate) Cow<'static, [QMediaType]>);
54
55/// A `MediaType` with an associated quality value.
56#[derive(Debug, Clone, PartialEq)]
57pub struct QMediaType(pub MediaType, pub Option<f32>);
58
59macro_rules! accept_constructor {
60    ($($name:ident ($check:ident): $str:expr, $t:expr,
61        $s:expr $(; $k:expr => $v:expr)*,)+) => {
62        $(
63            #[doc="An `Accept` header with the single media type for"]
64            #[doc=concat!("**", $str, "**: ", "_", $t, "/", $s, "_")]
65            #[allow(non_upper_case_globals)]
66            pub const $name: Accept = Accept({
67                const INNER: &[QMediaType] = &[QMediaType(MediaType::$name, None)];
68                Cow::Borrowed(INNER)
69            });
70         )+
71    };
72}
73
74impl Accept {
75    /// Constructs a new `Accept` header from one or more media types.
76    ///
77    /// The `items` parameter may be of type `QMediaType`, `[QMediaType]`,
78    /// `&[QMediaType]` or `Vec<QMediaType>`. To prevent additional allocations,
79    /// prefer to provide inputs of type `QMediaType`, `[QMediaType]`, or
80    /// `Vec<QMediaType>`.
81    ///
82    /// # Example
83    ///
84    /// ```rust
85    /// # extern crate rocket;
86    /// use rocket::http::{QMediaType, MediaType, Accept};
87    ///
88    /// // Construct an `Accept` via a `Vec<MediaType>`.
89    /// let json_then_html = vec![MediaType::JSON, MediaType::HTML];
90    /// let accept = Accept::new(json_then_html);
91    /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON);
92    ///
93    /// // Construct an `Accept` via an `[MediaType]`.
94    /// let accept = Accept::new([MediaType::JSON, MediaType::HTML]);
95    /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON);
96    ///
97    /// // Construct an `Accept` via a single `QMediaType`.
98    /// let accept = Accept::new(QMediaType(MediaType::JSON, Some(0.4)));
99    /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON);
100    /// ```
101    #[inline(always)]
102    pub fn new<T: IntoIterator<Item = M>, M: Into<QMediaType>>(items: T) -> Accept {
103        Accept(items.into_iter().map(|v| v.into()).collect())
104    }
105
106    /// Adds `media_type` to `self`.
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// # extern crate rocket;
112    /// use rocket::http::{QMediaType, MediaType, Accept};
113    ///
114    /// let mut accept = Accept::new(QMediaType(MediaType::JSON, Some(0.1)));
115    /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON);
116    /// assert_eq!(accept.iter().count(), 1);
117    ///
118    /// accept.add(QMediaType(MediaType::HTML, Some(0.7)));
119    /// assert_eq!(accept.preferred().media_type(), &MediaType::HTML);
120    /// assert_eq!(accept.iter().count(), 2);
121    ///
122    /// accept.add(QMediaType(MediaType::XML, Some(0.6)));
123    /// assert_eq!(accept.preferred().media_type(), &MediaType::HTML);
124    /// assert_eq!(accept.iter().count(), 3);
125    /// ```
126    #[inline(always)]
127    pub fn add<M: Into<QMediaType>>(&mut self, media_type: M) {
128        self.0.to_mut().push(media_type.into());
129    }
130
131    /// Retrieve the client's preferred media type. This method follows [RFC
132    /// 7231 5.3.2]. If the list of media types is empty, this method returns a
133    /// media type of any with no quality value: (`*/*`).
134    ///
135    /// [RFC 7231 5.3.2]: https://tools.ietf.org/html/rfc7231#section-5.3.2
136    ///
137    /// # Example
138    ///
139    /// ```rust
140    /// # extern crate rocket;
141    /// use rocket::http::{QMediaType, MediaType, Accept};
142    ///
143    /// let media_types = vec![
144    ///     QMediaType(MediaType::JSON, Some(0.3)),
145    ///     QMediaType(MediaType::HTML, Some(0.9))
146    /// ];
147    ///
148    /// let accept = Accept::new(media_types);
149    /// assert_eq!(accept.preferred().media_type(), &MediaType::HTML);
150    /// ```
151    pub fn preferred(&self) -> &QMediaType {
152        static ANY: QMediaType = QMediaType(MediaType::Any, None);
153
154        // See https://tools.ietf.org/html/rfc7231#section-5.3.2.
155        let mut all = self.iter();
156        let mut preferred = all.next().unwrap_or(&ANY);
157        for media_type in all {
158            if media_type.weight().is_none() && preferred.weight().is_some() {
159                // Media types without a `q` parameter are preferred.
160                preferred = media_type;
161            } else if media_type.weight_or(0.0) > preferred.weight_or(1.0) {
162                // Prefer media types with a greater weight, but if one doesn't
163                // have a weight, prefer the one we already have.
164                preferred = media_type;
165            } else if media_type.specificity() > preferred.specificity() {
166                // Prefer more specific media types over less specific ones. IE:
167                // text/html over application/*.
168                preferred = media_type;
169            } else if media_type == preferred {
170                // Finally, all other things being equal, prefer a media type
171                // with more parameters over one with fewer. IE: text/html; a=b
172                // over text/html.
173                if media_type.params().count() > preferred.params().count() {
174                    preferred = media_type;
175                }
176            }
177        }
178
179        preferred
180    }
181
182    /// Retrieve the first media type in `self`, if any.
183    ///
184    /// # Example
185    ///
186    /// ```rust
187    /// # extern crate rocket;
188    /// use rocket::http::{QMediaType, MediaType, Accept};
189    ///
190    /// let accept = Accept::new(QMediaType(MediaType::XML, None));
191    /// assert_eq!(accept.first(), Some(&MediaType::XML.into()));
192    /// ```
193    #[inline(always)]
194    pub fn first(&self) -> Option<&QMediaType> {
195        self.iter().next()
196    }
197
198    /// Returns an iterator over all of the (quality) media types in `self`.
199    /// Media types are returned in the order in which they appear in the
200    /// header.
201    ///
202    /// # Example
203    ///
204    /// ```rust
205    /// # extern crate rocket;
206    /// use rocket::http::{QMediaType, MediaType, Accept};
207    ///
208    /// let qmedia_types = vec![
209    ///     QMediaType(MediaType::JSON, Some(0.3)),
210    ///     QMediaType(MediaType::HTML, Some(0.9))
211    /// ];
212    ///
213    /// let accept = Accept::new(qmedia_types.clone());
214    ///
215    /// let mut iter = accept.iter();
216    /// assert_eq!(iter.next(), Some(&qmedia_types[0]));
217    /// assert_eq!(iter.next(), Some(&qmedia_types[1]));
218    /// assert_eq!(iter.next(), None);
219    /// ```
220    #[inline(always)]
221    pub fn iter(&self) -> impl Iterator<Item = &'_ QMediaType> + '_ {
222        self.0.iter()
223    }
224
225    /// Returns an iterator over all of the (bare) media types in `self`. Media
226    /// types are returned in the order in which they appear in the header.
227    ///
228    /// # Example
229    ///
230    /// ```rust
231    /// # extern crate rocket;
232    /// use rocket::http::{QMediaType, MediaType, Accept};
233    ///
234    /// let qmedia_types = vec![
235    ///     QMediaType(MediaType::JSON, Some(0.3)),
236    ///     QMediaType(MediaType::HTML, Some(0.9))
237    /// ];
238    ///
239    /// let accept = Accept::new(qmedia_types.clone());
240    ///
241    /// let mut iter = accept.media_types();
242    /// assert_eq!(iter.next(), Some(qmedia_types[0].media_type()));
243    /// assert_eq!(iter.next(), Some(qmedia_types[1].media_type()));
244    /// assert_eq!(iter.next(), None);
245    /// ```
246    #[inline(always)]
247    pub fn media_types(&self) -> impl Iterator<Item = &'_ MediaType> + '_ {
248        self.iter().map(|weighted_mt| weighted_mt.media_type())
249    }
250
251    known_media_types!(accept_constructor);
252}
253
254impl<T: IntoIterator<Item = MediaType>> From<T> for Accept {
255    #[inline(always)]
256    fn from(items: T) -> Accept {
257        Accept::new(items.into_iter().map(QMediaType::from))
258    }
259}
260
261impl PartialEq for Accept {
262    fn eq(&self, other: &Accept) -> bool {
263        self.iter().eq(other.iter())
264    }
265}
266
267impl fmt::Display for Accept {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        for (i, media_type) in self.iter().enumerate() {
270            if i >= 1 {
271                write!(f, ", {}", media_type.0)?;
272            } else {
273                write!(f, "{}", media_type.0)?;
274            }
275        }
276
277        Ok(())
278    }
279}
280
281impl FromStr for Accept {
282    // Ideally we'd return a `ParseError`, but that requires a lifetime.
283    type Err = String;
284
285    #[inline]
286    fn from_str(raw: &str) -> Result<Accept, String> {
287        parse_accept(raw).map_err(|e| e.to_string())
288    }
289}
290
291/// Creates a new `Header` with name `Accept` and the value set to the HTTP
292/// rendering of this `Accept` header.
293impl From<Accept> for Header<'static> {
294    #[inline(always)]
295    fn from(val: Accept) -> Self {
296        Header::new("Accept", val.to_string())
297    }
298}
299
300impl QMediaType {
301    /// Retrieve the weight of the media type, if there is any.
302    ///
303    /// # Example
304    ///
305    /// ```rust
306    /// # extern crate rocket;
307    /// use rocket::http::{MediaType, QMediaType};
308    ///
309    /// let q_type = QMediaType(MediaType::HTML, Some(0.3));
310    /// assert_eq!(q_type.weight(), Some(0.3));
311    /// ```
312    #[inline(always)]
313    pub fn weight(&self) -> Option<f32> {
314        self.1
315    }
316
317    /// Retrieve the weight of the media type or a given default value.
318    ///
319    /// # Example
320    ///
321    /// ```rust
322    /// # extern crate rocket;
323    /// use rocket::http::{MediaType, QMediaType};
324    ///
325    /// let q_type = QMediaType(MediaType::HTML, Some(0.3));
326    /// assert_eq!(q_type.weight_or(0.9), 0.3);
327    ///
328    /// let q_type = QMediaType(MediaType::HTML, None);
329    /// assert_eq!(q_type.weight_or(0.9), 0.9);
330    /// ```
331    #[inline(always)]
332    pub fn weight_or(&self, default: f32) -> f32 {
333        self.1.unwrap_or(default)
334    }
335
336    /// Borrow the internal `MediaType`.
337    ///
338    /// # Example
339    ///
340    /// ```rust
341    /// # extern crate rocket;
342    /// use rocket::http::{MediaType, QMediaType};
343    ///
344    /// let q_type = QMediaType(MediaType::HTML, Some(0.3));
345    /// assert_eq!(q_type.media_type(), &MediaType::HTML);
346    /// ```
347    #[inline(always)]
348    pub fn media_type(&self) -> &MediaType {
349        &self.0
350    }
351}
352
353impl IntoIterator for QMediaType {
354    type Item = Self;
355
356    type IntoIter = std::iter::Once<Self>;
357
358    fn into_iter(self) -> Self::IntoIter {
359        std::iter::once(self)
360    }
361}
362
363impl From<MediaType> for QMediaType {
364    #[inline(always)]
365    fn from(media_type: MediaType) -> QMediaType {
366        QMediaType(media_type, None)
367    }
368}
369
370impl Deref for QMediaType {
371    type Target = MediaType;
372
373    #[inline(always)]
374    fn deref(&self) -> &MediaType {
375        &self.0
376    }
377}
378
379#[cfg(test)]
380mod test {
381    use crate::{Accept, MediaType};
382
383    #[track_caller]
384    fn assert_preference(string: &str, expect: &str) {
385        let accept: Accept = string.parse().expect("accept string parse");
386        let expected: MediaType = expect.parse().expect("media type parse");
387        let preferred = accept.preferred();
388        let actual = preferred.media_type();
389        if *actual != expected {
390            panic!(
391                "mismatch for {}: expected {}, got {}",
392                string, expected, actual
393            )
394        }
395    }
396
397    #[test]
398    fn test_preferred() {
399        assert_preference("text/*", "text/*");
400        assert_preference("text/*, text/html", "text/html");
401        assert_preference("text/*; q=0.1, text/html", "text/html");
402        assert_preference("text/*; q=1, text/html", "text/html");
403        assert_preference("text/html, text/*", "text/html");
404        assert_preference("text/*, text/html", "text/html");
405        assert_preference("text/html, text/*; q=1", "text/html");
406        assert_preference("text/html; q=1, text/html", "text/html");
407        assert_preference("text/html, text/*; q=0.1", "text/html");
408
409        assert_preference("text/html, application/json", "text/html");
410        assert_preference("text/html, application/json; q=1", "text/html");
411        assert_preference("application/json; q=1, text/html", "text/html");
412
413        assert_preference("text/*, application/json", "application/json");
414        assert_preference("*/*, text/*", "text/*");
415        assert_preference("*/*, text/*, text/plain", "text/plain");
416
417        assert_preference("a/b; q=0.1, a/b; q=0.2", "a/b; q=0.2");
418        assert_preference("a/b; q=0.1, b/c; q=0.2", "b/c; q=0.2");
419        assert_preference("a/b; q=0.5, b/c; q=0.2", "a/b; q=0.5");
420
421        assert_preference("a/b; q=0.5, b/c; q=0.2, c/d", "c/d");
422        assert_preference("a/b; q=0.5; v=1, a/b", "a/b");
423
424        assert_preference("a/b; v=1, a/b; v=1; c=2", "a/b; v=1; c=2");
425        assert_preference("a/b; v=1; c=2, a/b; v=1", "a/b; v=1; c=2");
426        assert_preference(
427            "a/b; q=0.5; v=1, a/b; q=0.5; v=1; c=2",
428            "a/b; q=0.5; v=1; c=2",
429        );
430        assert_preference("a/b; q=0.6; v=1, a/b; q=0.5; v=1; c=2", "a/b; q=0.6; v=1");
431    }
432}