rocket_http/header/
accept.rs

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