From 0c7d375ba3b91c2193c82e9c348337b692022328 Mon Sep 17 00:00:00 2001 From: Sam Rijs Date: Sat, 23 Sep 2017 05:07:57 +1000 Subject: [PATCH] feat(lib): implement compatibility with http crate --- .travis.yml | 2 + Cargo.toml | 2 + src/client/compat.rs | 6 +++ src/client/compat_impl.rs | 53 ++++++++++++++++++++++++ src/client/mod.rs | 19 +++++++++ src/common/str.rs | 5 +++ src/header/mod.rs | 76 ++++++++++++++++++++++++++++++++++ src/http/request.rs | 37 ++++++++++++++++- src/http/response.rs | 29 +++++++++++++ src/lib.rs | 2 + src/method.rs | 86 +++++++++++++++++++++++++++++++++++++++ src/server/compat.rs | 7 ++++ src/server/compat_impl.rs | 82 +++++++++++++++++++++++++++++++++++++ src/server/mod.rs | 41 +++++++++++++++++++ src/status.rs | 33 +++++++++++++++ src/uri.rs | 20 +++++++++ src/version.rs | 36 ++++++++++++++++ 17 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 src/client/compat.rs create mode 100644 src/client/compat_impl.rs create mode 100644 src/server/compat.rs create mode 100644 src/server/compat_impl.rs diff --git a/.travis.yml b/.travis.yml index ea6cf68c6a..3f9c8da41c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ matrix: env: FEATURES="--features nightly" - rust: beta - rust: stable + - rust: stable + env: FEATURES="--features compat" - rust: 1.15.0 cache: diff --git a/Cargo.toml b/Cargo.toml index 75900aa168..f8abaa3f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ base64 = "0.6" bytes = "0.4.4" futures = "0.1.14" futures-cpupool = "0.1" +http = { version = "0.1", optional = true } httparse = "1.0" language-tags = "0.2" log = "0.3" @@ -45,3 +46,4 @@ spmc = "0.2" default = [] nightly = [] raw_status = [] +compat = [ "http" ] \ No newline at end of file diff --git a/src/client/compat.rs b/src/client/compat.rs new file mode 100644 index 0000000000..277d7fee9c --- /dev/null +++ b/src/client/compat.rs @@ -0,0 +1,6 @@ +//! Wrappers to build compatibility with the `http` crate. + +pub use super::compat_impl::{ + CompatClient, + CompatFutureResponse +}; diff --git a/src/client/compat_impl.rs b/src/client/compat_impl.rs new file mode 100644 index 0000000000..6ff7d3ebe8 --- /dev/null +++ b/src/client/compat_impl.rs @@ -0,0 +1,53 @@ +use futures::{Future, Poll, Stream}; +use http_types; +use tokio_service::Service; + +use client::{Connect, Client, FutureResponse}; +use error::Error; +use http::Body; + +/// A Client to make outgoing HTTP requests. +#[derive(Debug)] +pub struct CompatClient { + inner: Client +} + +pub fn client(client: Client) -> CompatClient { + CompatClient { inner: client } +} + +impl Service for CompatClient +where C: Connect, + B: Stream + 'static, + B::Item: AsRef<[u8]>, +{ + type Request = http_types::Request; + type Response = http_types::Response; + type Error = Error; + type Future = CompatFutureResponse; + + fn call(&self, req: Self::Request) -> Self::Future { + future(self.inner.call(req.into())) + } +} + +/// A `Future` that will resolve to an `http::Response`. +#[must_use = "futures do nothing unless polled"] +#[derive(Debug)] +pub struct CompatFutureResponse { + inner: FutureResponse +} + +pub fn future(fut: FutureResponse) -> CompatFutureResponse { + CompatFutureResponse { inner: fut } +} + +impl Future for CompatFutureResponse { + type Item = http_types::Response; + type Error = Error; + + fn poll(&mut self) -> Poll { + self.inner.poll() + .map(|a| a.map(|r| r.into())) + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 6f6e26805b..e4b26f66a9 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -9,6 +9,8 @@ use std::time::Duration; use futures::{future, Poll, Async, Future, Stream}; use futures::unsync::oneshot; +#[cfg(feature = "compat")] +use http_types; use tokio_io::{AsyncRead, AsyncWrite}; use tokio::reactor::Handle; use tokio_proto::BindClient; @@ -33,6 +35,10 @@ pub use self::connect::{HttpConnector, Connect}; mod connect; mod dns; mod pool; +#[cfg(feature = "compat")] +mod compat_impl; +#[cfg(feature = "compat")] +pub mod compat; /// A Client to make outgoing HTTP requests. // If the Connector is clone, then the Client can be clone easily. @@ -108,6 +114,19 @@ where C: Connect, pub fn request(&self, req: Request) -> FutureResponse { self.call(req) } + + /// Send an `http::Request` using this Client. + #[inline] + #[cfg(feature = "compat")] + pub fn request_compat(&self, req: http_types::Request) -> compat::CompatFutureResponse { + self::compat_impl::future(self.call(req.into())) + } + + /// Convert into a client accepting `http::Request`. + #[cfg(feature = "compat")] + pub fn into_compat(self) -> compat::CompatClient { + self::compat_impl::client(self) + } } /// A `Future` that will resolve to an HTTP Response. diff --git a/src/common/str.rs b/src/common/str.rs index 4479a01bb5..5615560189 100644 --- a/src/common/str.rs +++ b/src/common/str.rs @@ -29,6 +29,11 @@ impl ByteStr { pub fn as_str(&self) -> &str { unsafe { str::from_utf8_unchecked(self.0.as_ref()) } } + + #[cfg(feature = "compat")] + pub fn into_bytes(self) -> Bytes { + self.0 + } } impl Deref for ByteStr { diff --git a/src/header/mod.rs b/src/header/mod.rs index 883568980b..56969d7683 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -77,9 +77,14 @@ //! } //! ``` use std::borrow::{Cow, ToOwned}; +#[cfg(feature = "compat")] +use std::convert::From; use std::iter::{FromIterator, IntoIterator}; use std::{mem, fmt}; +#[cfg(feature = "compat")] +use http_types; + use unicase::Ascii; use self::internals::{Item, VecMap, Entry}; @@ -546,6 +551,54 @@ impl fmt::Debug for Headers { } } +#[cfg(feature = "compat")] +impl From for Headers { + fn from(mut header_map: http_types::HeaderMap) -> Headers { + let mut headers = Headers::new(); + for (name, mut value_drain) in header_map.drain() { + if let Some(first_value) = value_drain.next() { + let mut raw: Raw = first_value.as_bytes().into(); + for value in value_drain { + raw.push(value.as_bytes()); + } + headers.append_raw(name.as_str().to_string(), raw); + } + } + headers + } +} + +#[cfg(feature = "compat")] +impl From for http_types::HeaderMap { + fn from(headers: Headers) -> http_types::HeaderMap { + let mut header_map = http_types::HeaderMap::new(); + for header in headers.iter() { + let entry = header_map.entry(header.name()) + .expect("attempted to convert invalid header name"); + let mut value_iter = header.raw().iter().map(|line| { + http_types::header::HeaderValue::from_bytes(line) + .expect("attempted to convert invalid header value") + }); + match entry { + http_types::header::Entry::Occupied(mut occupied) => { + for value in value_iter { + occupied.append(value); + } + }, + http_types::header::Entry::Vacant(vacant) => { + if let Some(first_value) = value_iter.next() { + let mut occupied = vacant.insert_entry(first_value); + for value in value_iter { + occupied.append(value); + } + } + } + } + } + header_map + } +} + /// An `Iterator` over the fields in a `Headers` map. #[allow(missing_debug_implementations)] pub struct HeadersItems<'a> { @@ -940,6 +993,29 @@ mod tests { assert_ne!(headers1, headers2); } + #[test] + #[cfg(feature = "compat")] + fn test_compat() { + use http_types; + + let mut orig_hyper_headers = Headers::new(); + orig_hyper_headers.set(ContentLength(11)); + orig_hyper_headers.set(Host::new("foo.bar", None)); + orig_hyper_headers.append_raw("x-foo", b"bar".to_vec()); + orig_hyper_headers.append_raw("x-foo", b"quux".to_vec()); + + let mut orig_http_headers = http_types::HeaderMap::new(); + orig_http_headers.insert(http_types::header::CONTENT_LENGTH, "11".parse().unwrap()); + orig_http_headers.insert(http_types::header::HOST, "foo.bar".parse().unwrap()); + orig_http_headers.append("x-foo", "bar".parse().unwrap()); + orig_http_headers.append("x-foo", "quux".parse().unwrap()); + + let conv_hyper_headers: Headers = orig_http_headers.clone().into(); + let conv_http_headers: http_types::HeaderMap = orig_hyper_headers.clone().into(); + assert_eq!(orig_hyper_headers, conv_hyper_headers); + assert_eq!(orig_http_headers, conv_http_headers); + } + #[cfg(feature = "nightly")] #[bench] fn bench_headers_new(b: &mut Bencher) { diff --git a/src/http/request.rs b/src/http/request.rs index 2d08bace90..f8f68d4607 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,11 +1,16 @@ use std::fmt; +#[cfg(feature = "compat")] +use std::mem::replace; +use std::net::SocketAddr; + +#[cfg(feature = "compat")] +use http_types; use header::Headers; use http::{Body, MessageHead, RequestHead, RequestLine}; use method::Method; use uri::{self, Uri}; use version::HttpVersion; -use std::net::SocketAddr; /// An HTTP Request pub struct Request { @@ -132,6 +137,36 @@ impl fmt::Debug for Request { } } +#[cfg(feature = "compat")] +impl From for http_types::Request { + fn from(from_req: Request) -> http_types::Request { + let (m, u, v, h, b) = from_req.deconstruct(); + + let to_req = http_types::Request::new(()); + let (mut to_parts, _) = to_req.into_parts(); + + to_parts.method = m.into(); + to_parts.uri = u.into(); + to_parts.version = v.into(); + to_parts.headers = h.into(); + + http_types::Request::from_parts(to_parts, b) + } +} + +#[cfg(feature = "compat")] +impl From> for Request { + fn from(from_req: http_types::Request) -> Request { + let (from_parts, body) = from_req.into_parts(); + + let mut to_req = Request::new(from_parts.method.into(), from_parts.uri.into()); + to_req.set_version(from_parts.version.into()); + replace(to_req.headers_mut(), from_parts.headers.into()); + to_req.set_body(body); + to_req + } +} + /// Constructs a request using a received ResponseHead and optional body pub fn from_wire(addr: Option, incoming: RequestHead, body: B) -> Request { let MessageHead { version, subject: RequestLine(method, uri), headers } = incoming; diff --git a/src/http/response.rs b/src/http/response.rs index 480702dbbf..c96facd0d8 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,4 +1,9 @@ use std::fmt; +#[cfg(feature = "compat")] +use std::mem::replace; + +#[cfg(feature = "compat")] +use http_types; use header::{Header, Headers}; use http::{MessageHead, ResponseHead, Body}; @@ -142,6 +147,30 @@ impl fmt::Debug for Response { } } +#[cfg(feature = "compat")] +impl From> for Response { + fn from(from_res: http_types::Response) -> Response { + let (from_parts, body) = from_res.into_parts(); + let mut to_res = Response::new(); + to_res.version = from_parts.version.into(); + to_res.set_status(from_parts.status.into()); + replace(to_res.headers_mut(), from_parts.headers.into()); + to_res.with_body(body) + } +} + +#[cfg(feature = "compat")] +impl From for http_types::Response { + fn from(mut from_res: Response) -> http_types::Response { + let (mut to_parts, ()) = http_types::Response::new(()).into_parts(); + to_parts.version = from_res.version().into(); + to_parts.status = from_res.status().into(); + let from_headers = replace(from_res.headers_mut(), Headers::new()); + to_parts.headers = from_headers.into(); + http_types::Response::from_parts(to_parts, from_res.body()) + } +} + /// Constructs a response using a received ResponseHead and optional body #[inline] #[cfg(not(feature = "raw_status"))] diff --git a/src/lib.rs b/src/lib.rs index 13e66a0320..07b734d871 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,8 @@ extern crate base64; extern crate bytes; #[macro_use] extern crate futures; extern crate futures_cpupool; +#[cfg(feature = "compat")] +extern crate http as http_types; extern crate httparse; extern crate language_tags; #[macro_use] extern crate log; diff --git a/src/method.rs b/src/method.rs index 5e3164242a..35d83ff5a7 100644 --- a/src/method.rs +++ b/src/method.rs @@ -3,6 +3,9 @@ use std::fmt; use std::str::FromStr; use std::convert::AsRef; +#[cfg(feature = "compat")] +use http_types; + use error::Error; use self::Method::{Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch, Extension}; @@ -156,6 +159,68 @@ impl Default for Method { } } +#[cfg(feature = "compat")] +impl From for Method { + fn from(method: http_types::Method) -> Method { + match method { + http_types::Method::GET => + Method::Get, + http_types::Method::POST => + Method::Post, + http_types::Method::PUT => + Method::Put, + http_types::Method::DELETE => + Method::Delete, + http_types::Method::HEAD => + Method::Head, + http_types::Method::OPTIONS => + Method::Options, + http_types::Method::CONNECT => + Method::Connect, + http_types::Method::PATCH => + Method::Patch, + http_types::Method::TRACE => + Method::Trace, + _ => { + method.as_ref().parse() + .expect("attempted to convert invalid method") + } + } + } +} + +#[cfg(feature = "compat")] +impl From for http_types::Method { + fn from(method: Method) -> http_types::Method { + use http_types::HttpTryFrom; + + match method { + Method::Get => + http_types::Method::GET, + Method::Post => + http_types::Method::POST, + Method::Put => + http_types::Method::PUT, + Method::Delete => + http_types::Method::DELETE, + Method::Head => + http_types::Method::HEAD, + Method::Options => + http_types::Method::OPTIONS, + Method::Connect => + http_types::Method::CONNECT, + Method::Patch => + http_types::Method::PATCH, + Method::Trace => + http_types::Method::TRACE, + Method::Extension(s) => { + HttpTryFrom::try_from(s.as_str()) + .expect("attempted to convert invalid method") + } + } + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -210,4 +275,25 @@ mod tests { assert_eq!(Put.as_ref(), "PUT"); assert_eq!(Extension("MOVE".to_owned()).as_ref(), "MOVE"); } + + #[test] + #[cfg(feature = "compat")] + fn test_compat() { + use http_types::{self, HttpTryFrom}; + + let methods = vec![ + "GET", + "POST", + "PUT", + "MOVE" + ]; + for method in methods { + let orig_hyper_method = Method::from_str(method).unwrap(); + let orig_http_method = http_types::Method::try_from(method).unwrap(); + let conv_hyper_method: Method = orig_http_method.clone().into(); + let conv_http_method: http_types::Method = orig_hyper_method.clone().into(); + assert_eq!(orig_hyper_method, conv_hyper_method); + assert_eq!(orig_http_method, conv_http_method); + } + } } diff --git a/src/server/compat.rs b/src/server/compat.rs new file mode 100644 index 0000000000..933e02fb9c --- /dev/null +++ b/src/server/compat.rs @@ -0,0 +1,7 @@ +//! Wrappers to build compatibility with the `http` crate. + +pub use super::compat_impl::{ + CompatFuture, + CompatService, + NewCompatService +}; diff --git a/src/server/compat_impl.rs b/src/server/compat_impl.rs new file mode 100644 index 0000000000..4bf596cd74 --- /dev/null +++ b/src/server/compat_impl.rs @@ -0,0 +1,82 @@ +use std::io::{Error as IoError}; + +use futures::{Future, Poll}; +use http_types; +use tokio_service::{NewService, Service}; + +use error::Error; +use http::Body; +use http::request::Request; +use http::response::Response; + +/// Wraps a `Future` returning an `http::Response` into +/// a `Future` returning a `hyper::server::Response`. +#[derive(Debug)] +pub struct CompatFuture { + future: F +} + +impl Future for CompatFuture + where F: Future, Error=Error> +{ + type Item = Response; + type Error = Error; + + fn poll(&mut self) -> Poll { + self.future.poll() + .map(|a| a.map(|res| res.into())) + } +} + +/// Wraps a `Service` taking an `http::Request` and returning +/// an `http::Response` into a `Service` taking a `hyper::server::Request`, +/// and returning a `hyper::server::Response`. +#[derive(Debug)] +pub struct CompatService { + service: S +} + +pub fn service(service: S) -> CompatService { + CompatService { service: service } +} + +impl Service for CompatService + where S: Service, Response=http_types::Response, Error=Error> +{ + type Request = Request; + type Response = Response; + type Error = Error; + type Future = CompatFuture; + + fn call(&self, req: Self::Request) -> Self::Future { + CompatFuture { + future: self.service.call(req.into()) + } + } +} + +/// Wraps a `NewService` taking an `http::Request` and returning +/// an `http::Response` into a `NewService` taking a `hyper::server::Request`, +/// and returning a `hyper::server::Response`. +#[derive(Debug)] +pub struct NewCompatService { + new_service: S +} + +pub fn new_service(new_service: S) -> NewCompatService { + NewCompatService { new_service: new_service } +} + +impl NewService for NewCompatService + where S: NewService, Response=http_types::Response, Error=Error> +{ + type Request = Request; + type Response = Response; + type Error = Error; + type Instance = CompatService; + + fn new_service(&self) -> Result { + self.new_service.new_service() + .map(service) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index d61ea21b1e..26d7c335b0 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -3,6 +3,11 @@ //! A `Server` is created to listen on a port, parse HTTP requests, and hand //! them off to a `Service`. +#[cfg(feature = "compat")] +mod compat_impl; +#[cfg(feature = "compat")] +pub mod compat; + use std::cell::RefCell; use std::fmt; use std::io; @@ -16,6 +21,9 @@ use futures::task::{self, Task}; use futures::{Future, Stream, Poll, Async, Sink, StartSend, AsyncSink}; use futures::future::Map; +#[cfg(feature = "compat")] +use http_types; + use tokio_io::{AsyncRead, AsyncWrite}; use tokio::reactor::{Core, Handle, Timeout}; use tokio::net::TcpListener; @@ -27,6 +35,8 @@ pub use tokio_service::{NewService, Service}; use http; use http::response; use http::request; +#[cfg(feature = "compat")] +use http::Body; pub use http::response::Response; pub use http::request::Request; @@ -103,6 +113,18 @@ impl + 'static> Http { }) } + /// Bind a `NewService` using types from the `http` crate. + /// + /// See `Http::bind`. + #[cfg(feature = "compat")] + pub fn bind_compat(&self, addr: &SocketAddr, new_service: S) -> ::Result, Bd>> + where S: NewService, Response = http_types::Response, Error = ::Error> + + Send + Sync + 'static, + Bd: Stream, + { + self.bind(addr, self::compat_impl::new_service(new_service)) + } + /// Use this `Http` instance to create a new server task which handles the /// connection `io` provided. /// @@ -131,6 +153,25 @@ impl + 'static> Http { remote_addr: remote_addr, }) } + + /// Bind a `Service` using types from the `http` crate. + /// + /// See `Http::bind_connection`. + #[cfg(feature = "compat")] + pub fn bind_connection_compat(&self, + handle: &Handle, + io: I, + remote_addr: SocketAddr, + service: S) + where S: Service, Response = http_types::Response, Error = ::Error> + 'static, + Bd: Stream + 'static, + I: AsyncRead + AsyncWrite + 'static, + { + self.bind_server(handle, io, HttpService { + inner: compat_impl::service(service), + remote_addr: remote_addr, + }) + } } impl Clone for Http { diff --git a/src/status.rs b/src/status.rs index b67985d146..4884613532 100644 --- a/src/status.rs +++ b/src/status.rs @@ -2,6 +2,9 @@ use std::fmt; use std::cmp::Ordering; +#[cfg(feature = "compat")] +use http_types; + /// An HTTP status code (`status-code` in RFC 7230 et al.). /// /// This enum contains all common status codes and an Unregistered @@ -596,6 +599,22 @@ impl From for u16 { } } +#[cfg(feature = "compat")] +impl From for StatusCode { + fn from(status: http_types::StatusCode) -> StatusCode { + StatusCode::try_from(status.as_u16()) + .expect("attempted to convert invalid status code") + } +} + +#[cfg(feature = "compat")] +impl From for http_types::StatusCode { + fn from(status: StatusCode) -> http_types::StatusCode { + http_types::StatusCode::from_u16(status.as_u16()) + .expect("attempted to convert invalid status code") + } +} + /// The class of an HTTP `status-code`. /// /// [RFC 7231, section 6 (Response Status Codes)](https://tools.ietf.org/html/rfc7231#section-6): @@ -746,4 +765,18 @@ mod tests { StatusCode::try_from(0).unwrap_err(); StatusCode::try_from(1000).unwrap_err(); } + + #[test] + #[cfg(feature = "compat")] + fn test_compat() { + use http_types::{self, HttpTryFrom}; + for i in 100..600 { + let orig_hyper_status = StatusCode::try_from(i).unwrap(); + let orig_http_status = http_types::StatusCode::try_from(i).unwrap(); + let conv_hyper_status: StatusCode = orig_http_status.into(); + let conv_http_status: http_types::StatusCode = orig_hyper_status.into(); + assert_eq!(orig_hyper_status, conv_hyper_status); + assert_eq!(orig_http_status, conv_http_status); + } + } } diff --git a/src/uri.rs b/src/uri.rs index 5c87a07555..4e710ec4c3 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -2,6 +2,9 @@ use std::error::Error as StdError; use std::fmt::{Display, self}; use std::str::{self, FromStr}; +#[cfg(feature = "compat")] +use http_types; + use ::common::ByteStr; use bytes::{BufMut, Bytes, BytesMut}; @@ -315,6 +318,23 @@ impl Display for Uri { } } +#[cfg(feature = "compat")] +impl From for Uri { + fn from(uri: http_types::Uri) -> Uri { + uri.to_string().parse() + .expect("attempted to convert invalid uri") + } +} + +#[cfg(feature = "compat")] +impl From for http_types::Uri { + fn from(uri: Uri) -> http_types::Uri { + let bytes = uri.source.into_bytes(); + http_types::Uri::from_shared(bytes) + .expect("attempted to convert invalid uri") + } +} + pub unsafe fn from_utf8_unchecked(slice: Bytes) -> Result { Uri::new(ByteStr::from_utf8_unchecked(slice)) } diff --git a/src/version.rs b/src/version.rs index 1abe1c7352..4d303ad34f 100644 --- a/src/version.rs +++ b/src/version.rs @@ -5,6 +5,9 @@ use std::fmt; use std::str::FromStr; +#[cfg(feature = "compat")] +use http_types; + use error::Error; use self::HttpVersion::{Http09, Http10, Http11, H2, H2c}; @@ -58,6 +61,39 @@ impl Default for HttpVersion { } } +#[cfg(feature = "compat")] +impl From for HttpVersion { + fn from(v: http_types::Version) -> HttpVersion { + match v { + http_types::Version::HTTP_09 => + HttpVersion::Http09, + http_types::Version::HTTP_10 => + HttpVersion::Http10, + http_types::Version::HTTP_11 => + HttpVersion::Http11, + http_types::Version::HTTP_2 => + HttpVersion::H2 + } + } +} + +#[cfg(feature = "compat")] +impl From for http_types::Version { + fn from(v: HttpVersion) -> http_types::Version { + match v { + HttpVersion::Http09 => + http_types::Version::HTTP_09, + HttpVersion::Http10 => + http_types::Version::HTTP_10, + HttpVersion::Http11 => + http_types::Version::HTTP_11, + HttpVersion::H2 => + http_types::Version::HTTP_2, + _ => panic!("attempted to convert unexpected http version") + } + } +} + #[cfg(test)] mod tests { use std::str::FromStr;