use crate::{Error, HttpRequest, HttpResponse};
use serde::Serialize;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaType {
pub type_: String,
pub subtype: String,
pub params: HashMap<String, String>,
}
impl MediaType {
pub fn new(type_: impl Into<String>, subtype: impl Into<String>) -> Self {
Self {
type_: type_.into(),
subtype: subtype.into(),
params: HashMap::new(),
}
}
pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.params.insert(key.into(), value.into());
self
}
pub fn json() -> Self {
Self::new("application", "json")
}
pub fn html() -> Self {
Self::new("text", "html")
}
pub fn plain_text() -> Self {
Self::new("text", "plain")
}
pub fn xml() -> Self {
Self::new("application", "xml")
}
pub fn text_xml() -> Self {
Self::new("text", "xml")
}
pub fn form_urlencoded() -> Self {
Self::new("application", "x-www-form-urlencoded")
}
pub fn multipart_form_data() -> Self {
Self::new("multipart", "form-data")
}
pub fn octet_stream() -> Self {
Self::new("application", "octet-stream")
}
pub fn any() -> Self {
Self::new("*", "*")
}
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
let mut parts = s.split(';');
let type_subtype = parts.next()?.trim();
let mut type_parts = type_subtype.splitn(2, '/');
let type_ = type_parts.next()?.trim().to_lowercase();
let subtype = type_parts.next()?.trim().to_lowercase();
let mut params = HashMap::new();
for param in parts {
let param = param.trim();
if let Some((key, value)) = param.split_once('=') {
let key = key.trim().to_lowercase();
let value = value.trim().trim_matches('"').to_string();
if key != "q" {
params.insert(key, value);
}
}
}
Some(Self {
type_,
subtype,
params,
})
}
pub fn matches(&self, other: &MediaType) -> bool {
let type_matches = self.type_ == "*" || other.type_ == "*" || self.type_ == other.type_;
let subtype_matches =
self.subtype == "*" || other.subtype == "*" || self.subtype == other.subtype;
type_matches && subtype_matches
}
pub fn is_any(&self) -> bool {
self.type_ == "*" && self.subtype == "*"
}
pub fn is_type_wildcard(&self) -> bool {
self.type_ == "*"
}
pub fn is_subtype_wildcard(&self) -> bool {
self.subtype == "*"
}
pub fn mime_type(&self) -> String {
format!("{}/{}", self.type_, self.subtype)
}
pub fn to_header_value(&self) -> String {
let mut result = self.mime_type();
for (key, value) in &self.params {
result.push_str(&format!("; {}={}", key, value));
}
result
}
}
impl fmt::Display for MediaType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_header_value())
}
}
fn find_quality_param(s: &str) -> Option<usize> {
s.as_bytes()
.windows(3)
.position(|w| w[0] == b';' && w[1].eq_ignore_ascii_case(&b'q') && w[2] == b'=')
}
#[derive(Debug, Clone)]
pub struct Accept {
pub media_types: Vec<(MediaType, f32)>,
}
impl Default for Accept {
fn default() -> Self {
Self::new()
}
}
impl Accept {
pub fn new() -> Self {
Self {
media_types: vec![(MediaType::any(), 1.0)],
}
}
pub fn parse(header: &str) -> Self {
let mut media_types: Vec<(MediaType, f32)> = header
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
let (media_part, quality) = Self::extract_quality(part);
MediaType::parse(media_part).map(|mt| (mt, quality))
})
.collect();
media_types.sort_by(|a, b| {
match b.1.partial_cmp(&a.1) {
Some(Ordering::Equal) | None => {}
Some(ord) => return ord,
}
let a_specificity = Self::specificity(&a.0);
let b_specificity = Self::specificity(&b.0);
b_specificity.cmp(&a_specificity)
});
Self { media_types }
}
fn extract_quality(s: &str) -> (&str, f32) {
if let Some(q_pos) = find_quality_param(s) {
let media_part = &s[..q_pos];
let q_part = &s[q_pos + 3..];
let quality = q_part
.split(';')
.next()
.and_then(|q| q.trim().parse::<f32>().ok())
.unwrap_or(1.0)
.clamp(0.0, 1.0);
(media_part, quality)
} else {
(s, 1.0)
}
}
fn specificity(mt: &MediaType) -> u8 {
let mut score = 0u8;
if mt.type_ != "*" {
score += 2;
}
if mt.subtype != "*" {
score += 1;
}
score
}
pub fn accepts(&self, media_type: &MediaType) -> bool {
self.quality_for(media_type) > 0.0
}
pub fn quality_for(&self, media_type: &MediaType) -> f32 {
for (mt, quality) in &self.media_types {
if mt.matches(media_type) {
return *quality;
}
}
0.0
}
pub fn preferred(&self) -> Option<&MediaType> {
self.media_types.first().map(|(mt, _)| mt)
}
pub fn prefers_json(&self) -> bool {
self.quality_for(&MediaType::json()) > self.quality_for(&MediaType::html())
}
pub fn prefers_html(&self) -> bool {
self.quality_for(&MediaType::html()) > self.quality_for(&MediaType::json())
}
}
pub fn negotiate_media_type<'a>(
accept: &Accept,
available: &'a [MediaType],
) -> Option<&'a MediaType> {
let mut best: Option<(&'a MediaType, f32, u8)> = None;
for available_mt in available {
let quality = accept.quality_for(available_mt);
if quality > 0.0 {
let specificity = Accept::specificity(available_mt);
match &best {
None => best = Some((available_mt, quality, specificity)),
Some((_, best_q, best_s)) => {
if quality > *best_q || (quality == *best_q && specificity > *best_s) {
best = Some((available_mt, quality, specificity));
}
}
}
}
}
best.map(|(mt, _, _)| mt)
}
#[derive(Debug, Clone, PartialEq)]
pub struct LanguageTag {
pub primary: String,
pub subtag: Option<String>,
}
impl LanguageTag {
pub fn new(primary: impl Into<String>) -> Self {
Self {
primary: primary.into().to_lowercase(),
subtag: None,
}
}
pub fn with_subtag(primary: impl Into<String>, subtag: impl Into<String>) -> Self {
Self {
primary: primary.into().to_lowercase(),
subtag: Some(subtag.into().to_uppercase()),
}
}
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
if s.is_empty() || s == "*" {
return Some(Self::new("*"));
}
let mut parts = s.splitn(2, '-');
let primary = parts.next()?.trim().to_lowercase();
let subtag = parts.next().map(|s| s.trim().to_uppercase());
Some(Self { primary, subtag })
}
pub fn matches(&self, other: &LanguageTag) -> bool {
if self.primary == "*" || other.primary == "*" {
return true;
}
if self.primary != other.primary {
return false;
}
match (&self.subtag, &other.subtag) {
(Some(a), Some(b)) => a == b,
(None, _) => true, (Some(_), None) => false, }
}
}
impl fmt::Display for LanguageTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.subtag {
Some(sub) => write!(f, "{}-{}", self.primary, sub),
None => write!(f, "{}", self.primary),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AcceptLanguage {
pub languages: Vec<(LanguageTag, f32)>,
}
impl AcceptLanguage {
pub fn parse(header: &str) -> Self {
let mut languages: Vec<(LanguageTag, f32)> = header
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
let (lang_part, quality) = Self::extract_quality(part);
LanguageTag::parse(lang_part).map(|lt| (lt, quality))
})
.collect();
languages.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
Self { languages }
}
fn extract_quality(s: &str) -> (&str, f32) {
if let Some(q_pos) = find_quality_param(s) {
let lang_part = &s[..q_pos];
let q_part = &s[q_pos + 3..];
let quality = q_part.trim().parse::<f32>().unwrap_or(1.0).clamp(0.0, 1.0);
(lang_part, quality)
} else {
(s, 1.0)
}
}
pub fn quality_for(&self, language: &LanguageTag) -> f32 {
for (lt, quality) in &self.languages {
if lt.matches(language) {
return *quality;
}
}
0.0
}
pub fn preferred(&self) -> Option<&LanguageTag> {
self.languages.first().map(|(lt, _)| lt)
}
}
pub fn negotiate_language<'a>(
accept: &AcceptLanguage,
available: &'a [LanguageTag],
) -> Option<&'a LanguageTag> {
let mut best: Option<(&'a LanguageTag, f32)> = None;
for available_lt in available {
let quality = accept.quality_for(available_lt);
if quality > 0.0 {
match &best {
None => best = Some((available_lt, quality)),
Some((_, best_q)) if quality > *best_q => {
best = Some((available_lt, quality));
}
_ => {}
}
}
}
best.map(|(lt, _)| lt)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Encoding {
Gzip,
Deflate,
Brotli,
Zstd,
Identity,
}
impl Encoding {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"gzip" | "x-gzip" => Some(Self::Gzip),
"deflate" => Some(Self::Deflate),
"br" => Some(Self::Brotli),
"zstd" => Some(Self::Zstd),
"identity" => Some(Self::Identity),
_ => None,
}
}
pub fn to_header_value(&self) -> &'static str {
match self {
Self::Gzip => "gzip",
Self::Deflate => "deflate",
Self::Brotli => "br",
Self::Zstd => "zstd",
Self::Identity => "identity",
}
}
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_header_value())
}
}
#[derive(Debug, Clone, Default)]
pub struct AcceptEncoding {
pub encodings: Vec<(Encoding, f32)>,
}
impl AcceptEncoding {
pub fn parse(header: &str) -> Self {
let mut encodings: Vec<(Encoding, f32)> = header
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
let (enc_part, quality) = Self::extract_quality(part);
Encoding::parse(enc_part).map(|enc| (enc, quality))
})
.collect();
encodings.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
Self { encodings }
}
fn extract_quality(s: &str) -> (&str, f32) {
if let Some(q_pos) = find_quality_param(s) {
let enc_part = &s[..q_pos];
let q_part = &s[q_pos + 3..];
let quality = q_part.trim().parse::<f32>().unwrap_or(1.0).clamp(0.0, 1.0);
(enc_part, quality)
} else {
(s, 1.0)
}
}
pub fn quality_for(&self, encoding: Encoding) -> f32 {
for (enc, quality) in &self.encodings {
if *enc == encoding {
return *quality;
}
}
0.0
}
pub fn preferred(&self) -> Option<Encoding> {
self.encodings.first().map(|(enc, _)| *enc)
}
pub fn accepts(&self, encoding: Encoding) -> bool {
self.quality_for(encoding) > 0.0
}
}
pub fn negotiate_encoding(accept: &AcceptEncoding, available: &[Encoding]) -> Option<Encoding> {
let mut best: Option<(Encoding, f32)> = None;
for &enc in available {
let quality = accept.quality_for(enc);
if quality > 0.0 {
match &best {
None => best = Some((enc, quality)),
Some((_, best_q)) if quality > *best_q => {
best = Some((enc, quality));
}
_ => {}
}
}
}
best.map(|(enc, _)| enc)
}
#[derive(Debug, Clone, Default)]
pub struct AcceptCharset {
pub charsets: Vec<(String, f32)>,
}
impl AcceptCharset {
pub fn parse(header: &str) -> Self {
let mut charsets: Vec<(String, f32)> = header
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
let (charset_part, quality) = Self::extract_quality(part);
Some((charset_part.trim().to_lowercase(), quality))
})
.collect();
charsets.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
Self { charsets }
}
fn extract_quality(s: &str) -> (&str, f32) {
if let Some(q_pos) = find_quality_param(s) {
let charset_part = &s[..q_pos];
let q_part = &s[q_pos + 3..];
let quality = q_part.trim().parse::<f32>().unwrap_or(1.0).clamp(0.0, 1.0);
(charset_part, quality)
} else {
(s, 1.0)
}
}
pub fn quality_for(&self, charset: &str) -> f32 {
let charset = charset.to_lowercase();
for (cs, quality) in &self.charsets {
if cs == &charset || cs == "*" {
return *quality;
}
}
if charset == "utf-8" {
return 1.0;
}
0.0
}
pub fn preferred(&self) -> Option<&str> {
self.charsets.first().map(|(cs, _)| cs.as_str())
}
}
impl HttpRequest {
pub fn accept(&self) -> Accept {
self.headers
.get("Accept")
.or_else(|| self.headers.get("accept"))
.map(|h| Accept::parse(h))
.unwrap_or_default()
}
pub fn accept_language(&self) -> AcceptLanguage {
self.headers
.get("Accept-Language")
.or_else(|| self.headers.get("accept-language"))
.map(|h| AcceptLanguage::parse(h))
.unwrap_or_default()
}
pub fn accept_encoding(&self) -> AcceptEncoding {
self.headers
.get("Accept-Encoding")
.or_else(|| self.headers.get("accept-encoding"))
.map(|h| AcceptEncoding::parse(h))
.unwrap_or_default()
}
pub fn accept_charset(&self) -> AcceptCharset {
self.headers
.get("Accept-Charset")
.or_else(|| self.headers.get("accept-charset"))
.map(|h| AcceptCharset::parse(h))
.unwrap_or_default()
}
pub fn accepts(&self, media_type: &MediaType) -> bool {
self.accept().accepts(media_type)
}
pub fn prefers_json(&self) -> bool {
self.accept().prefers_json()
}
pub fn prefers_html(&self) -> bool {
self.accept().prefers_html()
}
pub fn negotiate_media_type<'a>(&self, available: &'a [MediaType]) -> Option<&'a MediaType> {
negotiate_media_type(&self.accept(), available)
}
pub fn negotiate_language<'a>(&self, available: &'a [LanguageTag]) -> Option<&'a LanguageTag> {
negotiate_language(&self.accept_language(), available)
}
pub fn negotiate_encoding(&self, available: &[Encoding]) -> Option<Encoding> {
negotiate_encoding(&self.accept_encoding(), available)
}
}
pub struct ContentNegotiator<J, H, T, X>
where
J: FnOnce() -> serde_json::Value,
H: FnOnce() -> String,
T: FnOnce() -> String,
X: FnOnce() -> String,
{
json_fn: Option<J>,
html_fn: Option<H>,
text_fn: Option<T>,
xml_fn: Option<X>,
default_media_type: MediaType,
}
impl ContentNegotiator<fn() -> serde_json::Value, fn() -> String, fn() -> String, fn() -> String> {
pub fn new() -> Self {
Self {
json_fn: None,
html_fn: None,
text_fn: None,
xml_fn: None,
default_media_type: MediaType::json(),
}
}
}
impl<J, H, T, X> ContentNegotiator<J, H, T, X>
where
J: FnOnce() -> serde_json::Value,
H: FnOnce() -> String,
T: FnOnce() -> String,
X: FnOnce() -> String,
{
pub fn json<NJ: FnOnce() -> serde_json::Value>(self, f: NJ) -> ContentNegotiator<NJ, H, T, X> {
ContentNegotiator {
json_fn: Some(f),
html_fn: self.html_fn,
text_fn: self.text_fn,
xml_fn: self.xml_fn,
default_media_type: self.default_media_type,
}
}
pub fn html<NH: FnOnce() -> String>(self, f: NH) -> ContentNegotiator<J, NH, T, X> {
ContentNegotiator {
json_fn: self.json_fn,
html_fn: Some(f),
text_fn: self.text_fn,
xml_fn: self.xml_fn,
default_media_type: self.default_media_type,
}
}
pub fn plain_text<NT: FnOnce() -> String>(self, f: NT) -> ContentNegotiator<J, H, NT, X> {
ContentNegotiator {
json_fn: self.json_fn,
html_fn: self.html_fn,
text_fn: Some(f),
xml_fn: self.xml_fn,
default_media_type: self.default_media_type,
}
}
pub fn xml<NX: FnOnce() -> String>(self, f: NX) -> ContentNegotiator<J, H, T, NX> {
ContentNegotiator {
json_fn: self.json_fn,
html_fn: self.html_fn,
text_fn: self.text_fn,
xml_fn: Some(f),
default_media_type: self.default_media_type,
}
}
pub fn default_to(mut self, media_type: MediaType) -> Self {
self.default_media_type = media_type;
self
}
pub fn negotiate(self, request: &HttpRequest) -> Result<HttpResponse, Error> {
let accept = request.accept();
let mut available = Vec::new();
if self.json_fn.is_some() {
available.push(MediaType::json());
}
if self.html_fn.is_some() {
available.push(MediaType::html());
}
if self.text_fn.is_some() {
available.push(MediaType::plain_text());
}
if self.xml_fn.is_some() {
available.push(MediaType::xml());
}
if available.is_empty() {
return Err(Error::Internal(
"No response formats configured".to_string(),
));
}
let best = negotiate_media_type(&accept, &available)
.cloned()
.unwrap_or_else(|| self.default_media_type.clone());
let mut response = HttpResponse::ok();
if best.matches(&MediaType::json()) {
if let Some(f) = self.json_fn {
let value = f();
let body =
serde_json::to_vec(&value).map_err(|e| Error::Serialization(e.to_string()))?;
response.body = body;
response
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
}
} else if best.matches(&MediaType::html()) {
if let Some(f) = self.html_fn {
let html = f();
response.body = html.into_bytes();
response.headers.insert(
"Content-Type".to_string(),
"text/html; charset=utf-8".to_string(),
);
}
} else if best.matches(&MediaType::plain_text()) {
if let Some(f) = self.text_fn {
let text = f();
response.body = text.into_bytes();
response.headers.insert(
"Content-Type".to_string(),
"text/plain; charset=utf-8".to_string(),
);
}
} else if best.matches(&MediaType::xml()) {
if let Some(f) = self.xml_fn {
let xml = f();
response.body = xml.into_bytes();
response.headers.insert(
"Content-Type".to_string(),
"application/xml; charset=utf-8".to_string(),
);
}
} else {
return Err(Error::NotAcceptable(format!(
"Cannot produce response in requested format: {}",
best
)));
}
response
.headers
.insert("Vary".to_string(), "Accept".to_string());
Ok(response)
}
}
impl Default
for ContentNegotiator<fn() -> serde_json::Value, fn() -> String, fn() -> String, fn() -> String>
{
fn default() -> Self {
Self::new()
}
}
pub fn respond_with<T: Serialize>(request: &HttpRequest, data: &T) -> Result<HttpResponse, Error> {
let accept = request.accept();
let mut response = HttpResponse::ok();
if accept.prefers_html() {
let json =
serde_json::to_string_pretty(data).map_err(|e| Error::Serialization(e.to_string()))?;
let html = format!(
"<!DOCTYPE html><html><body><pre>{}</pre></body></html>",
html_escape(&json)
);
response.body = html.into_bytes();
response.headers.insert(
"Content-Type".to_string(),
"text/html; charset=utf-8".to_string(),
);
} else {
response.body =
serde_json::to_vec(data).map_err(|e| Error::Serialization(e.to_string()))?;
response
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
}
response
.headers
.insert("Vary".to_string(), "Accept".to_string());
Ok(response)
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_media_type_parse() {
let mt = MediaType::parse("application/json").unwrap();
assert_eq!(mt.type_, "application");
assert_eq!(mt.subtype, "json");
}
#[test]
fn test_media_type_with_params() {
let mt = MediaType::parse("text/html; charset=utf-8").unwrap();
assert_eq!(mt.type_, "text");
assert_eq!(mt.subtype, "html");
assert_eq!(mt.params.get("charset"), Some(&"utf-8".to_string()));
}
#[test]
fn test_media_type_matches() {
let json = MediaType::json();
let any = MediaType::any();
let html = MediaType::html();
assert!(any.matches(&json));
assert!(json.matches(&any));
assert!(!json.matches(&html));
}
#[test]
fn test_accept_parse() {
let accept = Accept::parse("application/json, text/html;q=0.9, */*;q=0.1");
assert_eq!(accept.media_types.len(), 3);
assert_eq!(accept.media_types[0].0.subtype, "json");
assert_eq!(accept.media_types[0].1, 1.0);
assert_eq!(accept.media_types[1].0.subtype, "html");
assert_eq!(accept.media_types[1].1, 0.9);
}
#[test]
fn test_accept_quality_for() {
let accept = Accept::parse("application/json, text/html;q=0.9");
assert_eq!(accept.quality_for(&MediaType::json()), 1.0);
assert_eq!(accept.quality_for(&MediaType::html()), 0.9);
assert_eq!(accept.quality_for(&MediaType::xml()), 0.0);
}
#[test]
fn test_extract_quality_case_insensitive() {
let accept = Accept::parse("text/html;Q=0.8");
assert_eq!(accept.quality_for(&MediaType::html()), 0.8);
}
#[test]
fn test_extract_quality_non_ascii_no_panic() {
let accept = Accept::parse("application/json\u{212A}\u{212A};q=0.5");
assert_eq!(accept.media_types.len(), 1);
assert_eq!(accept.media_types[0].1, 0.5);
let accept_lang = AcceptLanguage::parse("en\u{212A}\u{212A};q=0.5");
assert_eq!(accept_lang.languages[0].1, 0.5);
let accept_charset = AcceptCharset::parse("utf\u{212A}\u{212A};q=0.5");
assert_eq!(accept_charset.charsets[0].1, 0.5);
let _ = AcceptEncoding::parse("gzip\u{212A}\u{212A};q=0.5");
}
#[test]
fn test_accept_prefers_json() {
let accept = Accept::parse("application/json, text/html;q=0.9");
assert!(accept.prefers_json());
assert!(!accept.prefers_html());
}
#[test]
fn test_accept_prefers_html() {
let accept = Accept::parse("text/html, application/json;q=0.9");
assert!(accept.prefers_html());
assert!(!accept.prefers_json());
}
#[test]
fn test_negotiate_media_type() {
let accept = Accept::parse("application/json, text/html;q=0.9");
let available = vec![MediaType::html(), MediaType::json()];
let best = negotiate_media_type(&accept, &available);
assert_eq!(best, Some(&MediaType::json()));
}
#[test]
fn test_language_tag_parse() {
let tag = LanguageTag::parse("en-US").unwrap();
assert_eq!(tag.primary, "en");
assert_eq!(tag.subtag, Some("US".to_string()));
}
#[test]
fn test_language_tag_matches() {
let en = LanguageTag::new("en");
let en_us = LanguageTag::with_subtag("en", "US");
let fr = LanguageTag::new("fr");
assert!(en.matches(&en_us)); assert!(!en_us.matches(&en)); assert!(!en.matches(&fr));
}
#[test]
fn test_accept_language_parse() {
let accept = AcceptLanguage::parse("en-US, en;q=0.9, fr;q=0.8");
assert_eq!(accept.languages.len(), 3);
assert_eq!(accept.languages[0].0.primary, "en");
}
#[test]
fn test_encoding_parse() {
assert_eq!(Encoding::parse("gzip"), Some(Encoding::Gzip));
assert_eq!(Encoding::parse("br"), Some(Encoding::Brotli));
assert_eq!(Encoding::parse("deflate"), Some(Encoding::Deflate));
}
#[test]
fn test_accept_encoding_parse() {
let accept = AcceptEncoding::parse("gzip, deflate, br;q=0.9");
assert_eq!(accept.encodings.len(), 3);
}
#[test]
fn test_accept_charset_parse() {
let accept = AcceptCharset::parse("utf-8, iso-8859-1;q=0.8");
assert_eq!(accept.charsets.len(), 2);
assert_eq!(accept.quality_for("utf-8"), 1.0);
}
#[test]
fn test_http_request_accept() {
let mut request = HttpRequest::new("GET".to_string(), "/".to_string());
request
.headers
.insert("Accept".to_string(), "application/json".to_string());
let accept = request.accept();
assert!(accept.accepts(&MediaType::json()));
}
#[test]
fn test_http_request_prefers_json() {
let mut request = HttpRequest::new("GET".to_string(), "/".to_string());
request.headers.insert(
"Accept".to_string(),
"application/json, text/html;q=0.9".to_string(),
);
assert!(request.prefers_json());
assert!(!request.prefers_html());
}
}