Compare commits
6 Commits
e522e69578
...
6c7daac551
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c7daac551 | |||
| cf0c820da3 | |||
| 269d405bb5 | |||
| 1922c99979 | |||
| 1269db042b | |||
| b1be2fdf6f |
@ -7,17 +7,18 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
hyper = {version = "=0.13.0-alpha.4", features = ["unstable-stream"]}
|
||||
futures-preview = {version = "=0.3.0-alpha.19", features = ["std", "async-await"]}
|
||||
tokio = "=0.2.0-alpha.6"
|
||||
hyper = "0.13"
|
||||
futures = "0.3"
|
||||
tokio = { version = "0.2", features = ["macros"] }
|
||||
crossbeam-channel = "0.4.0"
|
||||
http = "0.1.18"
|
||||
http = "0.2"
|
||||
log = "0.4.8"
|
||||
bstr = "0.2.8"
|
||||
regex = "1.3.1"
|
||||
url = "2.1.0"
|
||||
serde_json = "1.0.44"
|
||||
serde = "1.0.103"
|
||||
serde_urlencoded = "0.6.1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_env_logger = "0.3.1"
|
||||
|
||||
47
src/lib.rs
47
src/lib.rs
@ -41,18 +41,19 @@
|
||||
//!
|
||||
//! # Server behavior
|
||||
//!
|
||||
//! The Server is started with [Server::run()](struct.Server.html#method.run).
|
||||
//! The Server is started with [run()](struct.Server.html#method.run).
|
||||
//!
|
||||
//! The server will run in a background thread until it's dropped. Once dropped
|
||||
//! it will assert that every configured expectation has been met or will panic.
|
||||
//! You can also use [server.verify_and_clear()](struct.Server.html#method.verify_and_clear)
|
||||
//! You can also use [verify_and_clear()](struct.Server.html#method.verify_and_clear)
|
||||
//! to assert and clear the expectations while keeping the server running.
|
||||
//!
|
||||
//! [server.addr()](struct.Server.html#method.addr) will return the address the
|
||||
//! [addr()](struct.Server.html#method.addr) will return the address the
|
||||
//! server is listening on.
|
||||
//!
|
||||
//! [server.url()](struct.Server.html#method.url) will
|
||||
//! [url()](struct.Server.html#method.url) will
|
||||
//! construct a fully formed http url to the path provided i.e.
|
||||
//!
|
||||
//! `server.url("/foo?key=value") == "https://<server_addr>/foo?key=value"`.
|
||||
//!
|
||||
//! # Defining Expecations
|
||||
@ -127,18 +128,22 @@
|
||||
//!
|
||||
//! ## Times
|
||||
//!
|
||||
//! Each expectation defines how many times a matching requests is expected to
|
||||
//! Each expectation defines how many times a matching request is expected to
|
||||
//! be received. The [Times](enum.Times.html) enum defines the possibility.
|
||||
//! `Times::Exactly(1)` is the default value of an `Expectation` if one is not
|
||||
//! specified with the
|
||||
//! [Expectation.times()](struct.Expectation.html#method.times) method.
|
||||
//! [times()](struct.ExpectationBuilder.html#method.times) method.
|
||||
//!
|
||||
//! The server will respond to any requests that violate the times request with
|
||||
//! a 500 status code and the server will subsequently panic on Drop.
|
||||
//!
|
||||
//! ## Responder
|
||||
//!
|
||||
//! responders define how the server will respond to a matched request. There
|
||||
//! Responders define how the server will respond to a matched request. There
|
||||
//! are a number of implemented responders within the responders module. In
|
||||
//! addition to the predefined responders you can provide any
|
||||
//! hyper::Response<Vec<u8>> or obviously implement your own Responder.
|
||||
//! `hyper::Response` with a body that can be cloned or implement your own
|
||||
//! Responder.
|
||||
//!
|
||||
//! ## Responder example
|
||||
//!
|
||||
@ -165,26 +170,38 @@
|
||||
//!
|
||||
//! ```
|
||||
|
||||
//#![deny(missing_docs)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
// hidden from docs here because it's re-rexported from the mappers module.
|
||||
#[doc(hidden)]
|
||||
/// true if all the provided matchers return true.
|
||||
///
|
||||
/// The macro exists to conveniently box a list of mappers and put them into a
|
||||
/// `Vec<Box<dyn Mapper>>`. The translation is:
|
||||
///
|
||||
/// `all_of![a, b] => all_of(vec![Box::new(a), Box::new(b)])`
|
||||
#[macro_export]
|
||||
macro_rules! all_of {
|
||||
($($x:expr),*) => ($crate::mappers::all_of($crate::vec_of_boxes![$($x),*]));
|
||||
($($x:expr,)*) => ($crate::all_of![$($x),*]);
|
||||
}
|
||||
|
||||
// hidden from docs here because it's re-rexported from the mappers module.
|
||||
#[doc(hidden)]
|
||||
/// true if any of the provided matchers return true.
|
||||
///
|
||||
/// The macro exists to conveniently box a list of mappers and put them into a
|
||||
/// `Vec<Box<dyn Mapper>>`. The translation is:
|
||||
///
|
||||
/// `any_of![a, b] => any_of(vec![Box::new(a), Box::new(b)])`
|
||||
#[macro_export]
|
||||
macro_rules! any_of {
|
||||
($($x:expr),*) => ($crate::mappers::any_of($crate::vec_of_boxes![$($x),*]));
|
||||
($($x:expr,)*) => ($crate::any_of![$($x),*]);
|
||||
}
|
||||
|
||||
// hidden from docs here because it's re-rexported from the responders module.
|
||||
#[doc(hidden)]
|
||||
/// a Responder that cycles through a list of responses.
|
||||
///
|
||||
/// The macro exists to conveniently box a list of responders and put them into a
|
||||
/// `Vec<Box<dyn Responder>>`. The translation is:
|
||||
///
|
||||
/// `cycle![a, b] => cycle(vec![Box::new(a), Box::new(b)])`
|
||||
#[macro_export]
|
||||
macro_rules! cycle {
|
||||
($($x:expr),*) => ($crate::responders::cycle($crate::vec_of_boxes![$($x),*]));
|
||||
|
||||
189
src/mappers.rs
189
src/mappers.rs
@ -1,29 +1,52 @@
|
||||
//! Mapper implementations.
|
||||
//!
|
||||
//! This module contains mappers for composing a set of operations. The result
|
||||
//! of the composition usually results in a boolean. Any `Mapper` that results in a
|
||||
//! boolean value also implemens `Matcher`.
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::fmt;
|
||||
|
||||
// import the any_of and all_of macros from crate root so they are accessible if
|
||||
// people glob import this module.
|
||||
#[doc(inline)]
|
||||
pub use crate::all_of;
|
||||
#[doc(inline)]
|
||||
pub use crate::any_of;
|
||||
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod sequence;
|
||||
|
||||
/// The core trait. Defines how an input value should be turned into an output
|
||||
/// value. This allows for a flexible pattern of composition where two or more
|
||||
/// mappers are chained together to form a readable and flexible manipulation.
|
||||
///
|
||||
/// There is a special case of a Mapper that outputs a bool that is called a
|
||||
/// Matcher.
|
||||
pub trait Mapper<IN>: Send + fmt::Debug
|
||||
where
|
||||
IN: ?Sized,
|
||||
{
|
||||
/// The output type.
|
||||
type Out;
|
||||
|
||||
/// Map an input to output.
|
||||
fn map(&mut self, input: &IN) -> Self::Out;
|
||||
}
|
||||
|
||||
// Matcher is just a special case of Mapper that returns a boolean. Simply
|
||||
// provides the `matches` method rather than `map` as that reads a little
|
||||
// better.
|
||||
/// Matcher is just a special case of Mapper that returns a boolean. It simply
|
||||
/// provides the `matches` method rather than `map` as that reads a little
|
||||
/// better.
|
||||
///
|
||||
/// There is a blanket implementation for all Mappers that output bool values.
|
||||
/// You should never implement Matcher yourself, instead implement Mapper with a
|
||||
/// bool Out parameter.
|
||||
pub trait Matcher<IN>: Send + fmt::Debug
|
||||
where
|
||||
IN: ?Sized,
|
||||
{
|
||||
/// true if the input matches.
|
||||
fn matches(&mut self, input: &IN) -> bool;
|
||||
}
|
||||
impl<T, IN> Matcher<IN> for T
|
||||
@ -35,12 +58,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Always true.
|
||||
pub fn any() -> Any {
|
||||
Any
|
||||
}
|
||||
/// The `Any` mapper returned by [any()](fn.any.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Any;
|
||||
impl<IN> Mapper<IN> for Any {
|
||||
impl<IN> Mapper<IN> for Any
|
||||
where
|
||||
IN: ?Sized,
|
||||
{
|
||||
type Out = bool;
|
||||
|
||||
fn map(&mut self, _input: &IN) -> bool {
|
||||
@ -48,9 +76,11 @@ impl<IN> Mapper<IN> for Any {
|
||||
}
|
||||
}
|
||||
|
||||
/// true if the input is equal to value.
|
||||
pub fn eq<T>(value: T) -> Eq<T> {
|
||||
Eq(value)
|
||||
}
|
||||
/// The `Eq` mapper returned by [eq()](fn.eq.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Eq<T>(T);
|
||||
impl<IN, T> Mapper<IN> for Eq<T>
|
||||
@ -65,10 +95,59 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(value: &str) -> Matches {
|
||||
let regex = regex::bytes::Regex::new(value).expect("failed to create regex");
|
||||
Matches(regex)
|
||||
/// Call Deref::deref() on the input and pass it to the next mapper.
|
||||
pub fn deref<C>(inner: C) -> Deref<C> {
|
||||
Deref(inner)
|
||||
}
|
||||
/// The `Deref` mapper returned by [deref()](fn.deref.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Deref<C>(C);
|
||||
impl<C, IN> Mapper<IN> for Deref<C>
|
||||
where
|
||||
C: Mapper<IN::Target>,
|
||||
IN: std::ops::Deref,
|
||||
{
|
||||
type Out = C::Out;
|
||||
|
||||
fn map(&mut self, input: &IN) -> C::Out {
|
||||
self.0.map(input.deref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a regex.
|
||||
///
|
||||
/// This trait may panic if the regex failed to build.
|
||||
pub trait IntoRegex {
|
||||
/// turn self into a regex.
|
||||
fn into_regex(self) -> regex::bytes::Regex;
|
||||
}
|
||||
impl IntoRegex for &str {
|
||||
fn into_regex(self) -> regex::bytes::Regex {
|
||||
regex::bytes::Regex::new(self).expect("failed to create regex")
|
||||
}
|
||||
}
|
||||
impl IntoRegex for String {
|
||||
fn into_regex(self) -> regex::bytes::Regex {
|
||||
regex::bytes::Regex::new(&self).expect("failed to create regex")
|
||||
}
|
||||
}
|
||||
impl IntoRegex for &mut regex::bytes::RegexBuilder {
|
||||
fn into_regex(self) -> regex::bytes::Regex {
|
||||
self.build().expect("failed to create regex")
|
||||
}
|
||||
}
|
||||
impl IntoRegex for regex::bytes::Regex {
|
||||
fn into_regex(self) -> regex::bytes::Regex {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// true if the input matches the regex provided.
|
||||
pub fn matches(value: impl IntoRegex) -> Matches {
|
||||
//let regex = regex::bytes::Regex::new(value).expect("failed to create regex");
|
||||
Matches(value.into_regex())
|
||||
}
|
||||
/// The `Matches` mapper returned by [matches()](fn.matches.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Matches(regex::bytes::Regex);
|
||||
impl<IN> Mapper<IN> for Matches
|
||||
@ -82,9 +161,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// invert the result of the inner mapper.
|
||||
pub fn not<C>(inner: C) -> Not<C> {
|
||||
Not(inner)
|
||||
}
|
||||
/// The `Not` mapper returned by [not()](fn.not.html)
|
||||
pub struct Not<C>(C);
|
||||
impl<C, IN> Mapper<IN> for Not<C>
|
||||
where
|
||||
@ -106,6 +187,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// true if all the provided matchers return true. See the `all_of!` macro for
|
||||
/// convenient usage.
|
||||
pub fn all_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> AllOf<IN>
|
||||
where
|
||||
IN: fmt::Debug + ?Sized,
|
||||
@ -113,6 +196,7 @@ where
|
||||
AllOf(inner)
|
||||
}
|
||||
|
||||
/// The `AllOf` mapper returned by [all_of()](fn.all_of.html)
|
||||
#[derive(Debug)]
|
||||
pub struct AllOf<IN>(Vec<Box<dyn Mapper<IN, Out = bool>>>)
|
||||
where
|
||||
@ -128,12 +212,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// true if any of the provided matchers returns true. See the `any_of!` macro
|
||||
/// for convenient usage.
|
||||
pub fn any_of<IN>(inner: Vec<Box<dyn Mapper<IN, Out = bool>>>) -> AnyOf<IN>
|
||||
where
|
||||
IN: fmt::Debug + ?Sized,
|
||||
{
|
||||
AnyOf(inner)
|
||||
}
|
||||
/// The `AnyOf` mapper returned by [any_of()](fn.any_of.html)
|
||||
#[derive(Debug)]
|
||||
pub struct AnyOf<IN>(Vec<Box<dyn Mapper<IN, Out = bool>>>)
|
||||
where
|
||||
@ -149,15 +236,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uri_decoded<C>(inner: C) -> UriDecoded<C>
|
||||
where
|
||||
C: Mapper<[(String, String)]>,
|
||||
{
|
||||
UriDecoded(inner)
|
||||
/// url decode the input and pass the resulting slice of key-value pairs to the next mapper.
|
||||
pub fn url_decoded<C>(inner: C) -> UrlDecoded<C> {
|
||||
UrlDecoded(inner)
|
||||
}
|
||||
/// The `UrlDecoded` mapper returned by [url_decoded()](fn.url_decoded.html)
|
||||
#[derive(Debug)]
|
||||
pub struct UriDecoded<C>(C);
|
||||
impl<IN, C> Mapper<IN> for UriDecoded<C>
|
||||
pub struct UrlDecoded<C>(C);
|
||||
impl<IN, C> Mapper<IN> for UrlDecoded<C>
|
||||
where
|
||||
IN: AsRef<[u8]> + ?Sized,
|
||||
C: Mapper<[(String, String)]>,
|
||||
@ -172,12 +258,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_decoded<C>(inner: C) -> JsonDecoded<C>
|
||||
where
|
||||
C: Mapper<serde_json::Value>,
|
||||
{
|
||||
/// json decode the input and pass the resulting serde_json::Value to the next
|
||||
/// mapper.
|
||||
///
|
||||
/// If the input can't be decoded a serde_json::Value::Null is passed to the next
|
||||
/// mapper.
|
||||
pub fn json_decoded<C>(inner: C) -> JsonDecoded<C> {
|
||||
JsonDecoded(inner)
|
||||
}
|
||||
/// The `JsonDecoded` mapper returned by [json_decoded()](fn.json_decoded.html)
|
||||
#[derive(Debug)]
|
||||
pub struct JsonDecoded<C>(C);
|
||||
impl<IN, C> Mapper<IN> for JsonDecoded<C>
|
||||
@ -194,12 +283,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// lowercase the input and pass it to the next mapper.
|
||||
pub fn lowercase<C>(inner: C) -> Lowercase<C>
|
||||
where
|
||||
C: Mapper<[u8]>,
|
||||
{
|
||||
Lowercase(inner)
|
||||
}
|
||||
/// The `Lowercase` mapper returned by [lowercase()](fn.lowercase.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Lowercase<C>(C);
|
||||
impl<IN, C> Mapper<IN> for Lowercase<C>
|
||||
@ -215,6 +306,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// pass the input to the provided `Fn(T) -> bool` and return the result.
|
||||
pub fn map_fn<F>(f: F) -> MapFn<F> {
|
||||
MapFn(f)
|
||||
}
|
||||
/// The `MapFn` mapper returned by [map_fn()](fn.map_fn.html)
|
||||
pub struct MapFn<F>(F);
|
||||
impl<IN, F> Mapper<IN> for MapFn<F>
|
||||
where
|
||||
F: Fn(&IN) -> bool + Send,
|
||||
{
|
||||
type Out = bool;
|
||||
|
||||
fn map(&mut self, input: &IN) -> bool {
|
||||
self.0(input)
|
||||
}
|
||||
}
|
||||
impl<F> fmt::Debug for MapFn<F> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "MapFn")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -230,11 +343,36 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_matches() {
|
||||
// regex from str
|
||||
let mut c = matches(r#"^foo\d*bar$"#);
|
||||
assert_eq!(true, c.map("foobar"));
|
||||
assert_eq!(true, c.map("foo99bar"));
|
||||
assert_eq!(false, c.map("foo99barz"));
|
||||
assert_eq!(false, c.map("bat"));
|
||||
|
||||
// regex from String
|
||||
let mut c = matches(r#"^foo\d*bar$"#.to_owned());
|
||||
assert_eq!(true, c.map("foobar"));
|
||||
assert_eq!(true, c.map("foo99bar"));
|
||||
assert_eq!(false, c.map("foo99barz"));
|
||||
assert_eq!(false, c.map("bat"));
|
||||
|
||||
// regex from RegexBuilder
|
||||
let mut c = matches(regex::bytes::RegexBuilder::new("foobar").case_insensitive(true));
|
||||
assert_eq!(true, c.map("foobar"));
|
||||
assert_eq!(true, c.map("FOOBAR"));
|
||||
assert_eq!(false, c.map("FOO99BAR"));
|
||||
|
||||
// regex from Regex
|
||||
let mut c = matches(
|
||||
regex::bytes::RegexBuilder::new("foobar")
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(true, c.map("foobar"));
|
||||
assert_eq!(true, c.map("FOOBAR"));
|
||||
assert_eq!(false, c.map("FOO99BAR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -266,13 +404,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uri_decoded() {
|
||||
fn test_url_decoded() {
|
||||
let expected = vec![
|
||||
("key 1".to_owned(), "value 1".to_owned()),
|
||||
("key2".to_owned(), "".to_owned()),
|
||||
];
|
||||
let mut c = request::query(uri_decoded(eq(expected)));
|
||||
let req = http::Request::get("https://example.com/path?key%201=value%201&key2")
|
||||
let mut c = request::query(url_decoded(eq(expected)));
|
||||
let req = hyper::Request::get("https://example.com/path?key%201=value%201&key2")
|
||||
.body("")
|
||||
.unwrap();
|
||||
|
||||
@ -298,4 +436,13 @@ mod tests {
|
||||
assert_eq!(true, c.map("foobar"));
|
||||
assert_eq!(false, c.map("bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_mapper() {
|
||||
let mut c = map_fn(|input: &u64| input % 2 == 0);
|
||||
assert_eq!(true, c.map(&6));
|
||||
assert_eq!(true, c.map(&20));
|
||||
assert_eq!(true, c.map(&0));
|
||||
assert_eq!(false, c.map(&11));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
//! Mappers that extract information from HTTP requests.
|
||||
|
||||
use super::Mapper;
|
||||
|
||||
pub fn method<C>(inner: C) -> Method<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
/// Extract the method from the HTTP request and pass it to the next mapper.
|
||||
pub fn method<C>(inner: C) -> Method<C> {
|
||||
Method(inner)
|
||||
}
|
||||
/// The `Method` mapper returned by [method()](fn.method.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Method<C>(C);
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Method<C>
|
||||
@ -19,12 +20,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path<C>(inner: C) -> Path<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
/// Extract the path from the HTTP request and pass it to the next mapper.
|
||||
pub fn path<C>(inner: C) -> Path<C> {
|
||||
Path(inner)
|
||||
}
|
||||
/// The `Path` mapper returned by [path()](fn.path.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Path<C>(C);
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Path<C>
|
||||
@ -38,12 +38,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query<C>(inner: C) -> Query<C>
|
||||
where
|
||||
C: Mapper<str>,
|
||||
{
|
||||
/// Extract the query from the HTTP request and pass it to the next mapper.
|
||||
pub fn query<C>(inner: C) -> Query<C> {
|
||||
Query(inner)
|
||||
}
|
||||
/// The `Query` mapper returned by [query()](fn.query.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Query<C>(C);
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Query<C>
|
||||
@ -57,12 +56,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headers<C>(inner: C) -> Headers<C>
|
||||
where
|
||||
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
|
||||
{
|
||||
/// Extract the headers from the HTTP request and pass the sequence to the next
|
||||
/// mapper.
|
||||
pub fn headers<C>(inner: C) -> Headers<C> {
|
||||
Headers(inner)
|
||||
}
|
||||
/// The `Headers` mapper returned by [headers()](fn.headers.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Headers<C>(C);
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Headers<C>
|
||||
@ -81,19 +80,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the body from the HTTP request and pass it to the next mapper.
|
||||
pub fn body<C>(inner: C) -> Body<C> {
|
||||
Body(inner)
|
||||
}
|
||||
/// The `Body` mapper returned by [body()](fn.body.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Body<C>(C);
|
||||
impl<C, B> Mapper<hyper::Request<B>> for Body<C>
|
||||
where
|
||||
C: Mapper<B>,
|
||||
B: ToOwned,
|
||||
C: Mapper<B::Owned>,
|
||||
{
|
||||
type Out = C::Out;
|
||||
|
||||
fn map(&mut self, input: &hyper::Request<B>) -> C::Out {
|
||||
self.0.map(input.body())
|
||||
self.0.map(&input.body().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
//! Mappers that extract information from HTTP responses.
|
||||
|
||||
use super::Mapper;
|
||||
|
||||
pub fn status_code<C>(inner: C) -> StatusCode<C>
|
||||
where
|
||||
C: Mapper<u16>,
|
||||
{
|
||||
/// Extract the status code from the HTTP response and pass it to the next mapper.
|
||||
pub fn status_code<C>(inner: C) -> StatusCode<C> {
|
||||
StatusCode(inner)
|
||||
}
|
||||
/// The `StatusCode` mapper returned by [status_code()](fn.status_code.html)
|
||||
#[derive(Debug)]
|
||||
pub struct StatusCode<C>(C);
|
||||
impl<C, B> Mapper<hyper::Response<B>> for StatusCode<C>
|
||||
@ -19,12 +20,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headers<C>(inner: C) -> Headers<C>
|
||||
where
|
||||
C: Mapper<[(Vec<u8>, Vec<u8>)]>,
|
||||
{
|
||||
/// Extract the headers from the HTTP response and pass the sequence to the next
|
||||
/// mapper.
|
||||
pub fn headers<C>(inner: C) -> Headers<C> {
|
||||
Headers(inner)
|
||||
}
|
||||
/// The `Headers` mapper returned by [headers()](fn.headers.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Headers<C>(C);
|
||||
impl<C, B> Mapper<hyper::Response<B>> for Headers<C>
|
||||
@ -43,9 +44,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the body from the HTTP response and pass it to the next mapper.
|
||||
pub fn body<C>(inner: C) -> Body<C> {
|
||||
Body(inner)
|
||||
}
|
||||
/// The `Body` mapper returned by [body()](fn.body.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Body<C>(C);
|
||||
impl<C, B> Mapper<hyper::Response<B>> for Body<C>
|
||||
|
||||
61
src/mappers/sequence.rs
Normal file
61
src/mappers/sequence.rs
Normal file
@ -0,0 +1,61 @@
|
||||
//! Mappers that handle sequences of items.
|
||||
|
||||
use super::Mapper;
|
||||
|
||||
/// true if the provided mapper returns true for any of the elements in the
|
||||
/// sequence.
|
||||
pub fn contains<C>(inner: C) -> Contains<C> {
|
||||
Contains(inner)
|
||||
}
|
||||
/// The `Contains` mapper returned by [contains()](fn.contains.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Contains<C>(C);
|
||||
impl<C, E> Mapper<[E]> for Contains<C>
|
||||
where
|
||||
C: Mapper<E, Out = bool>,
|
||||
{
|
||||
type Out = bool;
|
||||
|
||||
fn map(&mut self, input: &[E]) -> bool {
|
||||
for elem in input {
|
||||
if self.0.map(elem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, KMapper, VMapper> Mapper<(K, V)> for (KMapper, VMapper)
|
||||
where
|
||||
KMapper: Mapper<K, Out = bool>,
|
||||
VMapper: Mapper<V, Out = bool>,
|
||||
{
|
||||
type Out = bool;
|
||||
|
||||
fn map(&mut self, input: &(K, V)) -> bool {
|
||||
self.0.map(&input.0) && self.1.map(&input.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mappers::*;
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
let mut c = contains(eq(100));
|
||||
assert_eq!(true, c.map(vec![100, 200, 300].as_slice()));
|
||||
assert_eq!(false, c.map(vec![99, 200, 300].as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple() {
|
||||
let kv = ("key1", "value1");
|
||||
assert_eq!(true, (matches("key1"), any()).map(&kv));
|
||||
assert_eq!(true, (matches("key1"), matches("value1")).map(&kv));
|
||||
assert_eq!(false, (matches("key1"), matches("value2")).map(&kv));
|
||||
assert_eq!(false, (matches("key2"), matches("value1")).map(&kv));
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,31 @@
|
||||
//! Responder implementations.
|
||||
//!
|
||||
//! Reponders determine how the server will respond.
|
||||
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
// import the cycle macro so that it's available if people glob import this module.
|
||||
#[doc(inline)]
|
||||
pub use crate::cycle;
|
||||
|
||||
/// Respond with an HTTP response.
|
||||
pub trait Responder: Send + fmt::Debug {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>>;
|
||||
/// Return a future that outputs an HTTP response.
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>>;
|
||||
}
|
||||
|
||||
/// respond with the provided status code.
|
||||
pub fn status_code(code: u16) -> impl Responder {
|
||||
StatusCode(code)
|
||||
}
|
||||
/// The `StatusCode` responder returned by [status_code()](fn.status_code.html)
|
||||
#[derive(Debug)]
|
||||
pub struct StatusCode(u16);
|
||||
impl Responder for StatusCode {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
|
||||
async fn _respond(status_code: u16) -> http::Response<hyper::Body> {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
|
||||
async fn _respond(status_code: u16) -> hyper::Response<hyper::Body> {
|
||||
hyper::Response::builder()
|
||||
.status(status_code)
|
||||
.body(hyper::Body::empty())
|
||||
@ -25,17 +35,22 @@ impl Responder for StatusCode {
|
||||
}
|
||||
}
|
||||
|
||||
/// respond with a body that is the json encoding of data.
|
||||
///
|
||||
/// The status code will be `200` and the content-type will be
|
||||
/// `application/json`.
|
||||
pub fn json_encoded<T>(data: T) -> impl Responder
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
JsonEncoded(serde_json::to_string(&data).unwrap())
|
||||
}
|
||||
/// The `JsonEncoded` responder returned by [json_encoded()](fn.json_encoded.html)
|
||||
#[derive(Debug)]
|
||||
pub struct JsonEncoded(String);
|
||||
impl Responder for JsonEncoded {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
|
||||
async fn _respond(body: String) -> http::Response<hyper::Body> {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
|
||||
async fn _respond(body: String) -> hyper::Response<hyper::Body> {
|
||||
hyper::Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
@ -46,17 +61,43 @@ impl Responder for JsonEncoded {
|
||||
}
|
||||
}
|
||||
|
||||
/// respond with a body that is the url encoding of data.
|
||||
///
|
||||
/// The status code will be `200` and the content-type will be
|
||||
/// `application/x-www-form-urlencoded`.
|
||||
pub fn url_encoded<T>(data: T) -> impl Responder
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
UrlEncoded(serde_urlencoded::to_string(&data).unwrap())
|
||||
}
|
||||
/// The `UrlEncoded` responder returned by [url_encoded()](fn.url_encoded.html)
|
||||
#[derive(Debug)]
|
||||
pub struct UrlEncoded(String);
|
||||
impl Responder for UrlEncoded {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
|
||||
async fn _respond(body: String) -> hyper::Response<hyper::Body> {
|
||||
hyper::Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body.into())
|
||||
.unwrap()
|
||||
}
|
||||
Box::pin(_respond(self.0.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Responder for hyper::Response<B>
|
||||
where
|
||||
B: Clone + Into<hyper::Body> + Send + fmt::Debug,
|
||||
{
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
|
||||
async fn _respond(resp: http::Response<hyper::Body>) -> http::Response<hyper::Body> {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
|
||||
async fn _respond(resp: hyper::Response<hyper::Body>) -> hyper::Response<hyper::Body> {
|
||||
resp
|
||||
}
|
||||
// Turn &hyper::Response<Vec<u8>> into a hyper::Response<hyper::Body>
|
||||
let mut builder = hyper::Response::builder();
|
||||
builder
|
||||
builder = builder
|
||||
.status(self.status().clone())
|
||||
.version(self.version().clone());
|
||||
*builder.headers_mut().unwrap() = self.headers().clone();
|
||||
@ -66,19 +107,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycle through the provided list of responders.
|
||||
pub fn cycle(responders: Vec<Box<dyn Responder>>) -> impl Responder {
|
||||
if responders.is_empty() {
|
||||
panic!("empty vector provided to cycle");
|
||||
}
|
||||
Cycle { idx: 0, responders }
|
||||
}
|
||||
/// The `Cycle` responder returned by [cycle()](fn.cycle.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Cycle {
|
||||
idx: usize,
|
||||
responders: Vec<Box<dyn Responder>>,
|
||||
}
|
||||
impl Responder for Cycle {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send>> {
|
||||
fn respond(&mut self) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send>> {
|
||||
let response = self.responders[self.idx].respond();
|
||||
self.idx = (self.idx + 1) % self.responders.len();
|
||||
response
|
||||
|
||||
@ -43,8 +43,7 @@ impl Server {
|
||||
async move {
|
||||
// read the full body into memory prior to handing it to mappers.
|
||||
let (head, body) = req.into_parts();
|
||||
use futures::TryStreamExt;
|
||||
let full_body = body.try_concat().await?;
|
||||
let full_body = hyper::body::to_bytes(body).await?;
|
||||
let req = hyper::Request::from_parts(head, full_body.to_vec());
|
||||
log::debug!("Received Request: {:?}", req);
|
||||
let resp = on_req(state, req).await;
|
||||
@ -61,7 +60,11 @@ impl Server {
|
||||
let addr = server.local_addr();
|
||||
let (trigger_shutdown, shutdown_received) = futures::channel::oneshot::channel();
|
||||
let join_handle = std::thread::spawn(move || {
|
||||
let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
|
||||
let mut runtime = tokio::runtime::Builder::new()
|
||||
.basic_scheduler()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
runtime.block_on(async move {
|
||||
futures::select! {
|
||||
_ = server.fuse() => {},
|
||||
@ -88,11 +91,12 @@ impl Server {
|
||||
/// If the server is listening on port 1234.
|
||||
///
|
||||
/// `server.url("/foo?q=1") == "http://localhost:1234/foo?q=1"`
|
||||
pub fn url<T>(&self, path_and_query: T) -> http::Uri
|
||||
pub fn url<T>(&self, path_and_query: T) -> hyper::Uri
|
||||
where
|
||||
http::uri::PathAndQuery: http::HttpTryFrom<T>,
|
||||
http::uri::PathAndQuery: std::convert::TryFrom<T>,
|
||||
<http::uri::PathAndQuery as std::convert::TryFrom<T>>::Error: Into<http::Error>,
|
||||
{
|
||||
http::Uri::builder()
|
||||
hyper::Uri::builder()
|
||||
.scheme("http")
|
||||
.authority(format!("{}", &self.addr).as_str())
|
||||
.path_and_query(path_and_query)
|
||||
@ -109,6 +113,11 @@ impl Server {
|
||||
/// all expectations leaving the server running in a clean state.
|
||||
pub fn verify_and_clear(&mut self) {
|
||||
let mut state = self.state.lock();
|
||||
if std::thread::panicking() {
|
||||
// If the test is already panicking don't double panic on drop.
|
||||
state.expected.clear();
|
||||
return;
|
||||
}
|
||||
for expectation in state.expected.iter() {
|
||||
let is_valid_cardinality = match &expectation.cardinality {
|
||||
Times::AnyNumber => true,
|
||||
@ -151,7 +160,7 @@ impl Drop for Server {
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_req(state: ServerState, req: FullRequest) -> http::Response<hyper::Body> {
|
||||
async fn on_req(state: ServerState, req: FullRequest) -> hyper::Response<hyper::Body> {
|
||||
let response_future = {
|
||||
let mut state = state.lock();
|
||||
let mut iter = state.expected.iter_mut();
|
||||
@ -193,8 +202,8 @@ async fn on_req(state: ServerState, req: FullRequest) -> http::Response<hyper::B
|
||||
if let Some(f) = response_future {
|
||||
f.await
|
||||
} else {
|
||||
http::Response::builder()
|
||||
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
hyper::Response::builder()
|
||||
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(hyper::Body::empty())
|
||||
.unwrap()
|
||||
}
|
||||
@ -300,14 +309,14 @@ fn cardinality_error(
|
||||
matcher: &dyn Matcher<FullRequest>,
|
||||
cardinality: &Times,
|
||||
hit_count: usize,
|
||||
) -> Pin<Box<dyn Future<Output = http::Response<hyper::Body>> + Send + 'static>> {
|
||||
) -> Pin<Box<dyn Future<Output = hyper::Response<hyper::Body>> + Send + 'static>> {
|
||||
let body = hyper::Body::from(format!(
|
||||
"Unexpected number of requests for matcher '{:?}'; received {}; expected {:?}",
|
||||
matcher, hit_count, cardinality,
|
||||
));
|
||||
Box::pin(async move {
|
||||
http::Response::builder()
|
||||
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
hyper::Response::builder()
|
||||
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
})
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use httptest::{mappers::*, responders::*, Expectation, Times};
|
||||
|
||||
async fn read_response_body(resp: hyper::Response<hyper::Body>) -> hyper::Response<Vec<u8>> {
|
||||
use futures::stream::TryStreamExt;
|
||||
async fn read_response_body(
|
||||
resp: hyper::Response<hyper::Body>,
|
||||
) -> hyper::Response<hyper::body::Bytes> {
|
||||
let (head, body) = resp.into_parts();
|
||||
let body = body.try_concat().await.unwrap().to_vec();
|
||||
let body = hyper::body::to_bytes(body).await.unwrap();
|
||||
hyper::Response::from_parts(head, body)
|
||||
}
|
||||
|
||||
@ -84,6 +85,7 @@ async fn test_expectation_cardinality_exceeded() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_json() {
|
||||
use bstr::B;
|
||||
let _ = pretty_env_logger::try_init();
|
||||
|
||||
let my_data = serde_json::json!({
|
||||
@ -103,11 +105,16 @@ async fn test_json() {
|
||||
.respond_with(json_encoded(my_data.clone())),
|
||||
);
|
||||
|
||||
// Issue the GET /foo to the server and verify it returns a 200.
|
||||
// Issue the GET /foo to the server and verify it returns a 200 with a json
|
||||
// body matching my_data.
|
||||
let client = hyper::Client::new();
|
||||
let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await;
|
||||
assert!(all_of![
|
||||
response::status_code(eq(200)),
|
||||
response::headers(sequence::contains((
|
||||
deref(eq(B("content-type"))),
|
||||
deref(eq(B("application/json"))),
|
||||
))),
|
||||
response::body(json_decoded(eq(my_data))),
|
||||
]
|
||||
.matches(&resp));
|
||||
@ -140,3 +147,45 @@ async fn test_cycle() {
|
||||
let resp = read_response_body(client.get(server.url("/foo")).await.unwrap()).await;
|
||||
assert!(response::status_code(eq(404)).matches(&resp));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_url_encoded() {
|
||||
use bstr::B;
|
||||
let _ = pretty_env_logger::try_init();
|
||||
|
||||
// Setup a server to expect a single GET /foo request and respond with a
|
||||
// json response.
|
||||
let my_data = vec![("key", "value")];
|
||||
let server = httptest::Server::run();
|
||||
server.expect(
|
||||
Expectation::matching(all_of![
|
||||
request::method(eq("GET")),
|
||||
request::path(eq("/foo")),
|
||||
request::query(url_decoded(sequence::contains((
|
||||
deref(eq("key")),
|
||||
deref(eq("value")),
|
||||
)))),
|
||||
])
|
||||
.times(Times::Exactly(1))
|
||||
.respond_with(url_encoded(my_data.clone())),
|
||||
);
|
||||
|
||||
// Issue the GET /foo?key=value to the server and verify it returns a 200 with an
|
||||
// application/x-www-form-urlencoded body of key=value.
|
||||
let client = hyper::Client::new();
|
||||
let resp = read_response_body(client.get(server.url("/foo?key=value")).await.unwrap()).await;
|
||||
assert!(all_of![
|
||||
response::status_code(eq(200)),
|
||||
response::headers(sequence::contains((
|
||||
deref(eq(B("content-type"))),
|
||||
deref(eq(B("application/x-www-form-urlencoded"))),
|
||||
))),
|
||||
response::body(url_decoded(sequence::contains((
|
||||
deref(eq("key")),
|
||||
deref(eq("value"))
|
||||
)))),
|
||||
]
|
||||
.matches(&resp));
|
||||
|
||||
// The Drop impl of the server will assert that all expectations were satisfied or else it will panic.
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user