use std::{ops::Deref, str::FromStr};
use mime::Mime;
use super::precedence::{AcceptPrecedence, MediaRangeSpecificity};
use crate::header::common::qvalue::{QValue, Q_PARAM_NAME};
#[derive(Debug, Clone)]
pub struct AcceptValue {
media_range: Mime,
accept_precedence: AcceptPrecedence,
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum InvalidEncodedAcceptValue {
#[error("Invalid header encoding")]
InvalidHeaderEncoding,
#[error("Invalid Weight")]
InvalidWeight,
}
impl FromStr for AcceptValue {
type Err = InvalidEncodedAcceptValue;
fn from_str(value_str: &str) -> Result<Self, Self::Err> {
let media_range: Mime = value_str
.parse()
.map_err(|_| InvalidEncodedAcceptValue::InvalidHeaderEncoding)?;
let weight: QValue =
if let Some(q_value) = media_range.get_param(Q_PARAM_NAME.as_token().as_ref()) {
q_value
.as_str()
.parse()
.map_err(|_| InvalidEncodedAcceptValue::InvalidWeight)?
} else {
QValue::DEFAULT
};
let accept_precedence = AcceptPrecedence {
weight,
media_range_specificity: MediaRangeSpecificity::from(&media_range),
};
Ok(Self {
media_range,
accept_precedence,
})
}
}
impl Deref for AcceptValue {
type Target = Mime;
#[inline]
fn deref(&self) -> &Self::Target {
&self.media_range
}
}
impl AcceptValue {
#[inline]
pub fn precedence(&self) -> &AcceptPrecedence {
&self.accept_precedence
}
#[inline]
pub fn weight(&self) -> &QValue {
&self.accept_precedence.weight
}
#[inline]
pub fn media_range(&self) -> &Mime {
&self.media_range
}
pub fn matches(&self, media_type: &Mime, consider_params: bool) -> bool {
match self.accept_precedence.media_range_specificity {
MediaRangeSpecificity::STAR_STAR => true,
MediaRangeSpecificity::TYPE_STAR => self.media_range.type_() == media_type.type_(),
MediaRangeSpecificity::EXACT { .. } => {
(media_type.essence_str() == self.media_range.essence_str())
&& (!consider_params || {
self.media_range
.params()
.all(|(k, v)| k == "q" || Some(v) == media_type.get_param(k))
})
}
}
}
}
#[cfg(test)]
mod tests_accept_value_parse {
use claims::{assert_err_eq, assert_ok};
use rstest::rstest;
use super::*;
#[rstest]
#[case("text")]
#[case("text / html")]
fn accept_value_with_invalid_media_range_will_be_rejected(#[case] accept_value_str: &str) {
assert_err_eq!(
AcceptValue::from_str(accept_value_str),
InvalidEncodedAcceptValue::InvalidHeaderEncoding
);
}
#[rstest]
#[case::non_number_qvalue("image/png; q=1-2345")]
#[case::out_of_range_qvalue("image/png; q=2.3")]
#[case::out_of_scale_qvalue("*/*; q=0.3333")]
fn accept_value_with_invalid_qvalue_will_be_rejected(#[case] accept_value_str: &str) {
assert_err_eq!(
AcceptValue::from_str(accept_value_str),
InvalidEncodedAcceptValue::InvalidWeight
);
}
fn precedence(
qvalue_str: &str,
media_type_specificity: MediaRangeSpecificity,
) -> AcceptPrecedence {
AcceptPrecedence {
weight: QValue::from_str(qvalue_str).unwrap(),
media_range_specificity: media_type_specificity,
}
}
pub fn assert_valid_accept_value(value: &str) -> AcceptValue {
assert_ok!(AcceptValue::from_str(value))
}
#[rstest]
#[case::star_star_default("*/*", precedence("1", MediaRangeSpecificity::STAR_STAR))]
#[case::star_star_explicit(
"*/*; q=0.321",
precedence("0.321", MediaRangeSpecificity::STAR_STAR)
)]
#[case::type_star_default("image/*", precedence("1", MediaRangeSpecificity::TYPE_STAR))]
#[case::type_star_explicit(
"text/*; q=0.5; charset=utf8",
precedence("0.5", MediaRangeSpecificity::TYPE_STAR)
)]
#[case::exact_default("text/html; charset=utf8", precedence("1", MediaRangeSpecificity::EXACT { param_count: 1 }))]
#[case::exact_explicit("image/png; q=0.7", precedence("0.7", MediaRangeSpecificity::EXACT { param_count: 1 }))]
fn valid_accept_value_will_be_parsed_correctly(
#[case] accept_value_str: &str,
#[case] expected_specificity: AcceptPrecedence,
) {
let accept_value = assert_valid_accept_value(accept_value_str);
assert_eq!(accept_value.precedence(), &expected_specificity);
}
}
#[cfg(test)]
mod tests_accept_value {
use std::{cmp::Ordering, str::FromStr};
use rstest::rstest;
use super::{tests_accept_value_parse::*, *};
#[rstest]
#[case::star_star_1("*/*", "image/png", false, true)]
#[case::type_star_1("image/*", "image/png", false, true)]
#[case::type_star_2("text/*", "image/png", false, false)]
#[case::exact_1("image/png", "image/png", false, true)]
#[case::exact_2("image/jpg", "image/png", false, false)]
#[case::exact_params1("text/html; q=1; charset=utf8", "text/html", false, true)]
#[case::exact_params2("text/html; q=1; charset=utf8", "text/html", true, false)]
#[case::exact_params2("text/html; charset=utf8; q=1", "text/html", true, false)]
#[case::exact_params3("text/html; q=1; charset=utf8", "text/html; charset=utf8", true, true)]
#[case::exact_params4("text/html; q=1; charset=utf8", "text/html; charset=UTF8", true, true)]
#[case::exact_params5("application/json", "application/ld+json", true, false)]
#[case::with_suffix5("application/ld+json", "application/ld+json", true, true)]
#[case("application/ld+json", "application/json", true, false)]
fn matches_works_correctly(
#[case] accept_value_str: &str,
#[case] media_range_str: &str,
#[case] consider_params: bool,
#[case] expected: bool,
) {
let accept_value = assert_valid_accept_value(accept_value_str);
let media_type = Mime::from_str(media_range_str).unwrap();
assert_eq!(accept_value.matches(&media_type, consider_params), expected);
}
#[rstest]
#[case("*/*", "*/*", Ordering::Equal)]
#[case("*/*; q=0.3", "image/*; q=0.3", Ordering::Less)]
#[case("*/*", "image/png", Ordering::Less)]
#[case("image/*", "*/*", Ordering::Greater)]
#[case("image/*; q=1", "text/*", Ordering::Equal)]
#[case("image/*", "text/html", Ordering::Less)]
#[case("image/png", "text/*", Ordering::Greater)]
#[case("text/html", "application/*", Ordering::Greater)]
#[case("image/png", "text/html", Ordering::Equal)]
#[case("text/plain;format=flowed", "text/plain", Ordering::Greater)]
#[case("*/*; q=0.3", "image/*; q=0.2", Ordering::Greater)]
#[case("image/png; q=0.5", "text/*; q=0.8", Ordering::Less)]
fn precedence_ordering_works_correctly(
#[case] accept_value1_str: &str,
#[case] accept_value2_str: &str,
#[case] expected: Ordering,
) {
let accept_value1 = assert_valid_accept_value(accept_value1_str);
let accept_value2 = assert_valid_accept_value(accept_value2_str);
assert_eq!(
accept_value1.precedence().cmp(accept_value2.precedence()),
expected
);
}
}