http_types_rs/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_rs::Result<()> {
34/// #
35/// use http_types_rs::content::{Accept, MediaTypeProposal};
36/// use http_types_rs::{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 { inner: self.entries.iter() }
152    }
153
154    /// An iterator visiting all entries.
155    pub fn iter_mut(&mut self) -> IterMut<'_> {
156        IterMut {
157            inner: self.entries.iter_mut(),
158        }
159    }
160}
161
162impl Header for Accept {
163    fn header_name(&self) -> HeaderName {
164        ACCEPT
165    }
166    fn header_value(&self) -> HeaderValue {
167        let mut output = String::new();
168        for (n, directive) in self.entries.iter().enumerate() {
169            let directive: HeaderValue = directive.clone().into();
170            match n {
171                0 => write!(output, "{}", directive).unwrap(),
172                _ => write!(output, ", {}", directive).unwrap(),
173            };
174        }
175
176        if self.wildcard {
177            match output.len() {
178                0 => write!(output, "*/*").unwrap(),
179                _ => write!(output, ", */*").unwrap(),
180            }
181        }
182
183        // SAFETY: the internal string is validated to be ASCII.
184        unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
185    }
186}
187
188impl IntoIterator for Accept {
189    type Item = MediaTypeProposal;
190    type IntoIter = IntoIter;
191
192    #[inline]
193    fn into_iter(self) -> Self::IntoIter {
194        IntoIter {
195            inner: self.entries.into_iter(),
196        }
197    }
198}
199
200impl<'a> IntoIterator for &'a Accept {
201    type Item = &'a MediaTypeProposal;
202    type IntoIter = Iter<'a>;
203
204    #[inline]
205    fn into_iter(self) -> Self::IntoIter {
206        self.iter()
207    }
208}
209
210impl<'a> IntoIterator for &'a mut Accept {
211    type Item = &'a mut MediaTypeProposal;
212    type IntoIter = IterMut<'a>;
213
214    #[inline]
215    fn into_iter(self) -> Self::IntoIter {
216        self.iter_mut()
217    }
218}
219
220/// A borrowing iterator over entries in `Accept`.
221#[derive(Debug)]
222pub struct IntoIter {
223    inner: std::vec::IntoIter<MediaTypeProposal>,
224}
225
226impl Iterator for IntoIter {
227    type Item = MediaTypeProposal;
228
229    fn next(&mut self) -> Option<Self::Item> {
230        self.inner.next()
231    }
232
233    #[inline]
234    fn size_hint(&self) -> (usize, Option<usize>) {
235        self.inner.size_hint()
236    }
237}
238
239/// A lending iterator over entries in `Accept`.
240#[derive(Debug)]
241pub struct Iter<'a> {
242    inner: slice::Iter<'a, MediaTypeProposal>,
243}
244
245impl<'a> Iterator for Iter<'a> {
246    type Item = &'a MediaTypeProposal;
247
248    fn next(&mut self) -> Option<Self::Item> {
249        self.inner.next()
250    }
251
252    #[inline]
253    fn size_hint(&self) -> (usize, Option<usize>) {
254        self.inner.size_hint()
255    }
256}
257
258/// A mutable iterator over entries in `Accept`.
259#[derive(Debug)]
260pub struct IterMut<'a> {
261    inner: slice::IterMut<'a, MediaTypeProposal>,
262}
263
264impl<'a> Iterator for IterMut<'a> {
265    type Item = &'a mut MediaTypeProposal;
266
267    fn next(&mut self) -> Option<Self::Item> {
268        self.inner.next()
269    }
270
271    #[inline]
272    fn size_hint(&self) -> (usize, Option<usize>) {
273        self.inner.size_hint()
274    }
275}
276
277impl Debug for Accept {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        let mut list = f.debug_list();
280        for directive in &self.entries {
281            list.entry(directive);
282        }
283        list.finish()
284    }
285}
286
287#[cfg(test)]
288mod test {
289    use super::*;
290    use crate::mime;
291    use crate::Response;
292
293    #[test]
294    fn smoke() -> crate::Result<()> {
295        let mut accept = Accept::new();
296        accept.push(mime::HTML);
297
298        let mut headers = Response::new(200);
299        accept.apply_header(&mut headers);
300
301        let accept = Accept::from_headers(headers)?.unwrap();
302        assert_eq!(accept.iter().next().unwrap(), mime::HTML);
303        Ok(())
304    }
305
306    #[test]
307    fn wildcard() -> crate::Result<()> {
308        let mut accept = Accept::new();
309        accept.set_wildcard(true);
310
311        let mut headers = Response::new(200);
312        accept.apply_header(&mut headers);
313
314        let accept = Accept::from_headers(headers)?.unwrap();
315        assert!(accept.wildcard());
316        Ok(())
317    }
318
319    #[test]
320    fn wildcard_and_header() -> crate::Result<()> {
321        let mut accept = Accept::new();
322        accept.push(mime::HTML);
323        accept.set_wildcard(true);
324
325        let mut headers = Response::new(200);
326        accept.apply_header(&mut headers);
327
328        let accept = Accept::from_headers(headers)?.unwrap();
329        assert!(accept.wildcard());
330        assert_eq!(accept.iter().next().unwrap(), mime::HTML);
331        Ok(())
332    }
333
334    #[test]
335    fn iter() -> crate::Result<()> {
336        let mut accept = Accept::new();
337        accept.push(mime::HTML);
338        accept.push(mime::XML);
339
340        let mut headers = Response::new(200);
341        accept.apply_header(&mut headers);
342
343        let accept = Accept::from_headers(headers)?.unwrap();
344        let mut accept = accept.iter();
345        assert_eq!(accept.next().unwrap(), mime::HTML);
346        assert_eq!(accept.next().unwrap(), mime::XML);
347        Ok(())
348    }
349
350    #[test]
351    fn reorder_based_on_weight() -> crate::Result<()> {
352        let mut accept = Accept::new();
353        accept.push(MediaTypeProposal::new(mime::HTML, Some(0.4))?);
354        accept.push(MediaTypeProposal::new(mime::XML, None)?);
355        accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
356
357        let mut headers = Response::new(200);
358        accept.apply_header(&mut headers);
359
360        let mut accept = Accept::from_headers(headers)?.unwrap();
361        accept.sort();
362        let mut accept = accept.iter();
363        assert_eq!(accept.next().unwrap(), mime::XML);
364        assert_eq!(accept.next().unwrap(), mime::PLAIN);
365        assert_eq!(accept.next().unwrap(), mime::HTML);
366        Ok(())
367    }
368
369    #[test]
370    fn reorder_based_on_weight_and_location() -> crate::Result<()> {
371        let mut accept = Accept::new();
372        accept.push(MediaTypeProposal::new(mime::HTML, None)?);
373        accept.push(MediaTypeProposal::new(mime::XML, None)?);
374        accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
375
376        let mut res = Response::new(200);
377        accept.apply_header(&mut res);
378
379        let mut accept = Accept::from_headers(res)?.unwrap();
380        accept.sort();
381        let mut accept = accept.iter();
382        assert_eq!(accept.next().unwrap(), mime::XML);
383        assert_eq!(accept.next().unwrap(), mime::HTML);
384        assert_eq!(accept.next().unwrap(), mime::PLAIN);
385        Ok(())
386    }
387
388    #[test]
389    fn negotiate() -> crate::Result<()> {
390        let mut accept = Accept::new();
391        accept.push(MediaTypeProposal::new(mime::HTML, Some(0.4))?);
392        accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
393        accept.push(MediaTypeProposal::new(mime::XML, None)?);
394
395        assert_eq!(accept.negotiate(&[mime::HTML, mime::XML])?, mime::XML);
396        Ok(())
397    }
398
399    #[test]
400    fn negotiate_not_acceptable() -> crate::Result<()> {
401        let mut accept = Accept::new();
402        let err = accept.negotiate(&[mime::JSON]).unwrap_err();
403        assert_eq!(err.status(), 406);
404
405        let mut accept = Accept::new();
406        accept.push(MediaTypeProposal::new(mime::JSON, Some(0.8))?);
407        let err = accept.negotiate(&[mime::XML]).unwrap_err();
408        assert_eq!(err.status(), 406);
409        Ok(())
410    }
411
412    #[test]
413    fn negotiate_wildcard() -> crate::Result<()> {
414        let mut accept = Accept::new();
415        accept.push(MediaTypeProposal::new(mime::JSON, Some(0.8))?);
416        accept.set_wildcard(true);
417
418        assert_eq!(accept.negotiate(&[mime::XML])?, mime::XML);
419        Ok(())
420    }
421
422    #[test]
423    fn negotiate_missing_encoding() -> crate::Result<()> {
424        let mime_html = "text/html".parse::<Mime>()?;
425
426        let mut browser_accept = Accept::new();
427        browser_accept.push(MediaTypeProposal::new(mime_html, None)?);
428
429        let acceptable = &[mime::HTML];
430
431        let content_type = browser_accept.negotiate(acceptable);
432
433        assert!(content_type.is_ok(), "server is expected to return HTML content");
434        Ok(())
435    }
436
437    #[test]
438    fn parse() -> crate::Result<()> {
439        let mut headers = Headers::new();
440        headers.insert("Accept", "application/json; q=0.8,*/*")?;
441        let accept = Accept::from_headers(headers)?.unwrap();
442
443        assert!(accept.wildcard());
444        assert_eq!(
445            accept.into_iter().collect::<Vec<_>>(),
446            vec![MediaTypeProposal::new(mime::JSON, Some(0.8))?]
447        );
448        Ok(())
449    }
450
451    #[test]
452    fn serialize() -> crate::Result<()> {
453        let mut accept = Accept::new();
454        accept.push(MediaTypeProposal::new(mime::JSON, Some(0.8))?);
455        accept.set_wildcard(true);
456
457        assert_eq!(accept.header_value().as_str(), "application/json;q=0.800, */*");
458        Ok(())
459    }
460}