Skip to main content

sip_header/
accept_language.rs

1//! SIP Accept-Language header parser (RFC 3261 ยง20.3).
2
3use std::fmt;
4
5/// A single Accept-Language entry: `language-range *(SEMI accept-param)`.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub struct SipAcceptLanguageEntry {
9    language: String,
10    params: Vec<(String, String)>,
11}
12
13impl SipAcceptLanguageEntry {
14    /// The language tag (e.g. `"en"`, `"en-US"`, `"*"`).
15    pub fn language(&self) -> &str {
16        &self.language
17    }
18
19    /// All parameters as `(key, value)` pairs.
20    pub fn params(&self) -> &[(String, String)] {
21        &self.params
22    }
23
24    /// Look up a parameter by key (case-insensitive).
25    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    /// The `q` quality value, if present.
33    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/// Errors from parsing an Accept-Language header value.
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum SipAcceptLanguageError {
52    /// The input string was empty or whitespace-only.
53    Empty,
54    /// An entry could not be parsed.
55    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/// Parsed SIP Accept-Language header value.
111#[derive(Debug, Clone, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct SipAcceptLanguage(Vec<SipAcceptLanguageEntry>);
114
115impl SipAcceptLanguage {
116    /// Parse a comma-separated Accept-Language header value.
117    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    /// Build from entries a transport already split; each is one `language`.
126    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    /// The parsed entries as a slice.
140    pub fn entries(&self) -> &[SipAcceptLanguageEntry] {
141        &self.0
142    }
143
144    /// Consume self and return entries as a `Vec`.
145    pub fn into_entries(self) -> Vec<SipAcceptLanguageEntry> {
146        self.0
147    }
148
149    /// Number of entries.
150    pub fn len(&self) -> usize {
151        self.0
152            .len()
153    }
154
155    /// Returns `true` if there are no entries.
156    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}