From dacf8f7b6b45c111aea8768bf6b38c42394ebc01 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 3 Jan 2021 00:29:02 +0000 Subject: [PATCH] add IntoHeaderPair trait --- actix-http/CHANGES.md | 8 ++ actix-http/src/header/common/if_range.rs | 10 +-- actix-http/src/header/into_pair.rs | 84 ++++++++++++++++++ actix-http/src/header/into_value.rs | 97 ++++++++++++++++++++ actix-http/src/header/map.rs | 2 +- actix-http/src/header/mod.rs | 108 ++--------------------- actix-http/src/header/shared/mod.rs | 2 +- actix-http/src/httpmessage.rs | 10 +-- actix-http/src/response.rs | 60 +++++++++---- 9 files changed, 253 insertions(+), 128 deletions(-) create mode 100644 actix-http/src/header/into_pair.rs create mode 100644 actix-http/src/header/into_value.rs diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index e9a94300b..2105e25e3 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,7 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed * `Response::content_type` now takes an `impl IntoHeaderValue` to support `mime` types. [#1894] +* `Response::set` and `Response::header` methods; use the respective `Response::set_header` and + `Response::append_header` methods. [#1869] [#1894]: https://github.com/actix/actix-web/pull/1894 @@ -32,7 +35,12 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 [#1857]: https://github.com/actix/actix-web/pull/1857 [#1864]: https://github.com/actix/actix-web/pull/1864 +<<<<<<< HEAD [#1878]: https://github.com/actix/actix-web/pull/1878 +======= +[#1869]: https://github.com/actix/actix-web/pull/1869 + +>>>>>>> 587000a2... add IntoHeaderPair trait ## 2.2.0 - 2020-11-25 ### Added diff --git a/actix-http/src/header/common/if_range.rs b/actix-http/src/header/common/if_range.rs index b14ad0391..017df9e4f 100644 --- a/actix-http/src/header/common/if_range.rs +++ b/actix-http/src/header/common/if_range.rs @@ -42,14 +42,13 @@ use crate::httpmessage::HttpMessage; /// let mut builder = Response::Ok(); /// builder.set(IfRange::EntityTag(EntityTag::new( /// false, -/// "xyzzy".to_owned(), +/// "abc".to_owned(), /// ))); /// ``` /// /// ```rust -/// use actix_http::Response; -/// use actix_http::http::header::IfRange; /// use std::time::{Duration, SystemTime}; +/// use actix_http::{http::header::IfRange, Response}; /// /// let mut builder = Response::Ok(); /// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); @@ -57,9 +56,10 @@ use crate::httpmessage::HttpMessage; /// ``` #[derive(Clone, Debug, PartialEq)] pub enum IfRange { - /// The entity-tag the client has of the resource + /// The entity-tag the client has of the resource. EntityTag(EntityTag), - /// The date when the client retrieved the resource + + /// The date when the client retrieved the resource. Date(HttpDate), } diff --git a/actix-http/src/header/into_pair.rs b/actix-http/src/header/into_pair.rs new file mode 100644 index 000000000..a91f26f46 --- /dev/null +++ b/actix-http/src/header/into_pair.rs @@ -0,0 +1,84 @@ +use std::convert::{Infallible, TryFrom}; + +use either::Either; +use http::{ + header::{HeaderName, InvalidHeaderName, InvalidHeaderValue}, + HeaderValue, +}; + +/// A trait for transforming things +pub trait IntoHeaderPair: Sized { + type Error; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>; +} + +impl IntoHeaderPair for (HeaderName, HeaderValue) { + type Error = Infallible; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + Ok(self) + } +} + +impl IntoHeaderPair for (HeaderName, &str) { + type Error = InvalidHeaderValue; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + let (name, value) = self; + let value = HeaderValue::try_from(value)?; + Ok((name, value)) + } +} + +impl IntoHeaderPair for (&str, HeaderValue) { + type Error = InvalidHeaderName; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + let (name, value) = self; + let name = HeaderName::try_from(name)?; + Ok((name, value)) + } +} + +impl IntoHeaderPair for (&str, &str) { + type Error = Either; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + let (name, value) = self; + let name = HeaderName::try_from(name).map_err(Either::Left)?; + let value = HeaderValue::try_from(value).map_err(Either::Right)?; + Ok((name, value)) + } +} + +impl IntoHeaderPair for (HeaderName, String) { + type Error = InvalidHeaderValue; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + let (name, value) = self; + let value = HeaderValue::try_from(&value)?; + Ok((name, value)) + } +} + +impl IntoHeaderPair for (String, HeaderValue) { + type Error = InvalidHeaderName; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + let (name, value) = self; + let name = HeaderName::try_from(&name)?; + Ok((name, value)) + } +} + +impl IntoHeaderPair for (String, String) { + type Error = Either; + + fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + let (name, value) = self; + let name = HeaderName::try_from(&name).map_err(Either::Left)?; + let value = HeaderValue::try_from(&value).map_err(Either::Right)?; + Ok((name, value)) + } +} diff --git a/actix-http/src/header/into_value.rs b/actix-http/src/header/into_value.rs new file mode 100644 index 000000000..3a7a90284 --- /dev/null +++ b/actix-http/src/header/into_value.rs @@ -0,0 +1,97 @@ +use std::convert::TryFrom; + +use bytes::Bytes; +use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; +use mime::Mime; + +/// A trait for any object that can be Converted to a `HeaderValue` +pub trait IntoHeaderValue: Sized { + /// The type returned in the event of a conversion error. + type Error: Into; + + /// Try to convert value to a Header pair value. + fn try_into(self) -> Result; +} + +impl IntoHeaderValue for HeaderValue { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + Ok(self) + } +} + +impl<'a> IntoHeaderValue for &'a str { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + self.parse() + } +} + +impl<'a> IntoHeaderValue for &'a [u8] { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_bytes(self) + } +} + +impl IntoHeaderValue for Bytes { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_maybe_shared(self) + } +} + +impl IntoHeaderValue for Vec { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::try_from(self) + } +} + +impl IntoHeaderValue for String { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::try_from(self) + } +} + +impl IntoHeaderValue for usize { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + let s = format!("{}", self); + HeaderValue::try_from(s) + } +} + +impl IntoHeaderValue for u64 { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + let s = format!("{}", self); + HeaderValue::try_from(s) + } +} + +impl IntoHeaderValue for Mime { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::try_from(format!("{}", self)) + } +} diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index adcf579d0..4c99f08fc 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -7,7 +7,7 @@ use http::header::{HeaderName, HeaderValue}; /// A set of HTTP headers /// /// `HeaderMap` is an multi-map of [`HeaderName`] to values. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct HeaderMap { pub(crate) inner: AHashMap, } diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 4a74dfb17..cfc44e48f 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -1,10 +1,8 @@ //! Various HTTP headers. -use std::{convert::TryFrom, fmt, str::FromStr}; +use std::{fmt, str::FromStr}; use bytes::{Bytes, BytesMut}; -use http::Error as HttpError; -use mime::Mime; use percent_encoding::{AsciiSet, CONTROLS}; pub use http::header::*; @@ -12,6 +10,9 @@ pub use http::header::*; use crate::error::ParseError; use crate::httpmessage::HttpMessage; +mod into_pair; +mod into_value; + mod common; pub(crate) mod map; mod shared; @@ -19,15 +20,14 @@ pub use self::common::*; #[doc(hidden)] pub use self::shared::*; +pub use self::into_pair::IntoHeaderPair; +pub use self::into_value::IntoHeaderValue; #[doc(hidden)] pub use self::map::GetAll; pub use self::map::HeaderMap; -/// A trait for any object that will represent a header field and value. -pub trait Header -where - Self: IntoHeaderValue, -{ +/// A trait for any object that already represents a valid header field and value. +pub trait Header: IntoHeaderValue { /// Returns the name of the header field fn name() -> HeaderName; @@ -35,98 +35,6 @@ where fn parse(msg: &T) -> Result; } -/// A trait for any object that can be Converted to a `HeaderValue` -pub trait IntoHeaderValue: Sized { - /// The type returned in the event of a conversion error. - type Error: Into; - - /// Try to convert value to a Header value. - fn try_into(self) -> Result; -} - -impl IntoHeaderValue for HeaderValue { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - Ok(self) - } -} - -impl<'a> IntoHeaderValue for &'a str { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - self.parse() - } -} - -impl<'a> IntoHeaderValue for &'a [u8] { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_bytes(self) - } -} - -impl IntoHeaderValue for Bytes { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_maybe_shared(self) - } -} - -impl IntoHeaderValue for Vec { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::try_from(self) - } -} - -impl IntoHeaderValue for String { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::try_from(self) - } -} - -impl IntoHeaderValue for usize { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - let s = format!("{}", self); - HeaderValue::try_from(s) - } -} - -impl IntoHeaderValue for u64 { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - let s = format!("{}", self); - HeaderValue::try_from(s) - } -} - -impl IntoHeaderValue for Mime { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::try_from(format!("{}", self)) - } -} - /// Represents supported types of content encodings #[derive(Copy, Clone, PartialEq, Debug)] pub enum ContentEncoding { diff --git a/actix-http/src/header/shared/mod.rs b/actix-http/src/header/shared/mod.rs index f2bc91634..d14f55ce9 100644 --- a/actix-http/src/header/shared/mod.rs +++ b/actix-http/src/header/shared/mod.rs @@ -1,4 +1,4 @@ -//! Copied for `hyper::header::shared`; +//! Originally taken from `hyper::header::shared`. pub use self::charset::Charset; pub use self::encoding::Encoding; diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/httpmessage.rs index 471fbbcdc..80c4c4d41 100644 --- a/actix-http/src/httpmessage.rs +++ b/actix-http/src/httpmessage.rs @@ -13,7 +13,7 @@ use crate::payload::Payload; struct Cookies(Vec>); -/// Trait that implements general purpose operations on http messages +/// Trait that implements general purpose operations on HTTP messages. pub trait HttpMessage: Sized { /// Type of message payload stream type Stream; @@ -30,8 +30,8 @@ pub trait HttpMessage: Sized { /// Mutable reference to a the request's extensions container fn extensions_mut(&self) -> RefMut<'_, Extensions>; + /// Get a header. #[doc(hidden)] - /// Get a header fn get_header(&self) -> Option where Self: Sized, @@ -43,8 +43,8 @@ pub trait HttpMessage: Sized { } } - /// Read the request content type. If request does not contain - /// *Content-Type* header, empty str get returned. + /// Read the request content type. If request did not contain a *Content-Type* header, an empty + /// string is returned. fn content_type(&self) -> &str { if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { if let Ok(content_type) = content_type.to_str() { @@ -90,7 +90,7 @@ pub trait HttpMessage: Sized { Ok(None) } - /// Check if request has chunked transfer encoding + /// Check if request has chunked transfer encoding. fn chunked(&self) -> Result { if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) { if let Ok(s) = encodings.to_str() { diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 0a1f2cfd2..1a17d8da2 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -14,7 +14,7 @@ use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; use crate::cookie::{Cookie, CookieJar}; use crate::error::Error; use crate::extensions::Extensions; -use crate::header::{Header, IntoHeaderValue}; +use crate::header::{Header, IntoHeaderPair, IntoHeaderValue}; use crate::http::header::{self, HeaderName, HeaderValue}; use crate::http::{Error as HttpError, HeaderMap, StatusCode}; use crate::message::{BoxedResponseHead, ConnectionType, ResponseHead}; @@ -399,35 +399,57 @@ impl ResponseBuilder { self } - /// Set a header. + /// Insert a header, replacing any that existed with an equivalent field name. /// /// ```rust /// use actix_http::{http, Request, Response}; /// /// fn index(req: Request) -> Response { /// Response::Ok() - /// .set_header("X-TEST", "value") - /// .set_header(http::header::CONTENT_TYPE, "application/json") + /// .set_header(("X-TEST", "value")) + /// .set_header(ContentType(mime::APPLICATION_JSON)) /// .finish() /// } /// ``` - pub fn set_header(&mut self, key: K, value: V) -> &mut Self + pub fn insert_header(&mut self, header: H) -> &mut Self where - HeaderName: TryFrom, - >::Error: Into, - V: IntoHeaderValue, + H: IntoHeaderPair, + H::Error: Into, { if let Some(parts) = parts(&mut self.head, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, + match header.try_into_header_pair() { + Ok((key, value)) => parts.headers.insert(key, value), Err(e) => self.err = Some(e.into()), }; } + + self + } + + /// Append a header, keeping any that existed with an equivalent field name. + /// + /// ```rust + /// use actix_http::{http, Request, Response}; + /// + /// fn index(req: Request) -> Response { + /// Response::Ok() + /// .append_header(("X-TEST", "value")) + /// .append_header(ContentType(mime::APPLICATION_JSON)) + /// .finish() + /// } + /// ``` + pub fn append_header(&mut self, header: H) -> &mut Self + where + H: IntoHeaderPair, + H::Error: Into, + { + if let Some(parts) = parts(&mut self.head, &self.err) { + match header.try_into_header_pair() { + Ok((key, value)) => parts.headers.append(key, value), + Err(e) => self.err = Some(e.into()), + }; + } + self } @@ -458,7 +480,13 @@ impl ResponseBuilder { if let Some(parts) = parts(&mut self.head, &self.err) { parts.set_connection_type(ConnectionType::Upgrade); } - self.set_header(header::UPGRADE, value) + + // TODO: fix signature + if let Ok(value) = value.try_into() { + self.insert_header((header::UPGRADE, value)); + } + + self } /// Force close connection, even if it is marked as keep-alive