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        let entries: Vec<_> = crate::split_comma_entries(raw)
123            .into_iter()
124            .map(parse_entry)
125            .collect::<Result<_, _>>()?;
126        if entries.is_empty() {
127            return Err(SipAcceptLanguageError::Empty);
128        }
129        Ok(Self(entries))
130    }
131
132    /// The parsed entries as a slice.
133    pub fn entries(&self) -> &[SipAcceptLanguageEntry] {
134        &self.0
135    }
136
137    /// Consume self and return entries as a `Vec`.
138    pub fn into_entries(self) -> Vec<SipAcceptLanguageEntry> {
139        self.0
140    }
141
142    /// Number of entries.
143    pub fn len(&self) -> usize {
144        self.0
145            .len()
146    }
147
148    /// Returns `true` if there are no entries.
149    pub fn is_empty(&self) -> bool {
150        self.0
151            .is_empty()
152    }
153}
154
155impl fmt::Display for SipAcceptLanguage {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        crate::fmt_joined(f, &self.0, ", ")
158    }
159}
160
161impl_from_str_via_parse!(SipAcceptLanguage, SipAcceptLanguageError);
162
163impl<'a> IntoIterator for &'a SipAcceptLanguage {
164    type Item = &'a SipAcceptLanguageEntry;
165    type IntoIter = std::slice::Iter<'a, SipAcceptLanguageEntry>;
166
167    fn into_iter(self) -> Self::IntoIter {
168        self.0
169            .iter()
170    }
171}
172
173impl IntoIterator for SipAcceptLanguage {
174    type Item = SipAcceptLanguageEntry;
175    type IntoIter = std::vec::IntoIter<SipAcceptLanguageEntry>;
176
177    fn into_iter(self) -> Self::IntoIter {
178        self.0
179            .into_iter()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn single_language() {
189        let al = SipAcceptLanguage::parse("en").unwrap();
190        assert_eq!(al.len(), 1);
191        assert_eq!(al.entries()[0].language(), "en");
192    }
193
194    #[test]
195    fn multiple_languages_with_q() {
196        let al = SipAcceptLanguage::parse("en;q=0.9, fr;q=0.8, *;q=0.1").unwrap();
197        assert_eq!(al.len(), 3);
198        assert_eq!(al.entries()[0].language(), "en");
199        assert_eq!(al.entries()[1].q(), Some("0.8"));
200        assert_eq!(al.entries()[2].language(), "*");
201    }
202
203    #[test]
204    fn language_subtag() {
205        let al = SipAcceptLanguage::parse("en-US").unwrap();
206        assert_eq!(al.entries()[0].language(), "en-us");
207    }
208
209    #[test]
210    fn empty_input() {
211        assert!(matches!(
212            SipAcceptLanguage::parse(""),
213            Err(SipAcceptLanguageError::Empty)
214        ));
215    }
216
217    #[test]
218    fn from_str() {
219        let al: SipAcceptLanguage = "en"
220            .parse()
221            .unwrap();
222        assert_eq!(al.len(), 1);
223    }
224
225    #[test]
226    fn display_roundtrip() {
227        let raw = "en;q=0.9";
228        let al = SipAcceptLanguage::parse(raw).unwrap();
229        assert_eq!(al.to_string(), raw);
230    }
231}