mirror of
https://github.com/actix/actix-web.git
synced 2024-11-25 19:11:10 +00:00
Quality
/ QualityItem
improvements (#2486)
This commit is contained in:
parent
d89c706cd6
commit
e1a2d9c606
16 changed files with 494 additions and 257 deletions
|
@ -5,8 +5,10 @@ lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dcli
|
||||||
# lib checking
|
# lib checking
|
||||||
ci-check-min = "hack --workspace check --no-default-features"
|
ci-check-min = "hack --workspace check --no-default-features"
|
||||||
ci-check-default = "hack --workspace check"
|
ci-check-default = "hack --workspace check"
|
||||||
|
ci-check-default-tests = "check --workspace --tests"
|
||||||
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
|
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
|
||||||
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
|
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
|
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
|
||||||
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
* `impl Clone for ws::HandshakeError`. [#2468]
|
* `impl Clone for ws::HandshakeError`. [#2468]
|
||||||
* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
||||||
* `impl Default ` for `ws::Codec`. [#1920]
|
* `impl Default ` for `ws::Codec`. [#1920]
|
||||||
|
* `header::QualityItem::{max, min}`. [#2486]
|
||||||
|
* `header::Quality::{MAX, MIN}`. [#2486]
|
||||||
|
* `impl Display` for `header::Quality`. [#2486]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Rename `body::BoxBody::{from_body => new}`. [#2468]
|
* Rename `body::BoxBody::{from_body => new}`. [#2468]
|
||||||
|
@ -27,10 +30,13 @@
|
||||||
* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
|
* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
|
||||||
* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
|
* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
|
||||||
* `impl Copy` for `ws::Codec`. [#1920]
|
* `impl Copy` for `ws::Codec`. [#1920]
|
||||||
|
* `header::qitem` helper. Replaced with `header::QualityItem::max` [#2486]
|
||||||
|
* `impl TryFrom<u16>` for `header::Quality` [#2486]
|
||||||
|
|
||||||
[#2483]: https://github.com/actix/actix-web/pull/2483
|
[#2483]: https://github.com/actix/actix-web/pull/2483
|
||||||
[#2468]: https://github.com/actix/actix-web/pull/2468
|
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||||
[#1920]: https://github.com/actix/actix-web/pull/1920
|
[#1920]: https://github.com/actix/actix-web/pull/1920
|
||||||
|
[#2486]: https://github.com/actix/actix-web/pull/2486
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.14 - 2021-11-30
|
## 3.0.0-beta.14 - 2021-11-30
|
||||||
|
|
|
@ -112,3 +112,7 @@ harness = false
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "uninit-headers"
|
name = "uninit-headers"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "quality-value"
|
||||||
|
harness = false
|
||||||
|
|
90
actix-http/benches/quality-value.rs
Normal file
90
actix-http/benches/quality-value.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||||
|
|
||||||
|
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||||
|
|
||||||
|
fn bench_quality_display_impls(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("quality value display impls");
|
||||||
|
|
||||||
|
for i in CODES.iter() {
|
||||||
|
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
|
||||||
|
b.iter(|| _new::Quality(i).to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||||
|
b.iter(|| _naive::Quality(i).to_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_quality_display_impls);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
mod _new {
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct Quality(pub(crate) u16);
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
1000 => f.write_str("1"),
|
||||||
|
|
||||||
|
// some number in the range 1–999
|
||||||
|
x => {
|
||||||
|
f.write_str("0.")?;
|
||||||
|
|
||||||
|
// this implementation avoids string allocation otherwise required
|
||||||
|
// for `.trim_end_matches('0')`
|
||||||
|
|
||||||
|
if x < 10 {
|
||||||
|
f.write_str("00")?;
|
||||||
|
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
} else if x < 100 {
|
||||||
|
f.write_str("0")?;
|
||||||
|
if x % 10 == 0 {
|
||||||
|
// trailing 0, divide by 10 and write
|
||||||
|
itoa::fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// x is in range 101–999
|
||||||
|
|
||||||
|
if x % 100 == 0 {
|
||||||
|
// two trailing 0s, divide by 100 and write
|
||||||
|
itoa::fmt(f, x / 100)
|
||||||
|
} else if x % 10 == 0 {
|
||||||
|
// one trailing 0, divide by 10 and write
|
||||||
|
itoa::fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod _naive {
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct Quality(pub(crate) u16);
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
1000 => f.write_str("1"),
|
||||||
|
|
||||||
|
x => {
|
||||||
|
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,13 +38,14 @@ pub mod map;
|
||||||
mod shared;
|
mod shared;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use self::shared::*;
|
|
||||||
|
|
||||||
pub use self::as_name::AsHeaderName;
|
pub use self::as_name::AsHeaderName;
|
||||||
pub use self::into_pair::IntoHeaderPair;
|
pub use self::into_pair::IntoHeaderPair;
|
||||||
pub use self::into_value::IntoHeaderValue;
|
pub use self::into_value::IntoHeaderValue;
|
||||||
pub use self::map::HeaderMap;
|
pub use self::map::HeaderMap;
|
||||||
|
pub use self::shared::{
|
||||||
|
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
|
||||||
|
LanguageTag, Quality, QualityItem,
|
||||||
|
};
|
||||||
pub use self::utils::{
|
pub use self::utils::{
|
||||||
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
|
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Originally from hyper v0.11.27 src/header/parsing.rs
|
//! Originally taken from `hyper::header::parsing`.
|
||||||
|
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,13 @@ mod charset;
|
||||||
mod content_encoding;
|
mod content_encoding;
|
||||||
mod extended;
|
mod extended;
|
||||||
mod http_date;
|
mod http_date;
|
||||||
|
mod quality;
|
||||||
mod quality_item;
|
mod quality_item;
|
||||||
|
|
||||||
pub use self::charset::Charset;
|
pub use self::charset::Charset;
|
||||||
pub use self::content_encoding::ContentEncoding;
|
pub use self::content_encoding::ContentEncoding;
|
||||||
pub use self::extended::{parse_extended_value, ExtendedValue};
|
pub use self::extended::{parse_extended_value, ExtendedValue};
|
||||||
pub use self::http_date::HttpDate;
|
pub use self::http_date::HttpDate;
|
||||||
pub use self::quality_item::{q, qitem, Quality, QualityItem};
|
pub use self::quality::{q, Quality};
|
||||||
|
pub use self::quality_item::QualityItem;
|
||||||
pub use language_tags::LanguageTag;
|
pub use language_tags::LanguageTag;
|
||||||
|
|
208
actix-http/src/header/shared/quality.rs
Normal file
208
actix-http/src/header/shared/quality.rs
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
fmt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use derive_more::{Display, Error};
|
||||||
|
|
||||||
|
const MAX_QUALITY_INT: u16 = 1000;
|
||||||
|
const MAX_QUALITY_FLOAT: f32 = 1.0;
|
||||||
|
|
||||||
|
/// Represents a quality used in q-factor values.
|
||||||
|
///
|
||||||
|
/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
|
||||||
|
///
|
||||||
|
/// # Implementation notes
|
||||||
|
/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
|
||||||
|
/// This means there are 1001 possible values. Since floating point numbers are not exact and the
|
||||||
|
/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
|
||||||
|
/// the quality internally.
|
||||||
|
///
|
||||||
|
/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_http::header::{Quality, q};
|
||||||
|
/// assert_eq!(q(1.0), Quality::MAX);
|
||||||
|
///
|
||||||
|
/// assert_eq!(q(0.42).to_string(), "0.42");
|
||||||
|
/// assert_eq!(q(1.0).to_string(), "1");
|
||||||
|
/// assert_eq!(Quality::MIN.to_string(), "0");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Quality(pub(super) u16);
|
||||||
|
|
||||||
|
impl Quality {
|
||||||
|
/// The maximum quality value, equivalent to `q=1.0`.
|
||||||
|
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
|
||||||
|
|
||||||
|
/// The minimum quality value, equivalent to `q=0.0`.
|
||||||
|
pub const MIN: Quality = Quality(0);
|
||||||
|
|
||||||
|
/// Converts a float in the range 0.0–1.0 to a `Quality`.
|
||||||
|
///
|
||||||
|
/// Intentionally private. External uses should rely on the `TryFrom` impl.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
||||||
|
fn from_f32(value: f32) -> Self {
|
||||||
|
// Check that `value` is within range should be done before calling this method.
|
||||||
|
// Just in case, this debug_assert should catch if we were forgetful.
|
||||||
|
debug_assert!(
|
||||||
|
(0.0f32..=1.0f32).contains(&value),
|
||||||
|
"q value must be between 0.0 and 1.0"
|
||||||
|
);
|
||||||
|
|
||||||
|
Quality((value * MAX_QUALITY_INT as f32) as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default value is [`Quality::MAX`].
|
||||||
|
impl Default for Quality {
|
||||||
|
fn default() -> Quality {
|
||||||
|
Quality::MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
MAX_QUALITY_INT => f.write_str("1"),
|
||||||
|
|
||||||
|
// some number in the range 1–999
|
||||||
|
x => {
|
||||||
|
f.write_str("0.")?;
|
||||||
|
|
||||||
|
// This implementation avoids string allocation for removing trailing zeroes.
|
||||||
|
// In benchmarks it is twice as fast as approach using something like
|
||||||
|
// `format!("{}").trim_end_matches('0')` for non-fast-path quality values.
|
||||||
|
|
||||||
|
if x < 10 {
|
||||||
|
// x in is range 1–9
|
||||||
|
|
||||||
|
f.write_str("00")?;
|
||||||
|
|
||||||
|
// 0 is already handled so it's not possible to have a trailing 0 in this range
|
||||||
|
// we can just write the integer
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
} else if x < 100 {
|
||||||
|
// x in is range 10–99
|
||||||
|
|
||||||
|
f.write_str("0")?;
|
||||||
|
|
||||||
|
if x % 10 == 0 {
|
||||||
|
// trailing 0, divide by 10 and write
|
||||||
|
itoa::fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// x is in range 100–999
|
||||||
|
|
||||||
|
if x % 100 == 0 {
|
||||||
|
// two trailing 0s, divide by 100 and write
|
||||||
|
itoa::fmt(f, x / 100)
|
||||||
|
} else if x % 10 == 0 {
|
||||||
|
// one trailing 0, divide by 10 and write
|
||||||
|
itoa::fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Display, Error)]
|
||||||
|
#[display(fmt = "quality out of bounds")]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct QualityOutOfBounds;
|
||||||
|
|
||||||
|
impl TryFrom<f32> for Quality {
|
||||||
|
type Error = QualityOutOfBounds;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
||||||
|
if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
|
||||||
|
Ok(Quality::from_f32(value))
|
||||||
|
} else {
|
||||||
|
Err(QualityOutOfBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function to create a [`Quality`] from an `f32` (0.0–1.0).
|
||||||
|
///
|
||||||
|
/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if value is out of range.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::header::{q, Quality};
|
||||||
|
/// let q1 = q(1.0);
|
||||||
|
/// assert_eq!(q1, Quality::MAX);
|
||||||
|
///
|
||||||
|
/// let q2 = q(0.0);
|
||||||
|
/// assert_eq!(q2, Quality::MIN);
|
||||||
|
///
|
||||||
|
/// let q3 = q(0.42);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// An out-of-range `f32` quality will panic.
|
||||||
|
/// ```should_panic
|
||||||
|
/// # use actix_http::header::q;
|
||||||
|
/// let _q2 = q(1.42);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn q<T>(quality: T) -> Quality
|
||||||
|
where
|
||||||
|
T: TryInto<Quality>,
|
||||||
|
T::Error: fmt::Debug,
|
||||||
|
{
|
||||||
|
quality.try_into().expect("quality value was out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn q_helper() {
|
||||||
|
assert_eq!(q(0.5), Quality(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_output() {
|
||||||
|
assert_eq!(q(0.0).to_string(), "0");
|
||||||
|
assert_eq!(q(1.0).to_string(), "1");
|
||||||
|
assert_eq!(q(0.001).to_string(), "0.001");
|
||||||
|
assert_eq!(q(0.5).to_string(), "0.5");
|
||||||
|
assert_eq!(q(0.22).to_string(), "0.22");
|
||||||
|
assert_eq!(q(0.123).to_string(), "0.123");
|
||||||
|
assert_eq!(q(0.999).to_string(), "0.999");
|
||||||
|
|
||||||
|
for x in 0..=1000 {
|
||||||
|
// if trailing zeroes are handled correctly, we would not expect the serialized length
|
||||||
|
// to ever exceed "0." + 3 decimal places = 5 in length
|
||||||
|
assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn negative_quality() {
|
||||||
|
q(-1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn quality_out_of_bounds() {
|
||||||
|
q(2.0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,85 +1,36 @@
|
||||||
use std::{
|
use std::{cmp, convert::TryFrom as _, fmt, str};
|
||||||
cmp,
|
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
fmt, str,
|
|
||||||
};
|
|
||||||
|
|
||||||
use derive_more::{Display, Error};
|
|
||||||
|
|
||||||
use crate::error::ParseError;
|
use crate::error::ParseError;
|
||||||
|
|
||||||
const MAX_QUALITY: u16 = 1000;
|
use super::Quality;
|
||||||
const MAX_FLOAT_QUALITY: f32 = 1.0;
|
|
||||||
|
|
||||||
/// Represents a quality used in quality values.
|
|
||||||
///
|
|
||||||
/// Can be created with the [`q`] function.
|
|
||||||
///
|
|
||||||
/// # Implementation notes
|
|
||||||
///
|
|
||||||
/// The quality value is defined as a number between 0 and 1 with three decimal
|
|
||||||
/// places. This means there are 1001 possible values. Since floating point
|
|
||||||
/// numbers are not exact and the smallest floating point data type (`f32`)
|
|
||||||
/// consumes four bytes, hyper uses an `u16` value to store the
|
|
||||||
/// quality internally. For performance reasons you may set quality directly to
|
|
||||||
/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality
|
|
||||||
/// `q=0.532`.
|
|
||||||
///
|
|
||||||
/// [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1) gives more
|
|
||||||
/// information on quality values in HTTP header fields.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct Quality(u16);
|
|
||||||
|
|
||||||
impl Quality {
|
|
||||||
/// # Panics
|
|
||||||
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
|
||||||
fn from_f32(value: f32) -> Self {
|
|
||||||
// Check that `value` is within range should be done before calling this method.
|
|
||||||
// Just in case, this debug_assert should catch if we were forgetful.
|
|
||||||
debug_assert!(
|
|
||||||
(0.0f32..=1.0f32).contains(&value),
|
|
||||||
"q value must be between 0.0 and 1.0"
|
|
||||||
);
|
|
||||||
|
|
||||||
Quality((value * MAX_QUALITY as f32) as u16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Quality {
|
|
||||||
fn default() -> Quality {
|
|
||||||
Quality(MAX_QUALITY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Display, Error)]
|
|
||||||
pub struct QualityOutOfBounds;
|
|
||||||
|
|
||||||
impl TryFrom<u16> for Quality {
|
|
||||||
type Error = QualityOutOfBounds;
|
|
||||||
|
|
||||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
|
||||||
if (0..=MAX_QUALITY).contains(&value) {
|
|
||||||
Ok(Quality(value))
|
|
||||||
} else {
|
|
||||||
Err(QualityOutOfBounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<f32> for Quality {
|
|
||||||
type Error = QualityOutOfBounds;
|
|
||||||
|
|
||||||
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
|
||||||
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
|
|
||||||
Ok(Quality::from_f32(value))
|
|
||||||
} else {
|
|
||||||
Err(QualityOutOfBounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents an item with a quality value as defined
|
/// Represents an item with a quality value as defined
|
||||||
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).
|
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).
|
||||||
|
///
|
||||||
|
/// # Parsing and Formatting
|
||||||
|
/// This wrapper be used to parse header value items that have a q-factor annotation as well as
|
||||||
|
/// serialize items with a their q-factor.
|
||||||
|
///
|
||||||
|
/// # Ordering
|
||||||
|
/// Since this context of use for this type is header value items, ordering is defined for
|
||||||
|
/// `QualityItem`s but _only_ considers the item's quality. Order of appearance should be used as
|
||||||
|
/// the secondary sorting parameter; i.e., a stable sort over the quality values will produce a
|
||||||
|
/// correctly sorted sequence.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::header::{QualityItem, q};
|
||||||
|
/// let q_item: QualityItem<String> = "hello;q=0.3".parse().unwrap();
|
||||||
|
/// assert_eq!(&q_item.item, "hello");
|
||||||
|
/// assert_eq!(q_item.quality, q(0.3));
|
||||||
|
///
|
||||||
|
/// // note that format is normalized compared to parsed item
|
||||||
|
/// assert_eq!(q_item.to_string(), "hello; q=0.3");
|
||||||
|
///
|
||||||
|
/// // item with q=0.3 is greater than item with q=0.1
|
||||||
|
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
|
||||||
|
/// assert!(q_item > q_item_fallback);
|
||||||
|
/// ```
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct QualityItem<T> {
|
pub struct QualityItem<T> {
|
||||||
/// The wrapped contents of the field.
|
/// The wrapped contents of the field.
|
||||||
|
@ -93,12 +44,22 @@ impl<T> QualityItem<T> {
|
||||||
/// Constructs a new `QualityItem` from an item and a quality value.
|
/// Constructs a new `QualityItem` from an item and a quality value.
|
||||||
///
|
///
|
||||||
/// The item can be of any type. The quality should be a value in the range [0, 1].
|
/// The item can be of any type. The quality should be a value in the range [0, 1].
|
||||||
pub fn new(item: T, quality: Quality) -> QualityItem<T> {
|
pub fn new(item: T, quality: Quality) -> Self {
|
||||||
QualityItem { item, quality }
|
QualityItem { item, quality }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `QualityItem` from an item, using the maximum q-value.
|
||||||
|
pub fn max(item: T) -> Self {
|
||||||
|
Self::new(item, Quality::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `QualityItem` from an item, using the minimum q-value.
|
||||||
|
pub fn min(item: T) -> Self {
|
||||||
|
Self::new(item, Quality::MIN)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
|
impl<T: PartialEq> PartialOrd for QualityItem<T> {
|
||||||
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
|
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
|
||||||
self.quality.partial_cmp(&other.quality)
|
self.quality.partial_cmp(&other.quality)
|
||||||
}
|
}
|
||||||
|
@ -108,10 +69,12 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fmt::Display::fmt(&self.item, f)?;
|
fmt::Display::fmt(&self.item, f)?;
|
||||||
|
|
||||||
match self.quality.0 {
|
match self.quality {
|
||||||
MAX_QUALITY => Ok(()),
|
// q-factor value is implied for max value
|
||||||
0 => f.write_str("; q=0"),
|
Quality::MAX => Ok(()),
|
||||||
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
|
|
||||||
|
Quality::MIN => f.write_str("; q=0"),
|
||||||
|
q => write!(f, "; q={}", q),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,78 +82,58 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
||||||
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
||||||
type Err = ParseError;
|
type Err = ParseError;
|
||||||
|
|
||||||
fn from_str(qitem_str: &str) -> Result<Self, Self::Err> {
|
fn from_str(q_item_str: &str) -> Result<Self, Self::Err> {
|
||||||
if !qitem_str.is_ascii() {
|
if !q_item_str.is_ascii() {
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set defaults used if parsing fails.
|
// set defaults used if quality-item parsing fails, i.e., item has no q attribute
|
||||||
let mut raw_item = qitem_str;
|
let mut raw_item = q_item_str;
|
||||||
let mut quality = 1f32;
|
let mut quality = Quality::MAX;
|
||||||
|
|
||||||
// TODO: MSRV(1.52): use rsplit_once
|
let parts = q_item_str
|
||||||
let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
|
.rsplit_once(';')
|
||||||
|
.map(|(item, q_attr)| (item.trim(), q_attr.trim()));
|
||||||
|
|
||||||
if parts.len() == 2 {
|
if let Some((val, q_attr)) = parts {
|
||||||
// example for item with q-factor:
|
// example for item with q-factor:
|
||||||
//
|
//
|
||||||
// gzip; q=0.65
|
// gzip;q=0.65
|
||||||
// ^^^^^^ parts[0]
|
// ^^^^ val
|
||||||
// ^^ start
|
// ^^^^^^ q_attr
|
||||||
|
// ^^ q
|
||||||
// ^^^^ q_val
|
// ^^^^ q_val
|
||||||
// ^^^^ parts[1]
|
|
||||||
|
|
||||||
if parts[0].len() < 2 {
|
if q_attr.len() < 2 {
|
||||||
// Can't possibly be an attribute since an attribute needs at least a name followed
|
// Can't possibly be an attribute since an attribute needs at least a name followed
|
||||||
// by an equals sign. And bare identifiers are forbidden.
|
// by an equals sign. And bare identifiers are forbidden.
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = &parts[0][0..2];
|
let q = &q_attr[0..2];
|
||||||
|
|
||||||
if start == "q=" || start == "Q=" {
|
if q == "q=" || q == "Q=" {
|
||||||
let q_val = &parts[0][2..];
|
let q_val = &q_attr[2..];
|
||||||
if q_val.len() > 5 {
|
if q_val.len() > 5 {
|
||||||
// longer than 5 indicates an over-precise q-factor
|
// longer than 5 indicates an over-precise q-factor
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
|
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
|
||||||
|
let q_value =
|
||||||
|
Quality::try_from(q_value).map_err(|_| ParseError::Header)?;
|
||||||
|
|
||||||
if (0f32..=1f32).contains(&q_value) {
|
|
||||||
quality = q_value;
|
quality = q_value;
|
||||||
raw_item = parts[1];
|
raw_item = val;
|
||||||
} else {
|
|
||||||
return Err(ParseError::Header);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
|
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
|
||||||
|
|
||||||
// we already checked above that the quality is within range
|
Ok(QualityItem::new(item, quality))
|
||||||
Ok(QualityItem::new(item, Quality::from_f32(quality)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to wrap a value in a `QualityItem`
|
|
||||||
/// Sets `q` to the default 1.0
|
|
||||||
pub fn qitem<T>(item: T) -> QualityItem<T> {
|
|
||||||
QualityItem::new(item, Quality::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience function to create a `Quality` from a float or integer.
|
|
||||||
///
|
|
||||||
/// Implemented for `u16` and `f32`. Panics if value is out of range.
|
|
||||||
pub fn q<T>(val: T) -> Quality
|
|
||||||
where
|
|
||||||
T: TryInto<Quality>,
|
|
||||||
T::Error: fmt::Debug,
|
|
||||||
{
|
|
||||||
// TODO: on next breaking change, handle unwrap differently
|
|
||||||
val.try_into().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -245,7 +188,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_quality_item_fmt_q_1() {
|
fn test_quality_item_fmt_q_1() {
|
||||||
use Encoding::*;
|
use Encoding::*;
|
||||||
let x = qitem(Chunked);
|
let x = QualityItem::max(Chunked);
|
||||||
assert_eq!(format!("{}", x), "chunked");
|
assert_eq!(format!("{}", x), "chunked");
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -344,25 +287,8 @@ mod tests {
|
||||||
fn test_quality_item_ordering() {
|
fn test_quality_item_ordering() {
|
||||||
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
|
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
|
||||||
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
|
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
|
||||||
let comparision_result: bool = x.gt(&y);
|
let comparison_result: bool = x.gt(&y);
|
||||||
assert!(comparision_result)
|
assert!(comparison_result)
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_quality() {
|
|
||||||
assert_eq!(q(0.5), Quality(500));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_quality_invalid() {
|
|
||||||
q(-1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_quality_invalid2() {
|
|
||||||
q(2.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -65,8 +65,9 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Percent encode a sequence of bytes with a character set defined in
|
/// Percent encode a sequence of bytes with a character set defined in [RFC 5987 §3.2].
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>
|
///
|
||||||
|
/// [RFC 5987 §3.2]: https://datatracker.ietf.org/doc/html/rfc5987#section-3.2
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
||||||
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
||||||
|
|
|
@ -435,10 +435,10 @@ impl Field {
|
||||||
|
|
||||||
/// Returns the field's Content-Disposition.
|
/// Returns the field's Content-Disposition.
|
||||||
///
|
///
|
||||||
/// Per [RFC 7578 §4.2]: 'Each part MUST contain a Content-Disposition header field where the
|
/// Per [RFC 7578 §4.2]: "Each part MUST contain a Content-Disposition header field where the
|
||||||
/// disposition type is "form-data". The Content-Disposition header field MUST also contain an
|
/// disposition type is `form-data`. The Content-Disposition header field MUST also contain an
|
||||||
/// additional parameter of "name"; the value of the "name" parameter is the original field name
|
/// additional parameter of `name`; the value of the `name` parameter is the original field name
|
||||||
/// from the form.'
|
/// from the form."
|
||||||
///
|
///
|
||||||
/// This crate validates that it exists before returning a `Field`. As such, it is safe to
|
/// This crate validates that it exists before returning a `Field`. As such, it is safe to
|
||||||
/// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as
|
/// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as
|
||||||
|
@ -451,7 +451,8 @@ impl Field {
|
||||||
|
|
||||||
/// Returns the field's name.
|
/// Returns the field's name.
|
||||||
///
|
///
|
||||||
/// See [content_disposition] regarding guarantees about
|
/// See [content_disposition](Self::content_disposition) regarding guarantees about existence of
|
||||||
|
/// the name field.
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
self.content_disposition()
|
self.content_disposition()
|
||||||
.get_name()
|
.get_name()
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::cmp::Ordering;
|
||||||
|
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
|
|
||||||
use super::{qitem, QualityItem};
|
use super::QualityItem;
|
||||||
use crate::http::header;
|
use crate::http::header;
|
||||||
|
|
||||||
crate::http::header::common_header! {
|
crate::http::header::common_header! {
|
||||||
|
@ -34,46 +34,40 @@ crate::http::header::common_header! {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{Accept, qitem};
|
/// use actix_web::http::header::{Accept, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// Accept(vec![
|
/// Accept(vec![
|
||||||
/// qitem(mime::TEXT_HTML),
|
/// QualityItem::max(mime::TEXT_HTML),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{Accept, qitem};
|
/// use actix_web::http::header::{Accept, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// Accept(vec![
|
/// Accept(vec![
|
||||||
/// qitem(mime::APPLICATION_JSON),
|
/// QualityItem::max(mime::APPLICATION_JSON),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{Accept, QualityItem, q, qitem};
|
/// use actix_web::http::header::{Accept, QualityItem, q};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// Accept(vec![
|
/// Accept(vec![
|
||||||
/// qitem(mime::TEXT_HTML),
|
/// QualityItem::max(mime::TEXT_HTML),
|
||||||
/// qitem("application/xhtml+xml".parse().unwrap()),
|
/// QualityItem::max("application/xhtml+xml".parse().unwrap()),
|
||||||
/// QualityItem::new(
|
/// QualityItem::new(mime::TEXT_XML, q(0.9)),
|
||||||
/// mime::TEXT_XML,
|
/// QualityItem::max("image/webp".parse().unwrap()),
|
||||||
/// q(900)
|
/// QualityItem::new(mime::STAR_STAR, q(0.8)),
|
||||||
/// ),
|
|
||||||
/// qitem("image/webp".parse().unwrap()),
|
|
||||||
/// QualityItem::new(
|
|
||||||
/// mime::STAR_STAR,
|
|
||||||
/// q(800)
|
|
||||||
/// ),
|
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -85,20 +79,20 @@ crate::http::header::common_header! {
|
||||||
test1,
|
test1,
|
||||||
vec![b"audio/*; q=0.2, audio/basic"],
|
vec![b"audio/*; q=0.2, audio/basic"],
|
||||||
Some(Accept(vec![
|
Some(Accept(vec![
|
||||||
QualityItem::new("audio/*".parse().unwrap(), q(200)),
|
QualityItem::new("audio/*".parse().unwrap(), q(0.2)),
|
||||||
qitem("audio/basic".parse().unwrap()),
|
QualityItem::max("audio/basic".parse().unwrap()),
|
||||||
])));
|
])));
|
||||||
|
|
||||||
crate::http::header::common_header_test!(
|
crate::http::header::common_header_test!(
|
||||||
test2,
|
test2,
|
||||||
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
|
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
|
||||||
Some(Accept(vec![
|
Some(Accept(vec![
|
||||||
QualityItem::new(mime::TEXT_PLAIN, q(500)),
|
QualityItem::new(mime::TEXT_PLAIN, q(0.5)),
|
||||||
qitem(mime::TEXT_HTML),
|
QualityItem::max(mime::TEXT_HTML),
|
||||||
QualityItem::new(
|
QualityItem::new(
|
||||||
"text/x-dvi".parse().unwrap(),
|
"text/x-dvi".parse().unwrap(),
|
||||||
q(800)),
|
q(0.8)),
|
||||||
qitem("text/x-c".parse().unwrap()),
|
QualityItem::max("text/x-c".parse().unwrap()),
|
||||||
])));
|
])));
|
||||||
|
|
||||||
// Custom tests
|
// Custom tests
|
||||||
|
@ -106,14 +100,14 @@ crate::http::header::common_header! {
|
||||||
test3,
|
test3,
|
||||||
vec![b"text/plain; charset=utf-8"],
|
vec![b"text/plain; charset=utf-8"],
|
||||||
Some(Accept(vec![
|
Some(Accept(vec![
|
||||||
qitem(mime::TEXT_PLAIN_UTF_8),
|
QualityItem::max(mime::TEXT_PLAIN_UTF_8),
|
||||||
])));
|
])));
|
||||||
crate::http::header::common_header_test!(
|
crate::http::header::common_header_test!(
|
||||||
test4,
|
test4,
|
||||||
vec![b"text/plain; charset=utf-8; q=0.5"],
|
vec![b"text/plain; charset=utf-8; q=0.5"],
|
||||||
Some(Accept(vec![
|
Some(Accept(vec![
|
||||||
QualityItem::new(mime::TEXT_PLAIN_UTF_8,
|
QualityItem::new(mime::TEXT_PLAIN_UTF_8,
|
||||||
q(500)),
|
q(0.5)),
|
||||||
])));
|
])));
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -130,27 +124,27 @@ crate::http::header::common_header! {
|
||||||
impl Accept {
|
impl Accept {
|
||||||
/// Construct `Accept: */*`.
|
/// Construct `Accept: */*`.
|
||||||
pub fn star() -> Accept {
|
pub fn star() -> Accept {
|
||||||
Accept(vec![qitem(mime::STAR_STAR)])
|
Accept(vec![QualityItem::max(mime::STAR_STAR)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct `Accept: application/json`.
|
/// Construct `Accept: application/json`.
|
||||||
pub fn json() -> Accept {
|
pub fn json() -> Accept {
|
||||||
Accept(vec![qitem(mime::APPLICATION_JSON)])
|
Accept(vec![QualityItem::max(mime::APPLICATION_JSON)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct `Accept: text/*`.
|
/// Construct `Accept: text/*`.
|
||||||
pub fn text() -> Accept {
|
pub fn text() -> Accept {
|
||||||
Accept(vec![qitem(mime::TEXT_STAR)])
|
Accept(vec![QualityItem::max(mime::TEXT_STAR)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct `Accept: image/*`.
|
/// Construct `Accept: image/*`.
|
||||||
pub fn image() -> Accept {
|
pub fn image() -> Accept {
|
||||||
Accept(vec![qitem(mime::IMAGE_STAR)])
|
Accept(vec![QualityItem::max(mime::IMAGE_STAR)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct `Accept: text/html`.
|
/// Construct `Accept: text/html`.
|
||||||
pub fn html() -> Accept {
|
pub fn html() -> Accept {
|
||||||
Accept(vec![qitem(mime::TEXT_HTML)])
|
Accept(vec![QualityItem::max(mime::TEXT_HTML)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
|
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
|
||||||
|
@ -213,10 +207,10 @@ impl Accept {
|
||||||
///
|
///
|
||||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||||
pub fn preference(&self) -> Mime {
|
pub fn preference(&self) -> Mime {
|
||||||
use actix_http::header::q;
|
use actix_http::header::Quality;
|
||||||
|
|
||||||
let mut max_item = None;
|
let mut max_item = None;
|
||||||
let mut max_pref = q(0);
|
let mut max_pref = Quality::MIN;
|
||||||
|
|
||||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||||
|
@ -244,11 +238,11 @@ mod tests {
|
||||||
let test = Accept(vec![]);
|
let test = Accept(vec![]);
|
||||||
assert!(test.ranked().is_empty());
|
assert!(test.ranked().is_empty());
|
||||||
|
|
||||||
let test = Accept(vec![qitem(mime::APPLICATION_JSON)]);
|
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
|
||||||
assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON));
|
assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON));
|
||||||
|
|
||||||
let test = Accept(vec![
|
let test = Accept(vec![
|
||||||
qitem(mime::TEXT_HTML),
|
QualityItem::max(mime::TEXT_HTML),
|
||||||
"application/xhtml+xml".parse().unwrap(),
|
"application/xhtml+xml".parse().unwrap(),
|
||||||
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
||||||
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
||||||
|
@ -264,9 +258,9 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let test = Accept(vec![
|
let test = Accept(vec![
|
||||||
qitem(mime::STAR_STAR),
|
QualityItem::max(mime::STAR_STAR),
|
||||||
qitem(mime::IMAGE_STAR),
|
QualityItem::max(mime::IMAGE_STAR),
|
||||||
qitem(mime::IMAGE_PNG),
|
QualityItem::max(mime::IMAGE_PNG),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test.ranked(),
|
test.ranked(),
|
||||||
|
@ -277,7 +271,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn preference_selection() {
|
fn preference_selection() {
|
||||||
let test = Accept(vec![
|
let test = Accept(vec![
|
||||||
qitem(mime::TEXT_HTML),
|
QualityItem::max(mime::TEXT_HTML),
|
||||||
"application/xhtml+xml".parse().unwrap(),
|
"application/xhtml+xml".parse().unwrap(),
|
||||||
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
||||||
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
||||||
|
@ -286,9 +280,9 @@ mod tests {
|
||||||
|
|
||||||
let test = Accept(vec![
|
let test = Accept(vec![
|
||||||
QualityItem::new("video/*".parse().unwrap(), q(0.8)),
|
QualityItem::new("video/*".parse().unwrap(), q(0.8)),
|
||||||
qitem(mime::IMAGE_PNG),
|
QualityItem::max(mime::IMAGE_PNG),
|
||||||
QualityItem::new(mime::STAR_STAR, q(0.5)),
|
QualityItem::new(mime::STAR_STAR, q(0.5)),
|
||||||
qitem(mime::IMAGE_SVG),
|
QualityItem::max(mime::IMAGE_SVG),
|
||||||
QualityItem::new(mime::IMAGE_STAR, q(0.8)),
|
QualityItem::new(mime::IMAGE_STAR, q(0.8)),
|
||||||
]);
|
]);
|
||||||
assert_eq!(test.preference(), mime::IMAGE_PNG);
|
assert_eq!(test.preference(), mime::IMAGE_PNG);
|
||||||
|
|
|
@ -22,11 +22,11 @@ crate::http::header::common_header! {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{AcceptCharset, Charset, qitem};
|
/// use actix_web::http::header::{AcceptCharset, Charset, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)])
|
/// AcceptCharset(vec![QualityItem::max(Charset::Us_Ascii)])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
@ -37,19 +37,19 @@ crate::http::header::common_header! {
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptCharset(vec![
|
/// AcceptCharset(vec![
|
||||||
/// QualityItem::new(Charset::Us_Ascii, q(900)),
|
/// QualityItem::new(Charset::Us_Ascii, q(0.9)),
|
||||||
/// QualityItem::new(Charset::Iso_8859_10, q(200)),
|
/// QualityItem::new(Charset::Iso_8859_10, q(0.2)),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{AcceptCharset, Charset, qitem};
|
/// use actix_web::http::header::{AcceptCharset, Charset, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
|
/// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
|
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
|
||||||
|
|
|
@ -29,36 +29,38 @@ common_header! {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, qitem};
|
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptEncoding(vec![qitem(Encoding::Chunked)])
|
/// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, qitem};
|
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptEncoding(vec![
|
/// AcceptEncoding(vec![
|
||||||
/// qitem(Encoding::Chunked),
|
/// QualityItem::max(Encoding::Chunked),
|
||||||
/// qitem(Encoding::Gzip),
|
/// QualityItem::max(Encoding::Gzip),
|
||||||
/// qitem(Encoding::Deflate),
|
/// QualityItem::max(Encoding::Deflate),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q, qitem};
|
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptEncoding(vec![
|
/// AcceptEncoding(vec![
|
||||||
/// qitem(Encoding::Chunked),
|
/// QualityItem::max(Encoding::Chunked),
|
||||||
/// QualityItem::new(Encoding::Gzip, q(600)),
|
/// QualityItem::new(Encoding::Gzip, q(0.60)),
|
||||||
/// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)),
|
/// QualityItem::min(Encoding::EncodingExt("*".to_owned())),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -77,3 +79,5 @@ common_header! {
|
||||||
common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: shortcut for EncodingExt(*) = Any
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use language_tags::LanguageTag;
|
use language_tags::LanguageTag;
|
||||||
|
|
||||||
use super::{common_header, Preference, QualityItem};
|
use super::{common_header, Preference, Quality, QualityItem};
|
||||||
use crate::http::header;
|
use crate::http::header;
|
||||||
|
|
||||||
common_header! {
|
common_header! {
|
||||||
|
@ -32,26 +32,26 @@ common_header! {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{AcceptLanguage, qitem};
|
/// use actix_web::http::header::{AcceptLanguage, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptLanguage(vec![
|
/// AcceptLanguage(vec![
|
||||||
/// qitem("en-US".parse().unwrap())
|
/// QualityItem::max("en-US".parse().unwrap())
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem};
|
/// use actix_web::http::header::{AcceptLanguage, QualityItem, q};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// AcceptLanguage(vec![
|
/// AcceptLanguage(vec![
|
||||||
/// qitem("da".parse().unwrap()),
|
/// QualityItem::max("da".parse().unwrap()),
|
||||||
/// QualityItem::new("en-GB".parse().unwrap(), q(800)),
|
/// QualityItem::new("en-GB".parse().unwrap(), q(0.8)),
|
||||||
/// QualityItem::new("en".parse().unwrap(), q(700)),
|
/// QualityItem::new("en".parse().unwrap(), q(0.7)),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -72,9 +72,9 @@ common_header! {
|
||||||
not_ordered_by_weight,
|
not_ordered_by_weight,
|
||||||
vec![b"en-US, en; q=0.5, fr"],
|
vec![b"en-US, en; q=0.5, fr"],
|
||||||
Some(AcceptLanguage(vec![
|
Some(AcceptLanguage(vec![
|
||||||
qitem("en-US".parse().unwrap()),
|
QualityItem::max("en-US".parse().unwrap()),
|
||||||
QualityItem::new("en".parse().unwrap(), q(500)),
|
QualityItem::new("en".parse().unwrap(), q(0.5)),
|
||||||
qitem("fr".parse().unwrap()),
|
QualityItem::max("fr".parse().unwrap()),
|
||||||
]))
|
]))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -82,11 +82,11 @@ common_header! {
|
||||||
has_wildcard,
|
has_wildcard,
|
||||||
vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"],
|
vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"],
|
||||||
Some(AcceptLanguage(vec![
|
Some(AcceptLanguage(vec![
|
||||||
qitem("fr-CH".parse().unwrap()),
|
QualityItem::max("fr-CH".parse().unwrap()),
|
||||||
QualityItem::new("fr".parse().unwrap(), q(900)),
|
QualityItem::new("fr".parse().unwrap(), q(0.9)),
|
||||||
QualityItem::new("en".parse().unwrap(), q(800)),
|
QualityItem::new("en".parse().unwrap(), q(0.8)),
|
||||||
QualityItem::new("de".parse().unwrap(), q(700)),
|
QualityItem::new("de".parse().unwrap(), q(0.7)),
|
||||||
QualityItem::new("*".parse().unwrap(), q(500)),
|
QualityItem::new("*".parse().unwrap(), q(0.5)),
|
||||||
]))
|
]))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -122,10 +122,8 @@ impl AcceptLanguage {
|
||||||
///
|
///
|
||||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||||
pub fn preference(&self) -> Preference<LanguageTag> {
|
pub fn preference(&self) -> Preference<LanguageTag> {
|
||||||
use actix_http::header::q;
|
|
||||||
|
|
||||||
let mut max_item = None;
|
let mut max_item = None;
|
||||||
let mut max_pref = q(0);
|
let mut max_pref = Quality::MIN;
|
||||||
|
|
||||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||||
|
@ -153,15 +151,15 @@ mod tests {
|
||||||
let test = AcceptLanguage(vec![]);
|
let test = AcceptLanguage(vec![]);
|
||||||
assert!(test.ranked().is_empty());
|
assert!(test.ranked().is_empty());
|
||||||
|
|
||||||
let test = AcceptLanguage(vec![qitem("fr-CH".parse().unwrap())]);
|
let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
|
||||||
assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap()));
|
assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap()));
|
||||||
|
|
||||||
let test = AcceptLanguage(vec![
|
let test = AcceptLanguage(vec![
|
||||||
QualityItem::new("fr".parse().unwrap(), q(900)),
|
QualityItem::new("fr".parse().unwrap(), q(0.900)),
|
||||||
QualityItem::new("fr-CH".parse().unwrap(), q(1000)),
|
QualityItem::new("fr-CH".parse().unwrap(), q(1.0)),
|
||||||
QualityItem::new("en".parse().unwrap(), q(800)),
|
QualityItem::new("en".parse().unwrap(), q(0.800)),
|
||||||
QualityItem::new("*".parse().unwrap(), q(500)),
|
QualityItem::new("*".parse().unwrap(), q(0.500)),
|
||||||
QualityItem::new("de".parse().unwrap(), q(700)),
|
QualityItem::new("de".parse().unwrap(), q(0.700)),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test.ranked(),
|
test.ranked(),
|
||||||
|
@ -175,11 +173,11 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let test = AcceptLanguage(vec![
|
let test = AcceptLanguage(vec![
|
||||||
qitem("fr".parse().unwrap()),
|
QualityItem::max("fr".parse().unwrap()),
|
||||||
qitem("fr-CH".parse().unwrap()),
|
QualityItem::max("fr-CH".parse().unwrap()),
|
||||||
qitem("en".parse().unwrap()),
|
QualityItem::max("en".parse().unwrap()),
|
||||||
qitem("*".parse().unwrap()),
|
QualityItem::max("*".parse().unwrap()),
|
||||||
qitem("de".parse().unwrap()),
|
QualityItem::max("de".parse().unwrap()),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test.ranked(),
|
test.ranked(),
|
||||||
|
@ -196,11 +194,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn preference_selection() {
|
fn preference_selection() {
|
||||||
let test = AcceptLanguage(vec![
|
let test = AcceptLanguage(vec![
|
||||||
QualityItem::new("fr".parse().unwrap(), q(900)),
|
QualityItem::new("fr".parse().unwrap(), q(0.900)),
|
||||||
QualityItem::new("fr-CH".parse().unwrap(), q(1000)),
|
QualityItem::new("fr-CH".parse().unwrap(), q(1.0)),
|
||||||
QualityItem::new("en".parse().unwrap(), q(800)),
|
QualityItem::new("en".parse().unwrap(), q(0.800)),
|
||||||
QualityItem::new("*".parse().unwrap(), q(500)),
|
QualityItem::new("*".parse().unwrap(), q(0.500)),
|
||||||
QualityItem::new("de".parse().unwrap(), q(700)),
|
QualityItem::new("de".parse().unwrap(), q(0.700)),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test.preference(),
|
test.preference(),
|
||||||
|
@ -208,11 +206,11 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let test = AcceptLanguage(vec![
|
let test = AcceptLanguage(vec![
|
||||||
qitem("fr".parse().unwrap()),
|
QualityItem::max("fr".parse().unwrap()),
|
||||||
qitem("fr-CH".parse().unwrap()),
|
QualityItem::max("fr-CH".parse().unwrap()),
|
||||||
qitem("en".parse().unwrap()),
|
QualityItem::max("en".parse().unwrap()),
|
||||||
qitem("*".parse().unwrap()),
|
QualityItem::max("*".parse().unwrap()),
|
||||||
qitem("de".parse().unwrap()),
|
QualityItem::max("de".parse().unwrap()),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test.preference(),
|
test.preference(),
|
||||||
|
|
|
@ -23,25 +23,25 @@ common_header! {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{ContentLanguage, LanguageTag, qitem};
|
/// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// ContentLanguage(vec![
|
/// ContentLanguage(vec![
|
||||||
/// qitem(LanguageTag::parse("en").unwrap()),
|
/// QualityItem::max(LanguageTag::parse("en").unwrap()),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::HttpResponse;
|
/// use actix_web::HttpResponse;
|
||||||
/// use actix_web::http::header::{ContentLanguage, LanguageTag, qitem};
|
/// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem};
|
||||||
///
|
///
|
||||||
/// let mut builder = HttpResponse::Ok();
|
/// let mut builder = HttpResponse::Ok();
|
||||||
/// builder.insert_header(
|
/// builder.insert_header(
|
||||||
/// ContentLanguage(vec![
|
/// ContentLanguage(vec![
|
||||||
/// qitem(LanguageTag::parse("da").unwrap()),
|
/// QualityItem::max(LanguageTag::parse("da").unwrap()),
|
||||||
/// qitem(LanguageTag::parse("en-GB").unwrap()),
|
/// QualityItem::max(LanguageTag::parse("en-GB").unwrap()),
|
||||||
/// ])
|
/// ])
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|
Loading…
Reference in a new issue