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 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}