#![forbid(unsafe_code)]
#[derive(Clone, Copy, Debug)]
struct Quality(f32);
impl Ord for Quality {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.total_cmp(&other.0)
}
}
impl PartialOrd for Quality {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Quality {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for Quality {}
impl PartialEq<f32> for Quality {
fn eq(&self, other: &f32) -> bool {
self.0 == *other
}
}
impl From<Quality> for f32 {
fn from(q: Quality) -> f32 {
q.0
}
}
impl TryFrom<f32> for Quality {
type Error = ();
fn try_from(f: f32) -> Result<Quality, ()> {
if f.is_finite() {
Ok(Quality(f))
} else {
Err(())
}
}
}
impl std::str::FromStr for Quality {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value: f32 = s.parse().map_err(|_| ())?;
if value.is_finite() {
Ok(Quality(value))
} else {
Err(())
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
struct AcceptLanguage<'a> {
pub language: Option<&'a str>,
pub quality: Quality,
}
fn parse_accept_single(accept: &str) -> AcceptLanguage<'_> {
if let Some((language, quality)) = accept.split_once("q=") {
let language = language.trim_end();
if let Some(language) = language.strip_suffix(';') {
let language = language.trim_end();
AcceptLanguage {
language: if language.starts_with('*') {
None
} else {
Some(language)
},
quality: quality.trim_start().parse().unwrap_or(Quality(0.0)),
}
} else {
debug_assert!(accept.len() >= language.len() + 2);
debug_assert!(&accept[language.len()..(language.len() + 1)] == "q");
let language = &accept[..(language.len() + 1)];
AcceptLanguage {
language: if language.starts_with('*') {
None
} else {
Some(language)
},
quality: quality.trim_start().parse().unwrap_or(Quality(1.0)),
}
}
} else if accept.is_empty() {
AcceptLanguage {
language: None,
quality: Quality(0.0),
}
} else {
AcceptLanguage {
language: if accept.starts_with('*') {
None
} else {
Some(accept)
},
quality: Quality(1.0),
}
}
}
fn parse_accept(accept: &str) -> Vec<AcceptLanguage<'_>> {
let mut languages = accept
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(parse_accept_single)
.collect::<Vec<_>>();
languages.sort_by_key(|lang| Quality(-lang.quality.0));
languages
}
fn fallback(language: &str) -> Option<&'static str> {
match language {
"zh-cn" => Some("zh-hans"),
"zh-hk" => Some("zh-hant"),
"zh-mo" => Some("zh-hant"),
"zh-my" => Some("zh-hans"),
"zh-sg" => Some("zh-hans"),
"zh-tw" => Some("zh-hant"),
_ => None,
}
}
fn prefixes(language: &str) -> impl Iterator<Item = &str> {
std::iter::successors(Some(language), |language| {
language.rsplit_once('-').map(|(prefix, _)| prefix)
})
}
fn alternatives(language: &str) -> impl Iterator<Item = &str> {
std::iter::once(language)
.chain(fallback(language))
.chain(prefixes(language).skip(1))
}
pub trait Language {
fn tag(&self) -> &str;
}
impl Language for &str {
fn tag(&self) -> &str {
self
}
}
impl Language for String {
fn tag(&self) -> &str {
self
}
}
pub fn match_lang<L: Language>(
available: impl IntoIterator<Item = L> + Clone,
accept: &str,
) -> Option<L> {
for accept_lang in alternatives(accept) {
for avail in available.clone() {
if accept_lang.eq_ignore_ascii_case(avail.tag()) {
return Some(avail);
}
}
}
None
}
fn match_multi<L: Language>(
available: impl IntoIterator<Item = L> + Clone,
accepted: &[AcceptLanguage<'_>],
) -> Option<L> {
for accept in accepted {
if accept.quality <= Quality(0.0) {
return None;
}
if let Some(accept_lang) = accept.language {
let r#match = match_lang(available.clone(), accept_lang);
if let Some(language) = r#match {
return Some(language);
}
} else {
return None;
}
}
None
}
pub fn match_accept<L: Language>(
available: impl IntoIterator<Item = L> + Clone,
accept: &str,
) -> Option<L> {
match_multi(available, &parse_accept(accept))
}
#[cfg(test)]
mod tests {
use super::{
alternatives, match_accept, match_lang, parse_accept,
parse_accept_single, prefixes, AcceptLanguage, Quality,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Language {
English,
German,
Japanese,
}
impl super::Language for Language {
fn tag(&self) -> &str {
match self {
Self::English => "en",
Self::German => "de",
Self::Japanese => "ja",
}
}
}
const LANGUAGES: &[Language] =
&[Language::English, Language::German, Language::Japanese];
#[test]
fn quality_cmp() {
assert_eq!(Quality(0.5), Quality(0.5));
assert_ne!(Quality(0.5), Quality(0.6));
assert!(Quality(0.5) < Quality(0.6));
assert!(Quality(0.7) > Quality(0.6));
assert!(Quality(0.5) <= Quality(0.6));
assert!(Quality(0.7) >= Quality(0.6));
assert!(Quality(0.5) >= Quality(0.5));
assert!(Quality(0.5) <= Quality(0.5));
}
#[test]
fn quality_cmp_float() {
assert_eq!(Quality(0.5), 0.5);
assert_ne!(Quality(0.5), 0.6);
}
#[test]
fn quality_clone() {
let quality = Quality(0.5);
assert_eq!(quality, quality.clone());
}
#[test]
fn quality_debug() {
assert_eq!(format!("{:?}", Quality(0.5)), "Quality(0.5)");
}
#[test]
fn quality_into_float() {
assert_eq!(f32::from(Quality(0.5)), 0.5);
}
#[test]
fn quality_from_float() {
assert_eq!(Quality::try_from(0.5), Ok(Quality(0.5)));
assert_eq!(Quality::try_from(f32::INFINITY), Err(()));
assert_eq!(Quality::try_from(f32::NEG_INFINITY), Err(()));
assert_eq!(Quality::try_from(f32::NAN), Err(()));
}
#[test]
fn test_parse_accept_single() {
assert_eq!(
parse_accept_single("en"),
AcceptLanguage {
language: Some("en"),
quality: Quality(1.0),
}
);
assert_eq!(
parse_accept_single("en;q=0.5"),
AcceptLanguage {
language: Some("en"),
quality: Quality(0.5),
}
);
assert_eq!(
parse_accept_single("*"),
AcceptLanguage {
language: None,
quality: Quality(1.0),
}
);
assert_eq!(
parse_accept_single("*x"),
AcceptLanguage {
language: None,
quality: Quality(1.0),
}
);
assert_eq!(
parse_accept_single("*;q=0.5"),
AcceptLanguage {
language: None,
quality: Quality(0.5),
}
);
assert_eq!(
parse_accept_single("q=0.5"),
AcceptLanguage {
language: Some("q"),
quality: Quality(0.5),
}
);
assert_eq!(
parse_accept_single("*q=0.5"),
AcceptLanguage {
language: None,
quality: Quality(0.5),
}
);
assert_eq!(
parse_accept_single("en;q=x"),
AcceptLanguage {
language: Some("en"),
quality: Quality(0.0),
}
);
assert_eq!(
parse_accept_single("en;q=inf"),
AcceptLanguage {
language: Some("en"),
quality: Quality(0.0),
}
);
assert_eq!(
parse_accept_single("en;q=nan"),
AcceptLanguage {
language: Some("en"),
quality: Quality(0.0),
}
);
assert_eq!(
parse_accept_single(""),
AcceptLanguage {
language: None,
quality: Quality(0.0),
}
);
}
#[test]
fn test_parse_accept() {
assert_eq!(
parse_accept("en, ja;q=0.2, , de;q=0.5, *;q=0.1"),
vec![
AcceptLanguage {
language: Some("en"),
quality: Quality(1.0),
},
AcceptLanguage {
language: Some("de"),
quality: Quality(0.5),
},
AcceptLanguage {
language: Some("ja"),
quality: Quality(0.2),
},
AcceptLanguage {
language: None,
quality: Quality(0.1),
},
]
);
}
#[test]
fn test_prefixes() {
assert_eq!(
prefixes("a-b-c").collect::<Vec<_>>(),
vec!["a-b-c", "a-b", "a"],
)
}
#[test]
fn test_alternatives() {
assert_eq!(
alternatives("a-b-c").collect::<Vec<_>>(),
vec!["a-b-c", "a-b", "a"]
);
assert_eq!(
alternatives("zh-cn").collect::<Vec<_>>(),
vec!["zh-cn", "zh-hans", "zh"]
);
}
#[test]
fn test_match_lang() {
let languages = LANGUAGES.iter().copied();
assert_eq!(
match_lang(languages.clone(), "en"),
Some(Language::English)
);
assert_eq!(
match_lang(languages.clone(), "en-gb"),
Some(Language::English)
);
assert_eq!(match_lang(languages.clone(), "de"), Some(Language::German));
assert_eq!(
match_lang(languages.clone(), "de-de"),
Some(Language::German)
);
assert_eq!(
match_lang(languages.clone(), "ja"),
Some(Language::Japanese)
);
assert_eq!(
match_lang(languages.clone(), "ja-ja"),
Some(Language::Japanese)
);
assert_eq!(match_lang(languages.clone(), "fi"), None);
assert_eq!(match_lang(languages.clone(), ""), None);
}
#[test]
fn test_match_lang_case() {
assert_eq!(match_lang(["DE", "EN", "JA"], "en"), Some("EN"));
}
#[test]
fn test_match_lang_string() {
assert_eq!(
match_lang(["de".to_string(), "en".to_string()], "en"),
Some("en".to_string())
);
}
#[test]
fn test_match_accept() {
let languages = LANGUAGES.iter().copied();
assert_eq!(
match_accept(languages.clone(), "en"),
Some(Language::English)
);
assert_eq!(
match_accept(languages.clone(), "en, de, ja"),
Some(Language::English)
);
assert_eq!(
match_accept(languages.clone(), "en-gb, de, ja"),
Some(Language::English)
);
assert_eq!(
match_accept(languages.clone(), "de"),
Some(Language::German)
);
assert_eq!(
match_accept(languages.clone(), "en;q=0.1, de;q=0.9"),
Some(Language::German)
);
assert_eq!(match_accept(languages.clone(), ""), None);
assert_eq!(match_accept(languages.clone(), "fi"), None);
assert_eq!(match_accept(languages.clone(), "en;q=-1"), None);
assert_eq!(match_accept(languages.clone(), "de;q=0.5, *;q=0.8"), None);
}
}