use http::StatusCode;
use http::request::Parts;
use crate::extractors::FromRequest;
use crate::extractors::FromRequestParts;
use crate::responder::Responder;
use crate::types::Request;
#[derive(Debug, Clone, PartialEq)]
pub struct LanguagePreference {
pub language: String,
pub quality: f32,
}
#[derive(Debug, Clone)]
pub struct AcceptLanguage {
pub languages: Vec<LanguagePreference>,
}
#[derive(Debug)]
pub enum AcceptLanguageError {
MissingHeader,
InvalidHeader,
ParseError(String),
}
impl Responder for AcceptLanguageError {
fn into_response(self) -> crate::types::Response {
match self {
AcceptLanguageError::MissingHeader => {
(StatusCode::BAD_REQUEST, "Missing Accept-Language header").into_response()
}
AcceptLanguageError::InvalidHeader => {
(StatusCode::BAD_REQUEST, "Invalid Accept-Language header").into_response()
}
AcceptLanguageError::ParseError(err) => (
StatusCode::BAD_REQUEST,
format!("Failed to parse Accept-Language header: {err}"),
)
.into_response(),
}
}
}
impl AcceptLanguage {
pub fn new() -> Self {
Self {
languages: Vec::new(),
}
}
pub fn preferred(&self) -> Option<&LanguagePreference> {
self.languages.first()
}
pub fn preferences(&self) -> &[LanguagePreference] {
&self.languages
}
pub fn accepts(&self, language: &str) -> bool {
self.languages.iter().any(|pref| pref.language == language)
}
fn extract_from_headers(headers: &http::HeaderMap) -> Result<Self, AcceptLanguageError> {
let header_value = headers
.get("Accept-Language")
.ok_or(AcceptLanguageError::MissingHeader)?;
let header_str = header_value
.to_str()
.map_err(|_| AcceptLanguageError::InvalidHeader)?;
Self::parse_accept_language(header_str)
}
pub fn parse_accept_language(header_value: &str) -> Result<Self, AcceptLanguageError> {
let mut languages = Vec::new();
for part in header_value.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let (language, quality) = if let Some(q_pos) = part.find(";q=") {
let language = part[..q_pos].trim().to_string();
let quality_str = &part[q_pos + 3..].trim();
let quality = quality_str.parse::<f32>().map_err(|e| {
AcceptLanguageError::ParseError(format!("Invalid quality value '{quality_str}': {e}"))
})?;
if !(0.0..=1.0).contains(&quality) {
return Err(AcceptLanguageError::ParseError(format!(
"Quality value must be between 0.0 and 1.0, got: {quality}"
)));
}
(language, quality)
} else {
(part.to_string(), 1.0)
};
if !language.is_empty() {
languages.push(LanguagePreference { language, quality });
}
}
languages.sort_by(|a, b| {
b.quality
.partial_cmp(&a.quality)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(AcceptLanguage { languages })
}
}
impl Default for AcceptLanguage {
fn default() -> Self {
Self::new()
}
}
impl<'a> FromRequest<'a> for AcceptLanguage {
type Error = AcceptLanguageError;
fn from_request(
req: &'a mut Request,
) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
futures_util::future::ready(Self::extract_from_headers(req.headers()))
}
}
impl<'a> FromRequestParts<'a> for AcceptLanguage {
type Error = AcceptLanguageError;
fn from_request_parts(
parts: &'a mut Parts,
) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
futures_util::future::ready(Self::extract_from_headers(&parts.headers))
}
}