use std::{
iter::{Enumerate, Peekable},
str::Chars,
};
use crate::headers::{
AcceptLanguageHeader, AcceptLanguageHeaderLanguage,
accept_header::{AcceptHeader, AcceptHeaderItem},
};
pub struct HeaderParser<'a> {
text: &'a str,
iter: Peekable<Enumerate<Chars<'a>>>,
}
impl<'a> HeaderParser<'a> {
pub fn new(text: &'a str) -> Self {
Self {
text,
iter: text.chars().enumerate().peekable(),
}
}
fn parse_number(&mut self) -> Option<f32> {
let (start, _) = *self.iter.peek()?;
let mut before_decimal = true;
let mut length: usize = 0;
while let Some((_, c)) = self.iter.peek() {
match c {
'0'..='9' => { }
'.' if before_decimal => {
before_decimal = false;
}
_ => {
break;
}
}
self.iter.next();
length += 1;
}
if length == 0 {
return None;
}
self.text[start..(start + length)].parse().ok()
}
fn skip_spaces(&mut self) {
while let Some((_, c)) = self.iter.peek() {
if *c == ' ' {
self.iter.next();
} else {
break;
}
}
}
fn peek_char(&mut self, c: char) -> bool {
match self.iter.peek() {
Some((_, cc)) => *cc == c,
None => false,
}
}
fn expect_char(&mut self, c: char) -> bool {
match self.iter.next() {
Some((_, cc)) => cc == c,
None => false,
}
}
fn peek_is_end_of_item(&mut self) -> bool {
matches!(self.iter.peek(), None | Some((_, ',')) | Some((_, ';')))
}
fn parse_optional_quality_parameter(&mut self) -> Option<f32> {
if self.peek_char(';') {
self.iter.next();
if self.expect_char('q')
&& self.expect_char('=')
&& let Some(num) = self.parse_number()
{
Some(num)
} else {
None
}
} else {
Some(1.0)
}
}
fn parse_language_code(&mut self) -> Option<AcceptLanguageHeaderLanguage> {
let (start, c) = *self.iter.peek()?;
if c == '*' {
self.iter.next();
if self.peek_is_end_of_item() {
return Some(AcceptLanguageHeaderLanguage::Wildcard);
} else {
return None;
}
}
let mut length: usize = 0;
while let Some((_, c)) = self.iter.peek() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' => {
length += 1;
}
';' | ',' => {
break;
}
_ => {
return None;
}
}
self.iter.next();
}
if length == 0 {
return None;
}
Some(AcceptLanguageHeaderLanguage::Code(
self.text[start..(start + length)].to_owned(),
))
}
fn parse_media_type_component(&mut self) -> Option<usize> {
let mut length: usize = 0;
while let Some((_, c)) = self.iter.peek() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' => {
length += 1;
}
';' | ',' | '/' | '+' => {
break;
}
_ => {
return None;
}
}
self.iter.next();
}
if length == 0 { None } else { Some(length) }
}
pub fn parse_accept_header_item(&mut self) -> Option<AcceptHeaderItem> {
let (start, c) = *self.iter.peek()?;
if c == '*' {
self.iter.next();
if self.expect_char('/') && self.expect_char('*') && self.peek_is_end_of_item() {
return Some(AcceptHeaderItem::Wildcard);
} else {
return None;
}
}
if let Some(length) = self.parse_media_type_component()
&& self.expect_char('/')
{
if self.peek_char('*') {
self.iter.next();
if self.peek_is_end_of_item() {
return Some(AcceptHeaderItem::PartialWildcard(
self.text[start..(start + length)].to_owned(),
));
} else {
return None;
}
}
} else {
return None;
}
self.parse_media_type_component()?;
if self.peek_char('+') {
self.iter.next();
self.parse_media_type_component()?;
}
let type_string = if let Some((offset, _)) = self.iter.peek() {
self.text[start..=(*offset - 1)].to_owned()
} else {
self.text[start..].to_owned()
};
Some(AcceptHeaderItem::MediaType(type_string))
}
pub fn is_at_end(&mut self) -> bool {
self.iter.peek().is_none()
}
pub fn parse_accept_language(&mut self) -> AcceptLanguageHeader {
let mut outputs = Vec::new();
while self.iter.peek().is_some() {
self.skip_spaces();
let language = if let Some(l) = self.parse_language_code() {
l
} else {
break;
};
let q = if let Some(q) = self.parse_optional_quality_parameter() {
q
} else {
break;
};
self.skip_spaces();
outputs.push((language, q));
if self.peek_char(',') {
self.iter.next();
}
}
outputs.sort_by(|(_, a), (_, b)| b.total_cmp(a));
return AcceptLanguageHeader { languages: outputs };
}
pub fn parse_accept(&mut self) -> AcceptHeader {
let mut outputs = Vec::new();
while self.iter.peek().is_some() {
self.skip_spaces();
let media_type = if let Some(t) = self.parse_accept_header_item() {
t
} else {
break;
};
let q = if let Some(q) = self.parse_optional_quality_parameter() {
q
} else {
break;
};
self.skip_spaces();
outputs.push((media_type, q));
if self.peek_char(',') {
self.iter.next();
}
}
outputs.sort_by(|(_, a), (_, b)| b.total_cmp(a));
return AcceptHeader { items: outputs };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_accept_language_header() {
assert_eq!(
HeaderParser::new("en-US, de-DE, *").parse_accept_language(),
AcceptLanguageHeader {
languages: vec![
(AcceptLanguageHeaderLanguage::Code("en-US".to_owned()), 1.0),
(AcceptLanguageHeaderLanguage::Code("de-DE".to_owned()), 1.0),
(AcceptLanguageHeaderLanguage::Wildcard, 1.0)
]
}
);
assert_eq!(
HeaderParser::new("").parse_accept_language(),
AcceptLanguageHeader { languages: vec![] }
);
assert_eq!(
HeaderParser::new("ja").parse_accept_language(),
AcceptLanguageHeader {
languages: vec![(AcceptLanguageHeaderLanguage::Code("ja".to_owned()), 1.0)]
}
);
assert_eq!(
HeaderParser::new("da, en-gb;q=0.8, en;q=0.7").parse_accept_language(),
AcceptLanguageHeader {
languages: vec![
(AcceptLanguageHeaderLanguage::Code("da".to_owned()), 1.0),
(AcceptLanguageHeaderLanguage::Code("en-gb".to_owned()), 0.8),
(AcceptLanguageHeaderLanguage::Code("en".to_owned()), 0.7),
]
}
);
assert_eq!(
HeaderParser::new("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5")
.parse_accept_language(),
AcceptLanguageHeader {
languages: vec![
(AcceptLanguageHeaderLanguage::Code("fr-CH".to_owned()), 1.0),
(AcceptLanguageHeaderLanguage::Code("fr".to_owned()), 0.9),
(AcceptLanguageHeaderLanguage::Code("en".to_owned()), 0.8),
(AcceptLanguageHeaderLanguage::Code("de".to_owned()), 0.7),
(AcceptLanguageHeaderLanguage::Wildcard, 0.5),
]
}
);
assert_eq!(
HeaderParser::new("en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7").parse_accept_language(),
AcceptLanguageHeader {
languages: vec![
(AcceptLanguageHeaderLanguage::Code("en-US".to_owned()), 1.0),
(AcceptLanguageHeaderLanguage::Code("en".to_owned()), 0.9),
(AcceptLanguageHeaderLanguage::Code("zh-CN".to_owned()), 0.8),
(AcceptLanguageHeaderLanguage::Code("zh".to_owned()), 0.7),
]
}
);
}
#[test]
fn test_parse_accept_header() {
assert_eq!(
HeaderParser::new("*/*").parse_accept(),
AcceptHeader {
items: vec![(AcceptHeaderItem::Wildcard, 1.0),]
}
);
assert_eq!(
HeaderParser::new(" ").parse_accept(),
AcceptHeader { items: vec![] }
);
assert_eq!(
HeaderParser::new("").parse_accept(),
AcceptHeader { items: vec![] }
);
assert_eq!(
HeaderParser::new(",").parse_accept(),
AcceptHeader { items: vec![] }
);
assert_eq!(
HeaderParser::new(
"text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
)
.parse_accept(),
AcceptHeader {
items: vec![
(AcceptHeaderItem::MediaType("text/html".to_owned()), 1.0),
(
AcceptHeaderItem::MediaType("application/xhtml+xml".to_owned()),
1.0
),
(AcceptHeaderItem::MediaType("image/webp".to_owned()), 1.0),
(
AcceptHeaderItem::MediaType("application/xml".to_owned()),
0.9
),
(AcceptHeaderItem::Wildcard, 0.8),
]
}
);
assert_eq!(
HeaderParser::new("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.parse_accept(),
AcceptHeader {
items: vec![
(AcceptHeaderItem::MediaType("text/html".to_owned()), 1.0),
(
AcceptHeaderItem::MediaType("application/xhtml+xml".to_owned()),
1.0
),
(
AcceptHeaderItem::MediaType("application/xml".to_owned()),
0.9
),
(AcceptHeaderItem::Wildcard, 0.8),
]
}
);
assert_eq!(
HeaderParser::new(
"image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"
)
.parse_accept(),
AcceptHeader {
items: vec![
(AcceptHeaderItem::MediaType("image/avif".to_owned()), 1.0),
(AcceptHeaderItem::MediaType("image/webp".to_owned()), 1.0),
(AcceptHeaderItem::MediaType("image/png".to_owned()), 1.0),
(AcceptHeaderItem::MediaType("image/svg+xml".to_owned()), 1.0),
(AcceptHeaderItem::PartialWildcard("image".to_owned()), 0.8),
(AcceptHeaderItem::Wildcard, 0.5),
]
}
);
assert_eq!(
HeaderParser::new("application/json").parse_accept(),
AcceptHeader {
items: vec![(
AcceptHeaderItem::MediaType("application/json".to_owned()),
1.0
),]
}
);
}
}