accept_header/
accept.rs

1use crate::{error::*, Accept, MediaType};
2use http::StatusCode;
3use itertools::Itertools;
4use mime::Mime;
5use std::{cmp::Ordering, fmt, str::FromStr};
6
7impl Accept {
8    /// Determine the most suitable `Content-Type` encoding.
9    pub fn negotiate(&self, available: &[Mime]) -> Result<Mime, StatusCode> {
10        for media_type in &self.types {
11            if available.contains(&media_type.mime) {
12                return Ok(media_type.mime.clone());
13            }
14        }
15
16        if self.wildcard.is_some() {
17            if available.contains(&mime::TEXT_HTML) {
18                return Ok(mime::TEXT_HTML);
19            }
20
21            if let Some(accept) = available.iter().next() {
22                return Ok(accept.clone());
23            }
24        }
25
26        Err(StatusCode::NOT_ACCEPTABLE)
27    }
28}
29
30impl FromStr for Accept {
31    type Err = Error;
32
33    fn from_str(s: &str) -> Result<Self, Self::Err> {
34        let mut types = Vec::new();
35        let mut wildcard = None;
36
37        for part in s.split(',') {
38            let mtype: MediaType = part.trim().parse()?;
39
40            if mtype.mime.type_() == mime::STAR {
41                wildcard = Some(mtype);
42            } else {
43                types.push(mtype);
44            }
45        }
46
47        types.sort_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
48
49        Ok(Accept { wildcard, types })
50    }
51}
52
53impl From<Mime> for Accept {
54    fn from(mime: Mime) -> Self {
55        Self {
56            wildcard: None,
57            types: vec![MediaType::from(mime)],
58        }
59    }
60}
61
62impl fmt::Display for Accept {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(f, "{}", self.types.iter().map(|m| m.to_string()).join(", "))?;
65
66        if let Some(wildcard) = &self.wildcard {
67            write!(f, ", {wildcard}")?;
68        }
69
70        Ok(())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn accept_should_be_parsed_and_sorted() {
80        let accept = "application/json, text/html;q=0.9, text/plain;q=0.8, */*;q=0.7, */*;q=0.6"
81            .parse::<Accept>()
82            .unwrap();
83
84        assert_eq!(
85            accept.wildcard,
86            Some(MediaType::from_str("*/*; q=0.6").unwrap())
87        );
88        assert_eq!(accept.types.len(), 3);
89        assert_eq!(
90            accept.types[0].mime,
91            Mime::from_str("application/json").unwrap()
92        );
93        assert_eq!(accept.types[0].weight, None);
94        assert_eq!(accept.types[1].mime, Mime::from_str("text/html").unwrap());
95        assert_eq!(accept.types[1].weight, Some(0.9));
96        assert_eq!(accept.types[2].mime, Mime::from_str("text/plain").unwrap());
97        assert_eq!(accept.types[2].weight, Some(0.8));
98    }
99
100    #[test]
101    fn content_negotiation_should_work() {
102        let accept = "application/json, text/html;q=0.9, text/plain;q=0.8, */*;q=0.7"
103            .parse::<Accept>()
104            .unwrap();
105
106        let available = &[
107            Mime::from_str("text/html").unwrap(),
108            Mime::from_str("application/json").unwrap(),
109        ];
110
111        let negotiated = accept.negotiate(&available[..]).unwrap();
112
113        assert_eq!(negotiated, Mime::from_str("application/json").unwrap());
114
115        let available = &[Mime::from_str("application/xml").unwrap()];
116        let negotiated = accept.negotiate(&available[..]).unwrap();
117        assert_eq!(negotiated, Mime::from_str("application/xml").unwrap());
118    }
119
120    #[test]
121    fn content_negotiation_should_fail_if_no_available() {
122        let accept = "application/json,text/html;q=0.9,text/plain;q=0.8"
123            .parse::<Accept>()
124            .unwrap();
125
126        let available = &[Mime::from_str("application/xml").unwrap()];
127        let negotiated = accept.negotiate(&available[..]);
128        assert_eq!(negotiated, Err(StatusCode::NOT_ACCEPTABLE));
129    }
130
131    #[test]
132    fn accept_to_string_should_work() {
133        let accept = "application/json, text/plain;q=0.8, text/html;q=0.9, */*;q=0.7,*/*;q=0.6"
134            .parse::<Accept>()
135            .unwrap();
136
137        assert_eq!(
138            accept.to_string(),
139            "application/json, text/html;q=0.9, text/plain;q=0.8, */*;q=0.6"
140        );
141    }
142}