web-lang 0.1.0

Match languages with the http `Accept-Language` header.
Documentation
pub use language_tags::{LanguageTag, ParseError};

pub type Quality = f32;

/// An error while parsing a
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Error {
    Empty,
    Parse(ParseError),
}

impl From<ParseError> for Error {
    fn from(e: ParseError) -> Error {
        Error::Parse(e)
    }
}

/// A parsed language with quality.
pub struct AcceptLanguage {
    tag: LanguageTag,
    quality: Quality,
}

impl FromStr for AcceptLanguage {
    type Error = Error;

    fn from_str(s: &str) -> Result<Self, Error> {
        let s = s.trim();
        if s.is_empty() {
            return Err(Error::Empty)
        }
        if let Some((language, quality)) = s.split_once(';') {
            if let Some(quality) = quality.trim_start().strip_prefix("q") {
                // NOTE django does not allow a space between "q="
                if let Some(quality) = quality.trim_start().strip_prefix("=") {
                    return Ok(Self {
                        language: s.parse(),  // TODO trim_end()
                        quality: quality.trim_start().parse().unwrap_or(0.0),
                    })
                }
            }
        }
        Ok(Self {
            language: s.parse(),
            quality: 1.0,
        })
    }
}

/// Parse a string of accepted languages with qualities
/// (as found in the http `Accept-Language` header)
/// into a vector sorted by quality.
pub fn parse(accept_languages: &str) -> Vec<AcceptLanguage> {
    // TODO handle "*" (any lang)
    let mut languages = accept_languages.split(",")
        .map(|lang| lang.parse::<AcceptLanguage>())
        .filter(|lang| lang.ok())  // TODO accept all
        .collect();
    languages.sort_by_key(|lang| -lang.quality);
    languages
}

/// A language match.
pub struct Match<'a> {
    language: &'a LanguageTag,
    quality: Quality,
}

/// Tries to match an available language to a list of accepted languages.
///
/// This function expects the list of accepted languages
/// to be sorted by quality.
///
/// Only accepted languages with positive quality are considered.
pub fn match<'a>(
    available: &'a [LanguageTag],
    accepted: &[AcceptLanguage],
) -> Option<Match<'a>> {
    for available_lang in available {
        for accepted_lang in accepted {
            if accepted_lang.quality > 0.0 {
                if available.matches(accepted_lang.tag) {
                    return Some(Match {
                        language: available,
                        quality: accepted_lang.quality,
                    })
                }
            }
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}




pub fn language_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,
    }
}

pub fn language_prefixes(language: &str) -> impl Iterator<Item=&str> {
    std::iter::successors(
        Some(language),
        || language.split_once('-').map(|(prefix, _)| prefix)
    )
}

pub fn language_alternatives(language: &str) -> impl Iterator<Item=&str> {
    language_fallback(language).iter().chain(language_prefixes(language))
}

pub fn match_single(
    available: &'a [LanguageTag],
    accepted: &str,
) -> Option<&'a LanguageTag> {
    // TODO accepted must be lowercase
    for code in language_alternatives(accepted) {
        if code == available {
            return Some(available)
        }
    }
    None
}

struct AcceptLanguage<'a> {
    language: Option<&'a str>,
    quality: f32,
}

fn parse_accept_language_single(accept: &str) -> AcceptLanguage {
    let accept = accept.trim();
    if let Some((language, quality)) = accept.split_once("q=") {

        let language = language.trim_end_matches(|c: char| c.is_whitespace() || c == ';');
        AcceptLanguage {
            language: if language.starts_with('*') { None } else { Some(language) },
            quality: quality.trim_start().parse().unwrap_or(0.0),
        }

        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(0.0),
            }
        } else {
            // missing ";" separator: "lang-tag-q=1.0"
            // assume the "q" is part of the language
            debug_assert!(accept.len() >= language.len() + 2);
            debug_assert!(accept[language.len()..(language.len() + 1)] == 'q');
            AcceptLanguage {
                language: accept[..(language.len() + 1)],
                quality: quality.trim_start().parse().unwrap_or(1.0),
            }
        }
    } else {
        AcceptLanguage {
            language: if accept.starts_with('*') { None } else { Some(accept) },
            quality: 1.0,
        }
    }
}

fn parse_accept_language(accept: &str) -> Vec<AcceptLanguage> {
    let mut languages = accept
        .split(',')
        .filter_map(parse_accept_language_single)
        .collect();
    languages.sort_by_key(|lang| -lang.quality);
    languages
}