http_types_2/content/
accept.rs

1//! Client header advertising which media types the client is able to understand.
2
3use crate::headers::{HeaderName, HeaderValue, Headers, ACCEPT};
4use crate::mime::Mime;
5use crate::utils::sort_by_weight;
6use crate::{
7    content::{ContentType, MediaTypeProposal},
8    headers::Header,
9};
10use crate::{Error, StatusCode};
11
12use std::fmt::{self, Debug, Write};
13
14use std::slice;
15
16/// Client header advertising which media types the client is able to understand.
17///
18/// Using content negotiation, the server then selects one of the proposals, uses
19/// it and informs the client of its choice with the `Content-Type` response
20/// header. Browsers set adequate values for this header depending on the context
21/// where the request is done: when fetching a CSS stylesheet a different value
22/// is set for the request than when fetching an image, video or a script.
23///
24/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)
25///
26/// # Specifications
27///
28/// - [RFC 7231, section 5.3.2: Accept](https://tools.ietf.org/html/rfc7231#section-5.3.2)
29///
30/// # Examples
31///
32/// ```
33/// # fn main() -> http_types::Result<()> {
34/// #
35/// use http_types::content::{Accept, MediaTypeProposal};
36/// use http_types::{mime, Response};
37///
38/// let mut accept = Accept::new();
39/// accept.push(MediaTypeProposal::new(mime::HTML, Some(0.8))?);
40/// accept.push(MediaTypeProposal::new(mime::XML, Some(0.4))?);
41/// accept.push(mime::PLAIN);
42///
43/// let mut res = Response::new(200);
44/// let content_type = accept.negotiate(&[mime::XML])?;
45/// res.insert_header(&content_type, &content_type);
46///
47/// assert_eq!(res["Content-Type"], "application/xml;charset=utf-8");
48/// #
49/// # Ok(()) }
50/// ```
51pub struct Accept {
52    wildcard: bool,
53    entries: Vec<MediaTypeProposal>,
54}
55
56impl Accept {
57    /// Create a new instance of `Accept`.
58    pub fn new() -> Self {
59        Self {
60            entries: vec![],
61            wildcard: false,
62        }
63    }
64
65    /// Create an instance of `Accept` from a `Headers` instance.
66    pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
67        let mut entries = vec![];
68        let headers = match headers.as_ref().get(ACCEPT) {
69            Some(headers) => headers,
70            None => return Ok(None),
71        };
72
73        let mut wildcard = false;
74
75        for value in headers {
76            for part in value.as_str().trim().split(',') {
77                let part = part.trim();
78
79                // Handle empty strings, and wildcard directives.
80                if part.is_empty() {
81                    continue;
82                } else if part == "*" {
83                    wildcard = true;
84                    continue;
85                }
86
87                // Try and parse a directive from a str. If the directive is
88                // unkown we skip it.
89                let entry = MediaTypeProposal::from_str(part)?;
90                entries.push(entry);
91            }
92        }
93
94        Ok(Some(Self { wildcard, entries }))
95    }
96
97    /// Push a directive into the list of entries.
98    pub fn push(&mut self, prop: impl Into<MediaTypeProposal>) {
99        self.entries.push(prop.into());
100    }
101
102    /// Returns `true` if a wildcard directive was passed.
103    pub fn wildcard(&self) -> bool {
104        self.wildcard
105    }
106
107    /// Set the wildcard directive.
108    pub fn set_wildcard(&mut self, wildcard: bool) {
109        self.wildcard = wildcard
110    }
111
112    /// Sort the header directives by weight.
113    ///
114    /// Headers with a higher `q=` value will be returned first. If two
115    /// directives have the same weight, the directive that was declared later
116    /// will be returned first.
117    pub fn sort(&mut self) {
118        sort_by_weight(&mut self.entries);
119    }
120
121    /// Determine the most suitable `Content-Type` encoding.
122    ///
123    /// # Errors
124    ///
125    /// If no suitable encoding is found, an error with the status of `406` will be returned.
126    pub fn negotiate(&mut self, available: &[Mime]) -> crate::Result<ContentType> {
127        // Start by ordering the encodings.
128        self.sort();
129
130        // Try and find the first encoding that matches.
131        for accept in &self.entries {
132            if let Some(accept) = available.iter().find(|m| m.subset_eq(accept.media_type())) {
133                return Ok(accept.clone().into());
134            }
135        }
136
137        // If no encoding matches and wildcard is set, send whichever encoding we got.
138        if self.wildcard {
139            if let Some(accept) = available.iter().next() {
140                return Ok(accept.clone().into());
141            }
142        }
143
144        let mut err = Error::new_adhoc("No suitable Content-Type found");
145        err.set_status(StatusCode::NotAcceptable);
146        Err(err)
147    }
148
149    /// An iterator visiting all entries.
150    pub fn iter(&self) -> Iter<'_> {
151        Iter {
152            inner: self.entries.iter(),
153        }
154    }
155
156    /// An iterator visiting all entries.
157    pub fn iter_mut(&mut self) -> IterMut<'_> {
158        IterMut {
159            inner: self.entries.iter_mut(),
160        }
161    }
162}
163
164impl Header for Accept {
165    fn header_name(&self) -> HeaderName {
166        ACCEPT
167    }
168    fn header_value(&self) -> HeaderValue {
169        let mut output = String::new();
170        for (n, directive) in self.entries.iter().enumerate() {
171            let directive: HeaderValue = directive.clone().into();
172            match n {
173                0 => write!(output, "{directive}").unwrap(),
174                _ => write!(output, ", {directive}").unwrap(),
175            };
176        }
177
178        if self.wildcard {
179            match output.len() {
180                0 => write!(output, "*").unwrap(),
181                _ => write!(output, ", *").unwrap(),
182            }
183        }
184
185        // SAFETY: the internal string is validated to be ASCII.
186        unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
187    }
188}
189
190impl IntoIterator for Accept {
191    type Item = MediaTypeProposal;
192    type IntoIter = IntoIter;
193
194    #[inline]
195    fn into_iter(self) -> Self::IntoIter {
196        IntoIter {
197            inner: self.entries.into_iter(),
198        }
199    }
200}
201
202impl<'a> IntoIterator for &'a Accept {
203    type Item = &'a MediaTypeProposal;
204    type IntoIter = Iter<'a>;
205
206    #[inline]
207    fn into_iter(self) -> Self::IntoIter {
208        self.iter()
209    }
210}
211
212impl<'a> IntoIterator for &'a mut Accept {
213    type Item = &'a mut MediaTypeProposal;
214    type IntoIter = IterMut<'a>;
215
216    #[inline]
217    fn into_iter(self) -> Self::IntoIter {
218        self.iter_mut()
219    }
220}
221
222/// A borrowing iterator over entries in `Accept`.
223#[derive(Debug)]
224pub struct IntoIter {
225    inner: std::vec::IntoIter<MediaTypeProposal>,
226}
227
228impl Iterator for IntoIter {
229    type Item = MediaTypeProposal;
230
231    fn next(&mut self) -> Option<Self::Item> {
232        self.inner.next()
233    }
234
235    #[inline]
236    fn size_hint(&self) -> (usize, Option<usize>) {
237        self.inner.size_hint()
238    }
239}
240
241/// A lending iterator over entries in `Accept`.
242#[derive(Debug)]
243pub struct Iter<'a> {
244    inner: slice::Iter<'a, MediaTypeProposal>,
245}
246
247impl<'a> Iterator for Iter<'a> {
248    type Item = &'a MediaTypeProposal;
249
250    fn next(&mut self) -> Option<Self::Item> {
251        self.inner.next()
252    }
253
254    #[inline]
255    fn size_hint(&self) -> (usize, Option<usize>) {
256        self.inner.size_hint()
257    }
258}
259
260/// A mutable iterator over entries in `Accept`.
261#[derive(Debug)]
262pub struct IterMut<'a> {
263    inner: slice::IterMut<'a, MediaTypeProposal>,
264}
265
266impl<'a> Iterator for IterMut<'a> {
267    type Item = &'a mut MediaTypeProposal;
268
269    fn next(&mut self) -> Option<Self::Item> {
270        self.inner.next()
271    }
272
273    #[inline]
274    fn size_hint(&self) -> (usize, Option<usize>) {
275        self.inner.size_hint()
276    }
277}
278
279impl Debug for Accept {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        let mut list = f.debug_list();
282        for directive in &self.entries {
283            list.entry(directive);
284        }
285        list.finish()
286    }
287}
288
289#[cfg(test)]
290mod test {
291    use super::*;
292    use crate::mime;
293    use crate::Response;
294
295    #[test]
296    fn smoke() -> crate::Result<()> {
297        let mut accept = Accept::new();
298        accept.push(mime::HTML);
299
300        let mut headers = Response::new(200);
301        accept.apply_header(&mut headers);
302
303        let accept = Accept::from_headers(headers)?.unwrap();
304        assert_eq!(accept.iter().next().unwrap(), mime::HTML);
305        Ok(())
306    }
307
308    #[test]
309    fn wildcard() -> crate::Result<()> {
310        let mut accept = Accept::new();
311        accept.set_wildcard(true);
312
313        let mut headers = Response::new(200);
314        accept.apply_header(&mut headers);
315
316        let accept = Accept::from_headers(headers)?.unwrap();
317        assert!(accept.wildcard());
318        Ok(())
319    }
320
321    #[test]
322    fn wildcard_and_header() -> crate::Result<()> {
323        let mut accept = Accept::new();
324        accept.push(mime::HTML);
325        accept.set_wildcard(true);
326
327        let mut headers = Response::new(200);
328        accept.apply_header(&mut headers);
329
330        let accept = Accept::from_headers(headers)?.unwrap();
331        assert!(accept.wildcard());
332        assert_eq!(accept.iter().next().unwrap(), mime::HTML);
333        Ok(())
334    }
335
336    #[test]
337    fn iter() -> crate::Result<()> {
338        let mut accept = Accept::new();
339        accept.push(mime::HTML);
340        accept.push(mime::XML);
341
342        let mut headers = Response::new(200);
343        accept.apply_header(&mut headers);
344
345        let accept = Accept::from_headers(headers)?.unwrap();
346        let mut accept = accept.iter();
347        assert_eq!(accept.next().unwrap(), mime::HTML);
348        assert_eq!(accept.next().unwrap(), mime::XML);
349        Ok(())
350    }
351
352    #[test]
353    fn reorder_based_on_weight() -> crate::Result<()> {
354        let mut accept = Accept::new();
355        accept.push(MediaTypeProposal::new(mime::HTML, Some(0.4))?);
356        accept.push(MediaTypeProposal::new(mime::XML, None)?);
357        accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
358
359        let mut headers = Response::new(200);
360        accept.apply_header(&mut headers);
361
362        let mut accept = Accept::from_headers(headers)?.unwrap();
363        accept.sort();
364        let mut accept = accept.iter();
365        assert_eq!(accept.next().unwrap(), mime::XML);
366        assert_eq!(accept.next().unwrap(), mime::PLAIN);
367        assert_eq!(accept.next().unwrap(), mime::HTML);
368        Ok(())
369    }
370
371    #[test]
372    fn reorder_based_on_weight_and_location() -> crate::Result<()> {
373        let mut accept = Accept::new();
374        accept.push(MediaTypeProposal::new(mime::HTML, None)?);
375        accept.push(MediaTypeProposal::new(mime::XML, None)?);
376        accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
377
378        let mut res = Response::new(200);
379        accept.apply_header(&mut res);
380
381        let mut accept = Accept::from_headers(res)?.unwrap();
382        accept.sort();
383        let mut accept = accept.iter();
384        assert_eq!(accept.next().unwrap(), mime::XML);
385        assert_eq!(accept.next().unwrap(), mime::HTML);
386        assert_eq!(accept.next().unwrap(), mime::PLAIN);
387        Ok(())
388    }
389
390    #[test]
391    fn negotiate() -> crate::Result<()> {
392        let mut accept = Accept::new();
393        accept.push(MediaTypeProposal::new(mime::HTML, Some(0.4))?);
394        accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
395        accept.push(MediaTypeProposal::new(mime::XML, None)?);
396
397        assert_eq!(accept.negotiate(&[mime::HTML, mime::XML])?, mime::XML);
398        Ok(())
399    }
400
401    #[test]
402    fn negotiate_not_acceptable() -> crate::Result<()> {
403        let mut accept = Accept::new();
404        let err = accept.negotiate(&[mime::JSON]).unwrap_err();
405        assert_eq!(err.status(), 406);
406
407        let mut accept = Accept::new();
408        accept.push(MediaTypeProposal::new(mime::JSON, Some(0.8))?);
409        let err = accept.negotiate(&[mime::XML]).unwrap_err();
410        assert_eq!(err.status(), 406);
411        Ok(())
412    }
413
414    #[test]
415    fn negotiate_wildcard() -> crate::Result<()> {
416        let mut accept = Accept::new();
417        accept.push(MediaTypeProposal::new(mime::JSON, Some(0.8))?);
418        accept.set_wildcard(true);
419
420        assert_eq!(accept.negotiate(&[mime::XML])?, mime::XML);
421        Ok(())
422    }
423
424    #[test]
425    fn negotiate_missing_encoding() -> crate::Result<()> {
426        let mime_html = "text/html".parse::<Mime>()?;
427
428        let mut browser_accept = Accept::new();
429        browser_accept.push(MediaTypeProposal::new(mime_html, None)?);
430
431        let acceptable = &[mime::HTML];
432
433        let content_type = browser_accept.negotiate(acceptable);
434
435        assert!(
436            content_type.is_ok(),
437            "server is expected to return HTML content"
438        );
439        Ok(())
440    }
441}