mirror of https://github.com/http-rs/http-types
Merge pull request #270 from http-rs/accept
Add `content::{Accept, ContentType}` headers
This commit is contained in:
commit
9bafdd6376
|
@ -0,0 +1,433 @@
|
|||
//! Client header advertising which media types the client is able to understand.
|
||||
|
||||
use crate::content::{ContentType, MediaTypeProposal};
|
||||
use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, ACCEPT};
|
||||
use crate::utils::sort_by_weight;
|
||||
use crate::{Error, Mime, StatusCode};
|
||||
|
||||
use std::fmt::{self, Debug, Write};
|
||||
use std::option;
|
||||
use std::slice;
|
||||
|
||||
/// Client header advertising which media types the client is able to understand.
|
||||
///
|
||||
/// Using content negotiation, the server then selects one of the proposals, uses
|
||||
/// it and informs the client of its choice with the `Content-Type` response
|
||||
/// header. Browsers set adequate values for this header depending on the context
|
||||
/// where the request is done: when fetching a CSS stylesheet a different value
|
||||
/// is set for the request than when fetching an image, video or a script.
|
||||
///
|
||||
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)
|
||||
///
|
||||
/// # Specifications
|
||||
///
|
||||
/// - [RFC 7231, section 5.3.2: Accept](https://tools.ietf.org/html/rfc7231#section-5.3.2)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> http_types::Result<()> {
|
||||
/// #
|
||||
/// use http_types::content::{Accept, MediaTypeProposal};
|
||||
/// use http_types::{mime, Response};
|
||||
///
|
||||
/// let mut accept = Accept::new();
|
||||
/// accept.push(MediaTypeProposal::new(mime::HTML, Some(0.8))?);
|
||||
/// accept.push(MediaTypeProposal::new(mime::XML, Some(0.4))?);
|
||||
/// accept.push(mime::PLAIN);
|
||||
///
|
||||
/// let mut res = Response::new(200);
|
||||
/// let content_type = accept.negotiate(&[mime::XML])?;
|
||||
/// content_type.apply(&mut res);
|
||||
///
|
||||
/// assert_eq!(res["Content-Type"], "application/xml;charset=utf-8");
|
||||
/// #
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub struct Accept {
|
||||
wildcard: bool,
|
||||
entries: Vec<MediaTypeProposal>,
|
||||
}
|
||||
|
||||
impl Accept {
|
||||
/// Create a new instance of `Accept`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: vec![],
|
||||
wildcard: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an instance of `Accept` from a `Headers` instance.
|
||||
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
|
||||
let mut entries = vec![];
|
||||
let headers = match headers.as_ref().get(ACCEPT) {
|
||||
Some(headers) => headers,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let mut wildcard = false;
|
||||
|
||||
for value in headers {
|
||||
for part in value.as_str().trim().split(',') {
|
||||
let part = part.trim();
|
||||
|
||||
// Handle empty strings, and wildcard directives.
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
} else if part == "*" {
|
||||
wildcard = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try and parse a directive from a str. If the directive is
|
||||
// unkown we skip it.
|
||||
let entry = MediaTypeProposal::from_str(part)?;
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(Self { entries, wildcard }))
|
||||
}
|
||||
|
||||
/// Push a directive into the list of entries.
|
||||
pub fn push(&mut self, prop: impl Into<MediaTypeProposal>) {
|
||||
self.entries.push(prop.into());
|
||||
}
|
||||
|
||||
/// Returns `true` if a wildcard directive was passed.
|
||||
pub fn wildcard(&self) -> bool {
|
||||
self.wildcard
|
||||
}
|
||||
|
||||
/// Set the wildcard directive.
|
||||
pub fn set_wildcard(&mut self, wildcard: bool) {
|
||||
self.wildcard = wildcard
|
||||
}
|
||||
|
||||
/// Sort the header directives by weight.
|
||||
///
|
||||
/// Headers with a higher `q=` value will be returned first. If two
|
||||
/// directives have the same weight, the directive that was declared later
|
||||
/// will be returned first.
|
||||
pub fn sort(&mut self) {
|
||||
sort_by_weight(&mut self.entries);
|
||||
}
|
||||
|
||||
/// Determine the most suitable `Content-Type` encoding.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If no suitable encoding is found, an error with the status of `406` will be returned.
|
||||
pub fn negotiate(&mut self, available: &[Mime]) -> crate::Result<ContentType> {
|
||||
// Start by ordering the encodings.
|
||||
self.sort();
|
||||
|
||||
// Try and find the first encoding that matches.
|
||||
for accept in &self.entries {
|
||||
if available.contains(&accept) {
|
||||
return Ok(accept.media_type.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
// If no encoding matches and wildcard is set, send whichever encoding we got.
|
||||
if self.wildcard {
|
||||
if let Some(accept) = available.iter().next() {
|
||||
return Ok(accept.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
let mut err = Error::new_adhoc("No suitable ContentEncoding found");
|
||||
err.set_status(StatusCode::NotAcceptable);
|
||||
Err(err)
|
||||
}
|
||||
|
||||
/// Sets the `Accept-Encoding` header.
|
||||
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
|
||||
headers.as_mut().insert(ACCEPT, self.value());
|
||||
}
|
||||
|
||||
/// Get the `HeaderName`.
|
||||
pub fn name(&self) -> HeaderName {
|
||||
ACCEPT
|
||||
}
|
||||
|
||||
/// Get the `HeaderValue`.
|
||||
pub fn value(&self) -> HeaderValue {
|
||||
let mut output = String::new();
|
||||
for (n, directive) in self.entries.iter().enumerate() {
|
||||
let directive: HeaderValue = directive.clone().into();
|
||||
match n {
|
||||
0 => write!(output, "{}", directive).unwrap(),
|
||||
_ => write!(output, ", {}", directive).unwrap(),
|
||||
};
|
||||
}
|
||||
|
||||
if self.wildcard {
|
||||
match output.len() {
|
||||
0 => write!(output, "*").unwrap(),
|
||||
_ => write!(output, ", *").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: the internal string is validated to be ASCII.
|
||||
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
|
||||
}
|
||||
|
||||
/// An iterator visiting all entries.
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
Iter {
|
||||
inner: self.entries.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator visiting all entries.
|
||||
pub fn iter_mut(&mut self) -> IterMut<'_> {
|
||||
IterMut {
|
||||
inner: self.entries.iter_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Accept {
|
||||
type Item = MediaTypeProposal;
|
||||
type IntoIter = IntoIter;
|
||||
|
||||
#[inline]
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
IntoIter {
|
||||
inner: self.entries.into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Accept {
|
||||
type Item = &'a MediaTypeProposal;
|
||||
type IntoIter = Iter<'a>;
|
||||
|
||||
#[inline]
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a mut Accept {
|
||||
type Item = &'a mut MediaTypeProposal;
|
||||
type IntoIter = IterMut<'a>;
|
||||
|
||||
#[inline]
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// A borrowing iterator over entries in `Accept`.
|
||||
#[derive(Debug)]
|
||||
pub struct IntoIter {
|
||||
inner: std::vec::IntoIter<MediaTypeProposal>,
|
||||
}
|
||||
|
||||
impl Iterator for IntoIter {
|
||||
type Item = MediaTypeProposal;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
/// A lending iterator over entries in `Accept`.
|
||||
#[derive(Debug)]
|
||||
pub struct Iter<'a> {
|
||||
inner: slice::Iter<'a, MediaTypeProposal>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = &'a MediaTypeProposal;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable iterator over entries in `Accept`.
|
||||
#[derive(Debug)]
|
||||
pub struct IterMut<'a> {
|
||||
inner: slice::IterMut<'a, MediaTypeProposal>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for IterMut<'a> {
|
||||
type Item = &'a mut MediaTypeProposal;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHeaderValues for Accept {
|
||||
type Iter = option::IntoIter<HeaderValue>;
|
||||
fn to_header_values(&self) -> crate::Result<Self::Iter> {
|
||||
// A HeaderValue will always convert into itself.
|
||||
Ok(self.value().to_header_values().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Accept {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
for directive in &self.entries {
|
||||
list.entry(directive);
|
||||
}
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::mime;
|
||||
use crate::Response;
|
||||
|
||||
#[test]
|
||||
fn smoke() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.push(mime::HTML);
|
||||
|
||||
let mut headers = Response::new(200);
|
||||
accept.apply(&mut headers);
|
||||
|
||||
let accept = Accept::from_headers(headers)?.unwrap();
|
||||
assert_eq!(accept.iter().next().unwrap(), mime::HTML);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.set_wildcard(true);
|
||||
|
||||
let mut headers = Response::new(200);
|
||||
accept.apply(&mut headers);
|
||||
|
||||
let accept = Accept::from_headers(headers)?.unwrap();
|
||||
assert!(accept.wildcard());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_and_header() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.push(mime::HTML);
|
||||
accept.set_wildcard(true);
|
||||
|
||||
let mut headers = Response::new(200);
|
||||
accept.apply(&mut headers);
|
||||
|
||||
let accept = Accept::from_headers(headers)?.unwrap();
|
||||
assert!(accept.wildcard());
|
||||
assert_eq!(accept.iter().next().unwrap(), mime::HTML);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.push(mime::HTML);
|
||||
accept.push(mime::XML);
|
||||
|
||||
let mut headers = Response::new(200);
|
||||
accept.apply(&mut headers);
|
||||
|
||||
let accept = Accept::from_headers(headers)?.unwrap();
|
||||
let mut accept = accept.iter();
|
||||
assert_eq!(accept.next().unwrap(), mime::HTML);
|
||||
assert_eq!(accept.next().unwrap(), mime::XML);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reorder_based_on_weight() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.push(MediaTypeProposal::new(mime::HTML, Some(0.4))?);
|
||||
accept.push(MediaTypeProposal::new(mime::XML, None)?);
|
||||
accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
|
||||
|
||||
let mut headers = Response::new(200);
|
||||
accept.apply(&mut headers);
|
||||
|
||||
let mut accept = Accept::from_headers(headers)?.unwrap();
|
||||
accept.sort();
|
||||
let mut accept = accept.iter();
|
||||
assert_eq!(accept.next().unwrap(), mime::PLAIN);
|
||||
assert_eq!(accept.next().unwrap(), mime::HTML);
|
||||
assert_eq!(accept.next().unwrap(), mime::XML);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reorder_based_on_weight_and_location() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.push(MediaTypeProposal::new(mime::HTML, None)?);
|
||||
accept.push(MediaTypeProposal::new(mime::XML, None)?);
|
||||
accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
|
||||
|
||||
let mut res = Response::new(200);
|
||||
accept.apply(&mut res);
|
||||
|
||||
let mut accept = Accept::from_headers(res)?.unwrap();
|
||||
accept.sort();
|
||||
let mut accept = accept.iter();
|
||||
assert_eq!(accept.next().unwrap(), mime::PLAIN);
|
||||
assert_eq!(accept.next().unwrap(), mime::XML);
|
||||
assert_eq!(accept.next().unwrap(), mime::HTML);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negotiate() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.push(MediaTypeProposal::new(mime::HTML, Some(0.4))?);
|
||||
accept.push(MediaTypeProposal::new(mime::PLAIN, Some(0.8))?);
|
||||
accept.push(MediaTypeProposal::new(mime::XML, None)?);
|
||||
|
||||
assert_eq!(accept.negotiate(&[mime::HTML, mime::XML])?, mime::HTML);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negotiate_not_acceptable() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
let err = accept.negotiate(&[mime::JSON]).unwrap_err();
|
||||
assert_eq!(err.status(), 406);
|
||||
|
||||
let mut accept = Accept::new();
|
||||
accept.push(MediaTypeProposal::new(mime::JSON, Some(0.8))?);
|
||||
let err = accept.negotiate(&[mime::XML]).unwrap_err();
|
||||
assert_eq!(err.status(), 406);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negotiate_wildcard() -> crate::Result<()> {
|
||||
let mut accept = Accept::new();
|
||||
accept.push(MediaTypeProposal::new(mime::JSON, Some(0.8))?);
|
||||
accept.set_wildcard(true);
|
||||
|
||||
assert_eq!(accept.negotiate(&[mime::XML])?, mime::XML);
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
use std::{convert::TryInto, str::FromStr};
|
||||
|
||||
use crate::headers::{HeaderName, HeaderValue, Headers, CONTENT_TYPE};
|
||||
use crate::Mime;
|
||||
|
||||
/// Indicate the media type of a resource's content.
|
||||
///
|
||||
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
|
||||
///
|
||||
/// # Specifications
|
||||
///
|
||||
/// - [RFC 7231, section 3.1.1.5: Content-Type](https://tools.ietf.org/html/rfc7231#section-3.1.1.5)
|
||||
/// - [RFC 7233, section 4.1: Content-Type in multipart](https://tools.ietf.org/html/rfc7233#section-4.1)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> http_types::Result<()> {
|
||||
/// #
|
||||
/// use http_types::content::ContentType;
|
||||
/// use http_types::{Response, Mime};
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// let content_type = ContentType::new("text/*");
|
||||
///
|
||||
/// let mut res = Response::new(200);
|
||||
/// content_type.apply(&mut res);
|
||||
///
|
||||
/// let content_type = ContentType::from_headers(res)?.unwrap();
|
||||
/// assert_eq!(content_type.value(), format!("{}", Mime::from_str("text/*")?).as_str());
|
||||
/// #
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct ContentType {
|
||||
media_type: Mime,
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
/// Create a new instance.
|
||||
pub fn new<U>(media_type: U) -> Self
|
||||
where
|
||||
U: TryInto<Mime>,
|
||||
U::Error: std::fmt::Debug,
|
||||
{
|
||||
Self {
|
||||
media_type: media_type
|
||||
.try_into()
|
||||
.expect("could not convert into a valid Mime type"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new instance from headers.
|
||||
///
|
||||
/// `Content-Type` headers can provide both full and partial URLs. In
|
||||
/// order to always return fully qualified URLs, a base URL must be passed to
|
||||
/// reference the current environment. In HTTP/1.1 and above this value can
|
||||
/// always be determined from the request.
|
||||
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
|
||||
let headers = match headers.as_ref().get(CONTENT_TYPE) {
|
||||
Some(headers) => headers,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// If we successfully parsed the header then there's always at least one
|
||||
// entry. We want the last entry.
|
||||
let ctation = headers.iter().last().unwrap();
|
||||
|
||||
let media_type = Mime::from_str(ctation.as_str()).map_err(|mut e| {
|
||||
e.set_status(400);
|
||||
e
|
||||
})?;
|
||||
Ok(Some(Self { media_type }))
|
||||
}
|
||||
|
||||
/// Sets the header.
|
||||
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
|
||||
headers.as_mut().insert(self.name(), self.value());
|
||||
}
|
||||
|
||||
/// Get the `HeaderName`.
|
||||
pub fn name(&self) -> HeaderName {
|
||||
CONTENT_TYPE
|
||||
}
|
||||
|
||||
/// Get the `HeaderValue`.
|
||||
pub fn value(&self) -> HeaderValue {
|
||||
let output = format!("{}", self.media_type);
|
||||
// SAFETY: the internal string is validated to be ASCII.
|
||||
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Mime> for ContentType {
|
||||
fn eq(&self, other: &Mime) -> bool {
|
||||
&self.media_type == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&Mime> for ContentType {
|
||||
fn eq(&self, other: &&Mime) -> bool {
|
||||
&&self.media_type == other
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mime> for ContentType {
|
||||
fn from(media_type: Mime) -> Self {
|
||||
Self { media_type }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::headers::Headers;
|
||||
|
||||
#[test]
|
||||
fn smoke() -> crate::Result<()> {
|
||||
let ct = ContentType::new(Mime::from_str("text/*")?);
|
||||
|
||||
let mut headers = Headers::new();
|
||||
ct.apply(&mut headers);
|
||||
|
||||
let ct = ContentType::from_headers(headers)?.unwrap();
|
||||
assert_eq!(
|
||||
ct.value(),
|
||||
format!("{}", Mime::from_str("text/*")?).as_str()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_request_on_parse_error() -> crate::Result<()> {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert(CONTENT_TYPE, "<nori ate the tag. yum.>");
|
||||
let err = ContentType::from_headers(headers).unwrap_err();
|
||||
assert_eq!(err.status(), 400);
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
use crate::ensure;
|
||||
use crate::headers::HeaderValue;
|
||||
use crate::Mime;
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{
|
||||
cmp::{Ordering, PartialEq},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// A proposed Media Type for the `Accept` header.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MediaTypeProposal {
|
||||
/// The proposed media_type.
|
||||
pub(crate) media_type: Mime,
|
||||
|
||||
/// The weight of the proposal.
|
||||
///
|
||||
/// This is a number between 0.0 and 1.0, and is max 3 decimal points.
|
||||
weight: Option<f32>,
|
||||
}
|
||||
|
||||
impl MediaTypeProposal {
|
||||
/// Create a new instance of `MediaTypeProposal`.
|
||||
pub fn new(media_type: impl Into<Mime>, weight: Option<f32>) -> crate::Result<Self> {
|
||||
if let Some(weight) = weight {
|
||||
ensure!(
|
||||
weight.is_sign_positive() && weight <= 1.0,
|
||||
"MediaTypeProposal should have a weight between 0.0 and 1.0"
|
||||
)
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
media_type: media_type.into(),
|
||||
weight,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the proposed media_type.
|
||||
pub fn media_type(&self) -> &Mime {
|
||||
&self.media_type
|
||||
}
|
||||
|
||||
/// Get the weight of the proposal.
|
||||
pub fn weight(&self) -> Option<f32> {
|
||||
self.weight
|
||||
}
|
||||
|
||||
/// Parse a string into a media type proposal.
|
||||
///
|
||||
/// Because `;` and `q=0.0` are all valid values for in use in a media type,
|
||||
/// we have to parse the full string to the media type first, and then see if
|
||||
/// a `q` value has been set.
|
||||
pub(crate) fn from_str(s: &str) -> crate::Result<Self> {
|
||||
let mut media_type = Mime::from_str(s)?;
|
||||
let weight = media_type
|
||||
.remove_param("q")
|
||||
.map(|param| param.as_str().parse())
|
||||
.transpose()?;
|
||||
Ok(Self::new(media_type, weight)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mime> for MediaTypeProposal {
|
||||
fn from(media_type: Mime) -> Self {
|
||||
Self {
|
||||
media_type,
|
||||
weight: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaTypeProposal> for Mime {
|
||||
fn from(accept: MediaTypeProposal) -> Self {
|
||||
accept.media_type
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Mime> for MediaTypeProposal {
|
||||
fn eq(&self, other: &Mime) -> bool {
|
||||
self.media_type == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Mime> for &MediaTypeProposal {
|
||||
fn eq(&self, other: &Mime) -> bool {
|
||||
self.media_type == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MediaTypeProposal {
|
||||
type Target = Mime;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.media_type
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for MediaTypeProposal {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.media_type
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: For Accept-Encoding Firefox sends the values: `gzip, deflate, br`. This means
|
||||
// when parsing media_types we should choose the last value in the list under
|
||||
// equal weights. This impl doesn't know which value was passed later, so that
|
||||
// behavior needs to be handled separately.
|
||||
//
|
||||
// NOTE: This comparison does not include a notion of `*` (any value is valid).
|
||||
// that needs to be handled separately.
|
||||
impl PartialOrd for MediaTypeProposal {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
match (self.weight, other.weight) {
|
||||
(Some(left), Some(right)) => left.partial_cmp(&right),
|
||||
(Some(_), None) => Some(Ordering::Greater),
|
||||
(None, Some(_)) => Some(Ordering::Less),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaTypeProposal> for HeaderValue {
|
||||
fn from(entry: MediaTypeProposal) -> HeaderValue {
|
||||
let s = match entry.weight {
|
||||
Some(weight) => format!("{};q={:.3}", entry.media_type, weight),
|
||||
None => entry.media_type.to_string(),
|
||||
};
|
||||
unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::mime;
|
||||
|
||||
#[test]
|
||||
fn smoke() -> crate::Result<()> {
|
||||
let _ = MediaTypeProposal::new(mime::JSON, Some(0.0)).unwrap();
|
||||
let _ = MediaTypeProposal::new(mime::XML, Some(0.5)).unwrap();
|
||||
let _ = MediaTypeProposal::new(mime::HTML, Some(1.0)).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_code_500() -> crate::Result<()> {
|
||||
let err = MediaTypeProposal::new(mime::JSON, Some(1.1)).unwrap_err();
|
||||
assert_eq!(err.status(), 500);
|
||||
|
||||
let err = MediaTypeProposal::new(mime::XML, Some(-0.1)).unwrap_err();
|
||||
assert_eq!(err.status(), 500);
|
||||
|
||||
let err = MediaTypeProposal::new(mime::HTML, Some(-0.0)).unwrap_err();
|
||||
assert_eq!(err.status(), 500);
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -4,20 +4,53 @@
|
|||
//! about which content it prefers, and the server responds by sharing which
|
||||
//! content it's chosen to share. This enables clients to receive resources with the
|
||||
//! best available compression, in the preferred language, and more.
|
||||
//!
|
||||
//! # Further Reading
|
||||
//!
|
||||
//! - [MDN: Content Negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation)
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! # fn main() -> http_types::Result<()> {
|
||||
//! #
|
||||
//! use http_types::content::{Accept, MediaTypeProposal};
|
||||
//! use http_types::{mime, Response};
|
||||
//!
|
||||
//! let mut accept = Accept::new();
|
||||
//! accept.push(MediaTypeProposal::new(mime::HTML, Some(0.8))?);
|
||||
//! accept.push(MediaTypeProposal::new(mime::XML, Some(0.4))?);
|
||||
//! accept.push(mime::PLAIN);
|
||||
//!
|
||||
//! let mut res = Response::new(200);
|
||||
//! let content_type = accept.negotiate(&[mime::XML])?;
|
||||
//! content_type.apply(&mut res);
|
||||
//!
|
||||
//! assert_eq!(res["Content-Type"], "application/xml;charset=utf-8");
|
||||
//! #
|
||||
//! # Ok(()) }
|
||||
//! ```
|
||||
|
||||
pub mod accept;
|
||||
pub mod accept_encoding;
|
||||
pub mod content_encoding;
|
||||
|
||||
mod content_length;
|
||||
mod content_location;
|
||||
mod content_type;
|
||||
mod encoding;
|
||||
mod encoding_proposal;
|
||||
mod media_type_proposal;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use accept::Accept;
|
||||
#[doc(inline)]
|
||||
pub use accept_encoding::AcceptEncoding;
|
||||
#[doc(inline)]
|
||||
pub use content_encoding::ContentEncoding;
|
||||
pub use content_length::ContentLength;
|
||||
pub use content_location::ContentLocation;
|
||||
pub use content_type::ContentType;
|
||||
pub use encoding::Encoding;
|
||||
pub use encoding_proposal::EncodingProposal;
|
||||
pub use media_type_proposal::MediaTypeProposal;
|
||||
|
|
|
@ -109,6 +109,33 @@ impl Mime {
|
|||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Remove a param from the set. Returns the `ParamValue` if it was contained within the set.
|
||||
pub fn remove_param(&mut self, name: impl Into<ParamName>) -> Option<ParamValue> {
|
||||
let name: ParamName = name.into();
|
||||
let mut unset_params = false;
|
||||
let ret = self
|
||||
.params
|
||||
.as_mut()
|
||||
.map(|inner| match inner {
|
||||
ParamKind::Vec(v) => match v.iter().position(|(k, _)| k == &name) {
|
||||
Some(index) => Some(v.remove(index).1),
|
||||
None => None,
|
||||
},
|
||||
ParamKind::Utf8 => match name {
|
||||
ParamName(Cow::Borrowed("charset")) => {
|
||||
unset_params = true;
|
||||
Some(ParamValue(Cow::Borrowed("utf8")))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
})
|
||||
.flatten();
|
||||
if unset_params {
|
||||
self.params = None;
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Mime> for Mime {
|
||||
|
|
|
@ -32,8 +32,8 @@ pub(crate) fn parse_weight(s: &str) -> crate::Result<f32> {
|
|||
|
||||
/// Order proposals by weight. Try ordering by q value first. If equal or undefined,
|
||||
/// order by index, favoring the latest provided value.
|
||||
pub(crate) fn sort_by_weight<T: PartialOrd + Copy>(props: &mut Vec<T>) {
|
||||
let mut arr: Vec<(usize, T)> = props.iter().copied().enumerate().collect();
|
||||
pub(crate) fn sort_by_weight<T: PartialOrd + Clone>(props: &mut Vec<T>) {
|
||||
let mut arr: Vec<(usize, T)> = props.iter().cloned().enumerate().collect();
|
||||
arr.sort_unstable_by(|a, b| match b.1.partial_cmp(&a.1) {
|
||||
None | Some(Ordering::Equal) => b.0.cmp(&a.0),
|
||||
Some(ord) => ord,
|
||||
|
|
Loading…
Reference in New Issue