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