hyper_stub/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5//! hyper-stub provides functions to create [hyper] clients that convert requests
6//! to responses using predefined functions, without doing any actual
7//! networking. This means the entire request/response lifecycle happens in a
8//! single process, and should have performance and stability improvements over,
9//! for example, binding to a port. One potential use case for this is stubbing
10//! HTTP interactions in tests to avoid slow/flakey/absent internet connections.
11//!
12//! The simplest case uses [`proxy_client_fn_ok`] to create a client bound to a
13//! simple function that directly maps from a request to a response:
14//!
15//! ```
16//! # extern crate futures;
17//! # extern crate hyper;
18//! # extern crate hyper_stub;
19//! # extern crate tokio;
20//! #
21//! use futures::{Future, Stream};
22//! use hyper::{Request, Response, Uri};
23//! use hyper_stub::proxy_client_fn_ok;
24//! use tokio::runtime::current_thread::Runtime;
25//!
26//! let echo_client = proxy_client_fn_ok(|request| {
27//!     let body = request.into_body();
28//!     Response::new(body)
29//! });
30//!
31//! let url: Uri = "http://example.com".parse().unwrap();
32//! let mut builder = Request::post(url);
33//! let request = builder.body("hello world".into()).unwrap();
34//! let future = echo_client.request(request)
35//!     .and_then(|res| res.into_body().concat2())
36//!     .map(|bytes| {
37//!         let body = String::from_utf8(bytes.to_vec()).unwrap();
38//!         println!("{}", body);
39//!     })
40//!     .map_err(|error| panic!("ERROR: {:?}", error));
41//!
42//! Runtime::new().unwrap().block_on(future).unwrap();
43//! ```
44//!
45//! If the function needs to return an error, or respond to the request
46//! asynchronously, [`proxy_client_fn`] can be used.
47//!
48//! Finally, an advanced use case is using hyper [`services`] instead of simple
49//! functions. This can be done with the [`proxy_client`] function.
50//!
51//! [hyper]: https://hyper.rs
52//! [services]: https://docs.rs/hyper/0.12.1/hyper/service/index.html
53//! [`proxy_client_fn_ok`]: fn.proxy_client_fn_ok.html
54//! [`proxy_client_fn`]: fn.proxy_client_fn.html
55//! [`proxy_client`]: fn.proxy_client.html
56
57extern crate futures;
58extern crate hyper;
59extern crate memsocket;
60extern crate tokio;
61
62mod connector;
63mod never;
64
65use connector::Connector;
66use futures::prelude::*;
67use hyper::body::{Body, Payload};
68use hyper::client::connect::Connect;
69use hyper::service::{NewService, Service};
70use hyper::{Client, Request, Response};
71use never::Never;
72use std::error::Error;
73
74/// Creates a hyper client whose requests are converted to responses by being
75/// passed through a hyper [`Service`] instantiated by and returned from the given
76/// [`NewService`].
77///
78/// [`proxy_client_fn`] is much more simple and almost as powerful, so should
79/// generally be preferred.
80///
81/// [`Service`]: https://docs.rs/hyper/0.12.1/hyper/service/index.html
82/// [`NewService`]: https://docs.rs/hyper/0.12.1/hyper/service/trait.NewService.html
83/// [`proxy_client_fn`]: fn.proxy_client_fn.html
84pub fn proxy_client<ResBody, ResponseError, ServiceError, ResponseFuture, ServiceFuture, S, N>(
85    new_service: N,
86) -> Client<Connector<N>>
87where
88    ResBody: Payload,
89    ResponseError: Error + Send + Sync + 'static,
90    ServiceError: Error + Send + Sync + 'static,
91    ResponseFuture: Future<Item = Response<S::ResBody>, Error = ResponseError> + Send + 'static,
92    ServiceFuture: Future<Item = S, Error = ServiceError> + Send + 'static,
93    S: Service<ReqBody = Body, ResBody = ResBody, Error = ResponseError, Future = ResponseFuture>
94        + Send
95        + 'static,
96    N: NewService<
97            ReqBody = S::ReqBody,
98            ResBody = S::ResBody,
99            Future = ServiceFuture,
100            Error = ResponseError,
101            Service = S,
102            InitError = ServiceError,
103        >
104        + Sync
105        + Send,
106{
107    Client::builder()
108        .set_host(true)
109        .build(Connector::new(new_service))
110}
111
112/// Creates a hyper client whose requests are converted to responses by being
113/// passed through the given handler function, which returns a future.
114pub fn proxy_client_fn<E, Fut, F>(handler: F) -> Client<impl Connect>
115where
116    E: Error + Send + Sync + 'static,
117    Fut: Future<Item = Response<Body>, Error = E> + Send + 'static,
118    F: Fn(Request<Body>) -> Fut + Send + Sync + Copy + 'static,
119{
120    use futures::future;
121    use hyper::service::service_fn;
122
123    proxy_client(move || future::ok::<_, Never>(service_fn(handler)))
124}
125
126/// Creates a hyper client whose requests are converted to responses by being
127/// passed through the given handler function.
128///
129/// See [`proxy_client_fn`] if errors or asynchronous processing are required.
130///
131/// [`proxy_client_fn`]: fn.proxy_client_fn.html
132pub fn proxy_client_fn_ok<F>(handler: F) -> Client<impl Connect>
133where
134    F: Fn(Request<Body>) -> Response<Body> + Send + Sync + Copy + 'static,
135{
136    use futures::future;
137
138    proxy_client_fn(move |req| future::ok::<_, Never>(handler(req)))
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_ok() {
147        use futures::prelude::*;
148        use tokio::runtime::current_thread::Runtime;
149
150        let client = proxy_client_fn_ok(|req| {
151            let query = req.uri().query().unwrap().to_string();
152            Response::new(query.into())
153        });
154
155        Runtime::new()
156            .unwrap()
157            .block_on({
158                client
159                    .get("https://example.com?foo=bar".parse().unwrap())
160                    .and_then(|res| res.into_body().concat2())
161                    .map(|bytes| {
162                        let body = String::from_utf8(bytes.to_vec()).unwrap();
163                        assert_eq!(body, "foo=bar");
164                    })
165                    .map_err(|err| panic!("{:?}", err))
166            })
167            .unwrap();
168    }
169
170    #[test]
171    fn test_err() {
172        use futures::future::{self, FutureResult};
173        use futures::prelude::*;
174        use std::fmt::{self, Display, Formatter};
175        use tokio::runtime::current_thread::Runtime;
176
177        #[derive(Debug)]
178        struct NewServiceError;
179
180        impl Display for NewServiceError {
181            fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
182                write!(fmt, "correct error for test")
183            }
184        }
185
186        impl Error for NewServiceError {
187            fn description(&self) -> &str {
188                "It broke"
189            }
190        }
191
192        impl Service for Never {
193            type ReqBody = Body;
194            type ResBody = Body;
195            type Error = Self;
196            type Future = FutureResult<Response<Self::ResBody>, Self>;
197
198            fn call(&mut self, _: Request<Self::ReqBody>) -> Self::Future {
199                unreachable!()
200            }
201        }
202
203        let client = proxy_client(|| future::err::<Never, _>(NewServiceError));
204
205        let _ = Runtime::new().unwrap().block_on({
206            client
207                .get("https://example.com".parse().unwrap())
208                .map(|res| panic!("didn't error: {:?}", res))
209                .map_err(|err| assert!(err.to_string().contains("correct error for test")))
210        });
211    }
212}