sip_header/
accept_language.rs1use std::fmt;
4
5use crate::accept::{all_blank, read_accept_params, write_accept_params};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct SipAcceptLanguageEntry {
11 language: String,
12 params: Vec<(String, String)>,
13}
14
15impl SipAcceptLanguageEntry {
16 pub fn language(&self) -> &str {
18 &self.language
19 }
20
21 pub fn params(&self) -> &[(String, String)] {
23 &self.params
24 }
25
26 pub fn param(&self, key: &str) -> Option<&str> {
28 self.params
29 .iter()
30 .find(|(k, _)| k.eq_ignore_ascii_case(key))
31 .map(|(_, v)| v.as_str())
32 }
33
34 pub fn q(&self) -> Option<&str> {
36 self.param("q")
37 }
38}
39
40impl fmt::Display for SipAcceptLanguageEntry {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{}", self.language)?;
43 write_accept_params(f, &self.params)
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum SipAcceptLanguageError {
51 Empty,
54 InvalidFormat(String),
56}
57
58impl fmt::Display for SipAcceptLanguageError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::Empty => write!(f, "empty Accept-Language header value"),
62 Self::InvalidFormat(raw) => {
63 write!(f, "invalid Accept-Language entry ({} bytes)", raw.len())
64 }
65 }
66 }
67}
68
69impl std::error::Error for SipAcceptLanguageError {}
70
71fn parse_entry(raw: &str) -> Result<SipAcceptLanguageEntry, SipAcceptLanguageError> {
72 let raw = raw.trim();
73 if raw.is_empty() {
74 return Err(SipAcceptLanguageError::InvalidFormat(raw.to_string()));
75 }
76
77 let (lang_part, params_part) = match raw.split_once(';') {
78 Some((l, p)) => (l.trim(), Some(p)),
79 None => (raw, None),
80 };
81
82 if lang_part.is_empty() {
83 return Err(SipAcceptLanguageError::InvalidFormat(raw.to_string()));
84 }
85
86 Ok(SipAcceptLanguageEntry {
87 language: lang_part.to_ascii_lowercase(),
88 params: read_accept_params(params_part.unwrap_or("")),
89 })
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct SipAcceptLanguage(Vec<SipAcceptLanguageEntry>);
96
97impl SipAcceptLanguage {
98 pub fn parse(raw: &str) -> Result<Self, SipAcceptLanguageError> {
102 Self::from_entries(crate::split_comma_entries(raw))
103 }
104
105 pub fn from_entries<'a>(
110 entries: impl IntoIterator<Item = &'a str>,
111 ) -> Result<Self, SipAcceptLanguageError> {
112 let entries: Vec<&str> = entries
113 .into_iter()
114 .collect();
115 if all_blank(&entries) {
116 return Ok(Self(Vec::new()));
117 }
118 entries
119 .into_iter()
120 .map(parse_entry)
121 .collect::<Result<_, _>>()
122 .map(Self)
123 }
124
125 pub fn entries(&self) -> &[SipAcceptLanguageEntry] {
127 &self.0
128 }
129
130 pub fn into_entries(self) -> Vec<SipAcceptLanguageEntry> {
132 self.0
133 }
134
135 pub fn len(&self) -> usize {
137 self.0
138 .len()
139 }
140
141 pub fn is_empty(&self) -> bool {
143 self.0
144 .is_empty()
145 }
146}
147
148impl fmt::Display for SipAcceptLanguage {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 crate::fmt_joined(f, &self.0, ", ")
151 }
152}
153
154impl_from_str_via_parse!(SipAcceptLanguage, SipAcceptLanguageError);
155
156impl<'a> IntoIterator for &'a SipAcceptLanguage {
157 type Item = &'a SipAcceptLanguageEntry;
158 type IntoIter = std::slice::Iter<'a, SipAcceptLanguageEntry>;
159
160 fn into_iter(self) -> Self::IntoIter {
161 self.0
162 .iter()
163 }
164}
165
166impl IntoIterator for SipAcceptLanguage {
167 type Item = SipAcceptLanguageEntry;
168 type IntoIter = std::vec::IntoIter<SipAcceptLanguageEntry>;
169
170 fn into_iter(self) -> Self::IntoIter {
171 self.0
172 .into_iter()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn single_language() {
182 let al = SipAcceptLanguage::parse("en").unwrap();
183 assert_eq!(al.len(), 1);
184 assert_eq!(al.entries()[0].language(), "en");
185 }
186
187 #[test]
188 fn multiple_languages_with_q() {
189 let al = SipAcceptLanguage::parse("en;q=0.9, fr;q=0.8, *;q=0.1").unwrap();
190 assert_eq!(al.len(), 3);
191 assert_eq!(al.entries()[0].language(), "en");
192 assert_eq!(al.entries()[1].q(), Some("0.8"));
193 assert_eq!(al.entries()[2].language(), "*");
194 }
195
196 #[test]
197 fn language_subtag() {
198 let al = SipAcceptLanguage::parse("en-US").unwrap();
199 assert_eq!(al.entries()[0].language(), "en-us");
200 }
201
202 #[test]
203 fn empty_input_is_empty_list() {
204 assert!(SipAcceptLanguage::parse("")
205 .unwrap()
206 .is_empty());
207 assert!(SipAcceptLanguage::parse(" ")
208 .unwrap()
209 .is_empty());
210 }
211
212 #[test]
213 fn flag_param_roundtrip() {
214 let raw = "en;foo";
215 let al = SipAcceptLanguage::parse(raw).unwrap();
216 assert_eq!(al.entries()[0].param("foo"), Some(""));
217 assert_eq!(al.to_string(), raw);
218 }
219
220 #[test]
221 fn quoted_param_keeps_semicolon() {
222 let raw = r#"en;x="a;b";q=0.5"#;
223 let al = SipAcceptLanguage::parse(raw).unwrap();
224 assert_eq!(al.entries()[0].param("x"), Some(r#""a;b""#));
225 assert_eq!(al.entries()[0].q(), Some("0.5"));
226 assert_eq!(al.to_string(), raw);
227 }
228
229 #[test]
230 fn error_display_omits_input() {
231 let err = SipAcceptLanguage::parse(";secretvalue").unwrap_err();
232 assert!(!err
233 .to_string()
234 .contains("secretvalue"));
235 }
236
237 #[test]
238 fn from_str() {
239 let al: SipAcceptLanguage = "en"
240 .parse()
241 .unwrap();
242 assert_eq!(al.len(), 1);
243 }
244
245 #[test]
246 fn display_roundtrip() {
247 let raw = "en;q=0.9";
248 let al = SipAcceptLanguage::parse(raw).unwrap();
249 assert_eq!(al.to_string(), raw);
250 }
251
252 #[test]
253 fn from_entries_matches_parse() {
254 let split = SipAcceptLanguage::from_entries(["en;q=0.9", "fr-CA"]).unwrap();
255 let joined = SipAcceptLanguage::parse("en;q=0.9, fr-CA").unwrap();
256 assert_eq!(split, joined);
257 }
258
259 #[test]
260 fn from_entries_bad_entry_is_error() {
261 assert!(matches!(
262 SipAcceptLanguage::from_entries(["en", " "]),
263 Err(SipAcceptLanguageError::InvalidFormat(_))
264 ));
265 }
266
267 #[test]
268 fn from_entries_empty_is_empty_list() {
269 assert!(SipAcceptLanguage::from_entries(std::iter::empty::<&str>())
270 .unwrap()
271 .is_empty());
272 assert!(SipAcceptLanguage::from_entries(["", " "])
273 .unwrap()
274 .is_empty());
275 }
276}