use std::fmt;
use crate::accept::{all_blank, read_accept_params, write_accept_params};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipAcceptLanguageEntry {
language: String,
params: Vec<(String, String)>,
}
impl SipAcceptLanguageEntry {
pub fn language(&self) -> &str {
&self.language
}
pub fn params(&self) -> &[(String, String)] {
&self.params
}
pub fn param(&self, key: &str) -> Option<&str> {
self.params
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v.as_str())
}
pub fn q(&self) -> Option<&str> {
self.param("q")
}
}
impl fmt::Display for SipAcceptLanguageEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.language)?;
write_accept_params(f, &self.params)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SipAcceptLanguageError {
Empty,
InvalidFormat(String),
}
impl fmt::Display for SipAcceptLanguageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "empty Accept-Language header value"),
Self::InvalidFormat(raw) => {
write!(f, "invalid Accept-Language entry ({} bytes)", raw.len())
}
}
}
}
impl std::error::Error for SipAcceptLanguageError {}
fn parse_entry(raw: &str) -> Result<SipAcceptLanguageEntry, SipAcceptLanguageError> {
let raw = raw.trim();
if raw.is_empty() {
return Err(SipAcceptLanguageError::InvalidFormat(raw.to_string()));
}
let (lang_part, params_part) = match raw.split_once(';') {
Some((l, p)) => (l.trim(), Some(p)),
None => (raw, None),
};
if lang_part.is_empty() {
return Err(SipAcceptLanguageError::InvalidFormat(raw.to_string()));
}
Ok(SipAcceptLanguageEntry {
language: lang_part.to_ascii_lowercase(),
params: read_accept_params(params_part.unwrap_or("")),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipAcceptLanguage(Vec<SipAcceptLanguageEntry>);
impl SipAcceptLanguage {
pub fn parse(raw: &str) -> Result<Self, SipAcceptLanguageError> {
Self::from_entries(crate::split_comma_entries(raw))
}
pub fn from_entries<'a>(
entries: impl IntoIterator<Item = &'a str>,
) -> Result<Self, SipAcceptLanguageError> {
let entries: Vec<&str> = entries
.into_iter()
.collect();
if all_blank(&entries) {
return Ok(Self(Vec::new()));
}
entries
.into_iter()
.map(parse_entry)
.collect::<Result<_, _>>()
.map(Self)
}
pub fn entries(&self) -> &[SipAcceptLanguageEntry] {
&self.0
}
pub fn into_entries(self) -> Vec<SipAcceptLanguageEntry> {
self.0
}
pub fn len(&self) -> usize {
self.0
.len()
}
pub fn is_empty(&self) -> bool {
self.0
.is_empty()
}
}
impl fmt::Display for SipAcceptLanguage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
crate::fmt_joined(f, &self.0, ", ")
}
}
impl_from_str_via_parse!(SipAcceptLanguage, SipAcceptLanguageError);
impl<'a> IntoIterator for &'a SipAcceptLanguage {
type Item = &'a SipAcceptLanguageEntry;
type IntoIter = std::slice::Iter<'a, SipAcceptLanguageEntry>;
fn into_iter(self) -> Self::IntoIter {
self.0
.iter()
}
}
impl IntoIterator for SipAcceptLanguage {
type Item = SipAcceptLanguageEntry;
type IntoIter = std::vec::IntoIter<SipAcceptLanguageEntry>;
fn into_iter(self) -> Self::IntoIter {
self.0
.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_language() {
let al = SipAcceptLanguage::parse("en").unwrap();
assert_eq!(al.len(), 1);
assert_eq!(al.entries()[0].language(), "en");
}
#[test]
fn multiple_languages_with_q() {
let al = SipAcceptLanguage::parse("en;q=0.9, fr;q=0.8, *;q=0.1").unwrap();
assert_eq!(al.len(), 3);
assert_eq!(al.entries()[0].language(), "en");
assert_eq!(al.entries()[1].q(), Some("0.8"));
assert_eq!(al.entries()[2].language(), "*");
}
#[test]
fn language_subtag() {
let al = SipAcceptLanguage::parse("en-US").unwrap();
assert_eq!(al.entries()[0].language(), "en-us");
}
#[test]
fn empty_input_is_empty_list() {
assert!(SipAcceptLanguage::parse("")
.unwrap()
.is_empty());
assert!(SipAcceptLanguage::parse(" ")
.unwrap()
.is_empty());
}
#[test]
fn flag_param_roundtrip() {
let raw = "en;foo";
let al = SipAcceptLanguage::parse(raw).unwrap();
assert_eq!(al.entries()[0].param("foo"), Some(""));
assert_eq!(al.to_string(), raw);
}
#[test]
fn quoted_param_keeps_semicolon() {
let raw = r#"en;x="a;b";q=0.5"#;
let al = SipAcceptLanguage::parse(raw).unwrap();
assert_eq!(al.entries()[0].param("x"), Some(r#""a;b""#));
assert_eq!(al.entries()[0].q(), Some("0.5"));
assert_eq!(al.to_string(), raw);
}
#[test]
fn error_display_omits_input() {
let err = SipAcceptLanguage::parse(";secretvalue").unwrap_err();
assert!(!err
.to_string()
.contains("secretvalue"));
}
#[test]
fn from_str() {
let al: SipAcceptLanguage = "en"
.parse()
.unwrap();
assert_eq!(al.len(), 1);
}
#[test]
fn display_roundtrip() {
let raw = "en;q=0.9";
let al = SipAcceptLanguage::parse(raw).unwrap();
assert_eq!(al.to_string(), raw);
}
#[test]
fn from_entries_matches_parse() {
let split = SipAcceptLanguage::from_entries(["en;q=0.9", "fr-CA"]).unwrap();
let joined = SipAcceptLanguage::parse("en;q=0.9, fr-CA").unwrap();
assert_eq!(split, joined);
}
#[test]
fn from_entries_bad_entry_is_error() {
assert!(matches!(
SipAcceptLanguage::from_entries(["en", " "]),
Err(SipAcceptLanguageError::InvalidFormat(_))
));
}
#[test]
fn from_entries_empty_is_empty_list() {
assert!(SipAcceptLanguage::from_entries(std::iter::empty::<&str>())
.unwrap()
.is_empty());
assert!(SipAcceptLanguage::from_entries(["", " "])
.unwrap()
.is_empty());
}
}