From 70f4747a2391ce870c0a33383a2ddc38c7bb1e99 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 24 Nov 2020 10:08:57 +0000 Subject: [PATCH] add method for getting accept type preference (#1793) --- actix-http/CHANGES.md | 2 + actix-http/src/header/common/accept.rs | 141 ++++++++++++++++++- actix-http/src/header/shared/quality_item.rs | 4 +- 3 files changed, 138 insertions(+), 9 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index eeed4e14..cafaa5e0 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - 2020-xx-xx ### Added * HttpResponse builders for 1xx status codes. [#1768] +* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793] ### Fixed * Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767] @@ -12,6 +13,7 @@ [#1767]: https://github.com/actix/actix-web/pull/1767 [#1768]: https://github.com/actix/actix-web/pull/1768 +[#1793]: https://github.com/actix/actix-web/pull/1793 ## 2.1.0 - 2020-10-30 diff --git a/actix-http/src/header/common/accept.rs b/actix-http/src/header/common/accept.rs index d52eba24..da26b026 100644 --- a/actix-http/src/header/common/accept.rs +++ b/actix-http/src/header/common/accept.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use mime::Mime; use crate::header::{qitem, QualityItem}; @@ -7,7 +9,7 @@ header! { /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) /// /// The `Accept` header field can be used by user agents to specify - /// response media types that are acceptable. Accept header fields can + /// response media types that are acceptable. Accept header fields can /// be used to indicate that the request is specifically limited to a /// small set of desired types, as in the case of a request for an /// in-line image @@ -97,14 +99,14 @@ header! { test_header!( test1, vec![b"audio/*; q=0.2, audio/basic"], - Some(HeaderField(vec![ + Some(Accept(vec![ QualityItem::new("audio/*".parse().unwrap(), q(200)), qitem("audio/basic".parse().unwrap()), ]))); test_header!( test2, vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], - Some(HeaderField(vec![ + Some(Accept(vec![ QualityItem::new(mime::TEXT_PLAIN, q(500)), qitem(mime::TEXT_HTML), QualityItem::new( @@ -138,23 +140,148 @@ header! { } impl Accept { - /// A constructor to easily create `Accept: */*`. + /// Construct `Accept: */*`. pub fn star() -> Accept { Accept(vec![qitem(mime::STAR_STAR)]) } - /// A constructor to easily create `Accept: application/json`. + /// Construct `Accept: application/json`. pub fn json() -> Accept { Accept(vec![qitem(mime::APPLICATION_JSON)]) } - /// A constructor to easily create `Accept: text/*`. + /// Construct `Accept: text/*`. pub fn text() -> Accept { Accept(vec![qitem(mime::TEXT_STAR)]) } - /// A constructor to easily create `Accept: image/*`. + /// Construct `Accept: image/*`. pub fn image() -> Accept { Accept(vec![qitem(mime::IMAGE_STAR)]) } + + /// Construct `Accept: text/html`. + pub fn html() -> Accept { + Accept(vec![qitem(mime::TEXT_HTML)]) + } + + /// Returns a sorted list of mime types from highest to lowest preference, accounting for + /// [q-factor weighting] and specificity. + /// + /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2 + pub fn mime_precedence(&self) -> Vec { + let mut types = self.0.clone(); + + // use stable sort so items with equal q-factor and specificity retain listed order + types.sort_by(|a, b| { + // sort by q-factor descending + b.quality.cmp(&a.quality).then_with(|| { + // use specificity rules on mime types with + // same q-factor (eg. text/html > text/* > */*) + + // subtypes are not comparable if main type is star, so return + match (a.item.type_(), b.item.type_()) { + (mime::STAR, mime::STAR) => return Ordering::Equal, + + // a is sorted after b + (mime::STAR, _) => return Ordering::Greater, + + // a is sorted before b + (_, mime::STAR) => return Ordering::Less, + + _ => {} + } + + // in both these match expressions, the returned ordering appears + // inverted because sort is high-to-low ("descending") precedence + match (a.item.subtype(), b.item.subtype()) { + (mime::STAR, mime::STAR) => Ordering::Equal, + + // a is sorted after b + (mime::STAR, _) => Ordering::Greater, + + // a is sorted before b + (_, mime::STAR) => Ordering::Less, + + _ => Ordering::Equal, + } + }) + }); + + types.into_iter().map(|qitem| qitem.item).collect() + } + + /// Extracts the most preferable mime type, accounting for [q-factor weighting]. + /// + /// If no q-factors are provided, the first mime type is chosen. Note that items without + /// q-factors are given the maximum preference value. + /// + /// Returns `None` if contained list is empty. + /// + /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2 + pub fn mime_preference(&self) -> Option { + let types = self.mime_precedence(); + types.first().cloned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::header::q; + + #[test] + fn test_mime_precedence() { + let test = Accept(vec![]); + assert!(test.mime_precedence().is_empty()); + + let test = Accept(vec![qitem(mime::APPLICATION_JSON)]); + assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON)); + + let test = Accept(vec![ + qitem(mime::TEXT_HTML), + "application/xhtml+xml".parse().unwrap(), + QualityItem::new("application/xml".parse().unwrap(), q(0.9)), + QualityItem::new(mime::STAR_STAR, q(0.8)), + ]); + assert_eq!( + test.mime_precedence(), + vec![ + mime::TEXT_HTML, + "application/xhtml+xml".parse().unwrap(), + "application/xml".parse().unwrap(), + mime::STAR_STAR, + ] + ); + + let test = Accept(vec![ + qitem(mime::STAR_STAR), + qitem(mime::IMAGE_STAR), + qitem(mime::IMAGE_PNG), + ]); + assert_eq!( + test.mime_precedence(), + vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR] + ); + } + + #[test] + fn test_mime_preference() { + let test = Accept(vec![ + qitem(mime::TEXT_HTML), + "application/xhtml+xml".parse().unwrap(), + QualityItem::new("application/xml".parse().unwrap(), q(0.9)), + QualityItem::new(mime::STAR_STAR, q(0.8)), + ]); + assert_eq!(test.mime_preference(), Some(mime::TEXT_HTML)); + + let test = Accept(vec![ + QualityItem::new("video/*".parse().unwrap(), q(0.8)), + qitem(mime::IMAGE_PNG), + QualityItem::new(mime::STAR_STAR, q(0.5)), + qitem(mime::IMAGE_SVG), + QualityItem::new(mime::IMAGE_STAR, q(0.8)), + ]); + assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG)); + } } diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 98230dec..f4331a18 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -18,7 +18,7 @@ use self::internal::IntoQuality; /// /// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) /// gives more information on quality values in HTTP header fields. -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Quality(u16); impl Default for Quality { @@ -121,7 +121,7 @@ fn from_f32(f: f32) -> Quality { /// Convenience function to wrap a value in a `QualityItem` /// Sets `q` to the default 1.0 pub fn qitem(item: T) -> QualityItem { - QualityItem::new(item, Default::default()) + QualityItem::new(item, Quality::default()) } /// Convenience function to create a `Quality` from a float or integer.