├── .gitignore ├── .travis.yml ├── src ├── error.rs ├── client │ ├── stub │ │ ├── error.rs │ │ ├── settings.rs │ │ ├── builder.rs │ │ └── mod.rs │ ├── direct.rs │ ├── mod.rs │ ├── generic.rs │ └── replay.rs ├── config.rs ├── request_builder.rs ├── lib.rs ├── body.rs ├── helper.rs ├── response.rs └── request.rs ├── Cargo.toml ├── examples └── simple.rs ├── tests ├── direct.rs └── helper │ └── mod.rs ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: xenial 3 | 4 | language: rust 5 | rust: 6 | - stable 7 | - beta 8 | - nightly 9 | 10 | cache: 11 | cargo: true 12 | 13 | matrix: 14 | allow_failures: 15 | - rust: nightly 16 | fast_finish: true -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Defines the `Error` type we use in this library (error-chain). 2 | 3 | error_chain! { 4 | types { 5 | Error, ErrorKind, ResultExt; 6 | } 7 | 8 | links { 9 | } 10 | 11 | foreign_links { 12 | Io(::std::io::Error); 13 | Reqwest(::reqwest::Error); 14 | SerdeJson(::serde_json::Error); 15 | FromUtf8(::std::string::FromUtf8Error); 16 | } 17 | 18 | errors { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reqwest_mock" 3 | version = "0.7.0" 4 | authors = ["Leo Schwarz "] 5 | license = "Apache-2.0" 6 | description = "Provides a mockable reqwest-like HTTP client." 7 | repository = "https://github.com/leoschwarz/reqwest_mock" 8 | documentation = "https://docs.rs/reqwest_mock/" 9 | keywords = ["http", "request", "client"] 10 | 11 | [dependencies] 12 | base64 = "0.12.0" 13 | error-chain = "0.12" 14 | http = "0.2" 15 | log = "0.4.0" 16 | reqwest = { version = "0.10", features = ["blocking", "gzip"] } 17 | serde = "1.0" 18 | serde_derive = "1.0" 19 | serde_json = "1.0" 20 | twox-hash = "1.1" 21 | url = "2.1" 22 | 23 | [dev-dependencies] 24 | futures = "0.1" 25 | hyper = "0.11" 26 | regex = "1.1.0" 27 | -------------------------------------------------------------------------------- /src/client/stub/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | // TODO: Hide what is not needed. 4 | // TODO: impl Error 5 | 6 | // TODO: Should be crate or module visible. 7 | #[derive(Debug)] 8 | pub struct FieldError { 9 | /// The name of the missing field. 10 | pub field_name: &'static str, 11 | /// The strictness level, which implied this field being required. 12 | pub strictness: &'static str, 13 | } 14 | 15 | // TODO: Variants should be private. 16 | #[derive(Debug)] 17 | pub enum RegisterStubError { 18 | // "Tried registering stub without `{}` even though `{}` requires its presence." 19 | MissingField(FieldError), 20 | 21 | // "Tried registering stub with `{}` in the request, even though `{}` means you don't want to check it in requests. Please remove the field or set a higher `StubStrictness`." 22 | UnescessaryField(FieldError), 23 | ReadFile(io::Error), 24 | } 25 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | extern crate reqwest_mock; 2 | 3 | use reqwest_mock::client::*; 4 | 5 | const URL: &'static str = "https://httpbin.org/uuid"; 6 | const URL2: &'static str = "https://httpbin.org/uuid#"; 7 | 8 | fn perform_request(client: &C, url: &str) -> String { 9 | // This method is just a placeholder for some fancy computations. 10 | let response = client.get(url).send().unwrap(); 11 | response.body_to_utf8().unwrap() 12 | } 13 | 14 | fn main() { 15 | let client1 = DirectClient::new(); 16 | let resp1 = perform_request(&client1, URL); 17 | let resp2 = perform_request(&client1, URL); 18 | // httpbin should return a different UUID for every actual request 19 | assert_ne!(resp1, resp2); 20 | 21 | let client2 = ReplayClient::new(RecordingTarget::file("simple.replay")); 22 | let resp1 = perform_request(&client2, URL); 23 | let resp2 = perform_request(&client2, URL); 24 | assert_eq!(resp1, resp2); 25 | 26 | let resp3 = perform_request(&client2, URL2); 27 | assert_ne!(resp1, resp3); 28 | 29 | let resp4 = perform_request(&client2, URL2); 30 | assert_eq!(resp3, resp4); 31 | 32 | println!("Tests finished."); 33 | } 34 | -------------------------------------------------------------------------------- /tests/direct.rs: -------------------------------------------------------------------------------- 1 | //! Test the `DirectClient`. 2 | 3 | extern crate futures; 4 | extern crate hyper; 5 | extern crate regex; 6 | extern crate reqwest; 7 | extern crate reqwest_mock; 8 | mod helper; 9 | 10 | use reqwest_mock::client::DirectClient; 11 | use reqwest_mock::{Client, StatusCode}; 12 | 13 | #[test] 14 | fn direct_client() { 15 | let server = helper::run_server("127.0.0.1:19241".parse().unwrap()); 16 | 17 | // Good request. 18 | let client = DirectClient::new(); 19 | let resp = client 20 | .get("http://127.0.0.1:19241/abc") 21 | .body("42") 22 | .send() 23 | .unwrap(); 24 | assert_eq!(resp.status, StatusCode::OK); 25 | let lines: Vec = resp 26 | .body_to_utf8() 27 | .unwrap() 28 | .lines() 29 | .map(String::from) 30 | .collect(); 31 | assert_eq!(lines[0], "43"); 32 | assert_eq!(lines[1], "GET /abc"); 33 | 34 | // Bad request. 35 | let resp = client 36 | .get("http://127.0.0.1:19241/xyz") 37 | .body("pi") 38 | .send() 39 | .unwrap(); 40 | assert_eq!(resp.status, StatusCode::BAD_REQUEST); 41 | assert_eq!(resp.body_to_utf8().unwrap(), "pi"); 42 | 43 | server.terminate(); 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reqwest_mock: Discontinued 2 | Thank you for your interest in this project. 3 | This project was a proof of concept to create a generic interface for the Rust reqwest crate that would allow users to mock and record and replay HTTP requests for testing purposes. 4 | 5 | This project is no longer updated or maintained and this repository will be archived. 6 | 7 | The following non-exhaustive list of alternative solutions are recommended instead to mock HTTP requests in Rust (as of 18.5.2023): 8 | 9 | - https://github.com/chorusone/rvcr 10 | - https://github.com/lukemathwalker/wiremock-rs 11 | - https://github.com/beltram/stubr 12 | - https://github.com/alexliesenfeld/httpmock 13 | - https://github.com/lipanski/mockito 14 | 15 | ## Old readme 16 | [Crates.io Link](https://crates.io/crates/reqwest_mock) 17 | 18 | Provides a mockable reqwest-like HTTP client. 19 | 20 | Write your code generic over the [Client](https://docs.rs/reqwest_mock/latest/reqwest_mock/client/trait.Client.html) trait, 21 | and in production use [DirectClient](https://docs.rs/reqwest_mock/latest/reqwest_mock/client/struct.DirectClient.html) while in testing 22 | you can use [ReplayClient](https://docs.rs/reqwest_mock/latest/reqwest_mock/client/struct.ReplayClient.html), which will record a request 23 | the first time and replay it every time the exact same request is made in the 24 | future. 25 | 26 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Some types used to configure a `Client` instance. 2 | 3 | use std::time::Duration; 4 | 5 | /// Configures some parameters for a `Client` instance. 6 | #[derive(Clone, Debug)] 7 | pub struct ClientConfig { 8 | /// Enable auto gzip decompression checking the `ContentEncoding` response header. 9 | /// 10 | /// Default is enabled. 11 | pub gzip: bool, 12 | 13 | /// `RedirectPolicy` for this client. 14 | /// 15 | /// Default will follow up to 10 redirects. 16 | pub redirect: RedirectPolicy, 17 | 18 | /// Enable or disable automatic setting of the `Referer` header. 19 | /// 20 | /// Default is true. 21 | /// 22 | /// TODO: Not actually implemented yet. 23 | pub referer: bool, 24 | 25 | /// Timeout for both the read and write operations of a client. 26 | pub timeout: Option, 27 | } 28 | 29 | impl Default for ClientConfig { 30 | fn default() -> Self { 31 | ClientConfig { 32 | gzip: true, 33 | redirect: RedirectPolicy::default(), 34 | referer: true, 35 | timeout: None, 36 | } 37 | } 38 | } 39 | 40 | impl ClientConfig { 41 | pub fn new() -> Self { 42 | Self::default() 43 | } 44 | 45 | // TODO implement builder pattern 46 | } 47 | 48 | /// Specifies how to hande redirects. 49 | #[derive(Clone, Debug)] 50 | pub enum RedirectPolicy { 51 | Limit(usize), 52 | None, 53 | } 54 | 55 | impl Default for RedirectPolicy { 56 | fn default() -> Self { 57 | RedirectPolicy::Limit(10) 58 | } 59 | } 60 | 61 | impl From for ::reqwest::redirect::Policy { 62 | fn from(p: RedirectPolicy) -> Self { 63 | match p { 64 | RedirectPolicy::Limit(n) => ::reqwest::redirect::Policy::limited(n), 65 | RedirectPolicy::None => ::reqwest::redirect::Policy::none(), 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/request_builder.rs: -------------------------------------------------------------------------------- 1 | use body::Body; 2 | use client::Client; 3 | use error::{Error, ResultExt}; 4 | use request::{Request, RequestHeader}; 5 | use reqwest::header::{HeaderMap, HeaderValue, IntoHeaderName}; 6 | use reqwest::{IntoUrl, Method, Url}; 7 | use response::Response; 8 | 9 | pub struct RequestBuilder<'cl, Cl: Client + 'cl> { 10 | client: &'cl Cl, 11 | 12 | url: Result, 13 | method: Method, 14 | headers: HeaderMap, 15 | body: Option, 16 | } 17 | 18 | impl<'cl, Cl: Client + 'cl> RequestBuilder<'cl, Cl> { 19 | #[doc(hidden)] 20 | pub fn new(client: &'cl Cl, url: U, method: Method) -> Self { 21 | RequestBuilder { 22 | client: client, 23 | url: url.into_url().chain_err(|| "invalid url"), 24 | method: method, 25 | headers: HeaderMap::new(), 26 | body: None, 27 | } 28 | } 29 | 30 | /// Add a header to the request. 31 | pub fn header(mut self, name: H, value: HeaderValue) -> Self { 32 | self.headers.insert(name, value); 33 | self 34 | } 35 | 36 | /// Add multiple headers to the request. 37 | pub fn headers(mut self, headers: HeaderMap) -> Self { 38 | self.headers.extend(headers); 39 | self 40 | } 41 | 42 | /// Set the body of the request. 43 | pub fn body>(mut self, body: B) -> Self { 44 | self.body = Some(body.into()); 45 | self 46 | } 47 | 48 | /// Send the request. 49 | pub fn send(self) -> Result { 50 | let request = Request { 51 | header: RequestHeader { 52 | url: self.url?, 53 | method: self.method, 54 | headers: self.headers, 55 | }, 56 | body: self.body, 57 | }; 58 | 59 | self.client.execute(None, request) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/client/stub/settings.rs: -------------------------------------------------------------------------------- 1 | /// Control how strict the `StubClient` is about matching stubs to the requests you make through 2 | /// the client. 3 | /// 4 | /// When you make a request it consists of a url, a HTTP method, headers and a body. To determine which 5 | /// response to return the `StubClient` checks the different fields against the ones stored in its 6 | /// list of stubs. You can control which fields are checked for equality. 7 | /// 8 | /// TODO: Determine the need of matching the full header. 9 | #[derive(Clone, Debug, Eq, PartialEq)] 10 | pub enum StubStrictness { 11 | /// Full equality in all fields between stub and actual request required. 12 | Full, 13 | 14 | /// `body`, `method` and `url` have to be equal. 15 | BodyMethodUrl, 16 | 17 | /// `headers`, `method` and `url` have to be equal. 18 | HeadersMethodUrl, 19 | 20 | /// `method` and `url` have to be equal. 21 | MethodUrl, 22 | 23 | /// `url` has to be equal. 24 | Url, 25 | } 26 | 27 | /// Define the default action to be taken by the `StubClient` when no matching stub is found. 28 | #[derive(Clone, Debug, Eq, PartialEq)] 29 | pub enum StubDefault { 30 | /// Just directly perform any requests. Warning: only use this if you are sure it is ok. 31 | PerformRequest, 32 | 33 | /// Panic if such a request is made. 34 | Panic, 35 | 36 | /// Return an `Err` if such a request is made. 37 | Error, 38 | } 39 | 40 | /// Some settings for the `StubClient`. 41 | #[derive(Clone, Debug, Eq, PartialEq)] 42 | pub struct StubSettings { 43 | /// Specifies the default action to be taken when no stub is found for a request. 44 | pub default: StubDefault, 45 | 46 | /// Specifies how strict matching requests to stubs is. 47 | pub strictness: StubStrictness, 48 | } 49 | 50 | impl Default for StubSettings { 51 | fn default() -> Self { 52 | StubSettings { 53 | default: StubDefault::Error, 54 | strictness: StubStrictness::Full, 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Provides a mockable reqwest-like HTTP client. 2 | //! 3 | //! Write your code generic over the [Client](client/trait.Client.html) trait, 4 | //! and in production use [DirectClient](client/struct.DirectClient.html) while in testing 5 | //! you can use [ReplayClient](client/struct.ReplayClient.html), which will record a request 6 | //! the first time and replay it every time the exact same request is made in the 7 | //! future. 8 | //! 9 | //! # Examples 10 | //! 11 | //! ``` 12 | //! use reqwest_mock::{Client, DirectClient, ReplayClient, Error}; 13 | //! use reqwest_mock::header::USER_AGENT; 14 | //! 15 | //! struct MyClient { 16 | //! client: C, 17 | //! } 18 | //! 19 | //! fn new_client() -> MyClient { 20 | //! MyClient { 21 | //! client: DirectClient::new() 22 | //! } 23 | //! } 24 | //! 25 | //! #[cfg(test)] 26 | //! fn test_client(path: &str) -> MyClient { 27 | //! MyClient { 28 | //! client: ReplayClient::new(path) 29 | //! } 30 | //! } 31 | //! 32 | //! impl MyClient { 33 | //! /// For simplicity's sake we are not parsing the response but just extracting the 34 | //! /// response body. 35 | //! /// Also in your own code it might be a good idea to define your own `Error` type. 36 | //! pub fn get_time(&self) -> Result { 37 | //! let response = self.client 38 | //! .get("https://now.httpbin.org/") 39 | //! .header(USER_AGENT, "MyClient".parse().unwrap()) 40 | //! .send()?; 41 | //! 42 | //! response.body_to_utf8() 43 | //! } 44 | //! } 45 | //! ``` 46 | 47 | extern crate base64; 48 | #[macro_use] 49 | extern crate error_chain; 50 | extern crate http; 51 | #[macro_use] 52 | extern crate log; 53 | extern crate reqwest; 54 | extern crate serde; 55 | #[macro_use] 56 | extern crate serde_derive; 57 | extern crate serde_json; 58 | extern crate twox_hash; 59 | extern crate url; 60 | 61 | mod helper; 62 | 63 | pub mod config; 64 | pub mod error; 65 | 66 | mod body; 67 | pub use body::Body; 68 | 69 | mod request; 70 | mod response; 71 | 72 | pub mod client; 73 | mod request_builder; 74 | 75 | pub use self::client::*; 76 | pub use self::error::Error; 77 | 78 | pub use reqwest::{header, IntoUrl, Method, StatusCode, Url}; 79 | pub use url::ParseError as UrlError; 80 | -------------------------------------------------------------------------------- /src/body.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, Read}; 3 | 4 | /// A HTTP request body. 5 | /// 6 | /// This is either a file pointer or a memory sequence of bytes. 7 | /// This distinction only matters when using `DirectClient`, in which case a file 8 | /// might be read chunked. 9 | /// 10 | /// Note that this is **not** the same type as the one found in the `reqwest` crate. 11 | #[derive(Debug)] 12 | pub struct Body { 13 | value: BodyValue, 14 | } 15 | 16 | #[derive(Debug)] 17 | enum BodyValue { 18 | /// Bytes kept in memory. 19 | Bytes(Vec), 20 | 21 | /// A pointer to a file yet to be read. 22 | File(File), 23 | } 24 | 25 | impl Body { 26 | // TODO: Consider whether this should be public for everyone. 27 | pub(crate) fn try_to_vec(self) -> Result, io::Error> { 28 | match self.value { 29 | BodyValue::Bytes(bs) => Ok(bs), 30 | BodyValue::File(mut f) => { 31 | let mut bytes = Vec::new(); 32 | f.read_to_end(&mut bytes)?; 33 | Ok(bytes) 34 | } 35 | } 36 | } 37 | } 38 | 39 | impl From for ::reqwest::blocking::Body { 40 | fn from(b: Body) -> ::reqwest::blocking::Body { 41 | match b.value { 42 | BodyValue::Bytes(b) => b.into(), 43 | BodyValue::File(f) => f.into(), 44 | } 45 | } 46 | } 47 | 48 | impl From> for Body { 49 | #[inline] 50 | fn from(v: Vec) -> Self { 51 | Body { 52 | value: BodyValue::Bytes(v.into()), 53 | } 54 | } 55 | } 56 | 57 | impl From for Body { 58 | #[inline] 59 | fn from(s: String) -> Self { 60 | Body { 61 | value: BodyValue::Bytes(s.into()), 62 | } 63 | } 64 | } 65 | 66 | impl<'a> From<&'a str> for Body { 67 | #[inline] 68 | fn from(s: &'a str) -> Self { 69 | Body { 70 | value: BodyValue::Bytes(s.into()), 71 | } 72 | } 73 | } 74 | 75 | impl From<&'static [u8]> for Body { 76 | #[inline] 77 | fn from(s: &'static [u8]) -> Self { 78 | Body { 79 | value: BodyValue::Bytes(s.into()), 80 | } 81 | } 82 | } 83 | 84 | impl From for Body { 85 | #[inline] 86 | fn from(f: File) -> Self { 87 | Body { 88 | value: BodyValue::File(f), 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | //! Defines some things used from different modules but not to be exported. 2 | 3 | use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; 4 | use std::collections::BTreeMap; 5 | use std::iter::FromIterator; 6 | 7 | pub fn serialize_headers(headers: &HeaderMap) -> BTreeMap { 8 | let tuples_iter = headers 9 | .iter() 10 | .map(|(hn, hv)| (hn.to_string(), hv.to_str().unwrap().to_string())); 11 | 12 | BTreeMap::::from_iter(tuples_iter) 13 | } 14 | 15 | pub fn deserialize_headers(map: &BTreeMap) -> HeaderMap { 16 | let mut headers = ::reqwest::header::HeaderMap::new(); 17 | for (name, value) in map.iter() { 18 | headers.insert( 19 | HeaderName::from_bytes(name.as_ref()).unwrap(), 20 | HeaderValue::from_str(value).unwrap(), 21 | ); 22 | } 23 | 24 | headers 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | use reqwest::header::{CONTENT_TYPE, USER_AGENT}; 31 | 32 | /// Just a basic example of one single header being serialized. 33 | #[test] 34 | fn serialize_headers() { 35 | let mut headers = HeaderMap::new(); 36 | headers.insert(USER_AGENT, "testing".parse().unwrap()); 37 | let serialized = super::serialize_headers(&headers); 38 | let mut expected = BTreeMap::new(); 39 | expected.insert("user-agent".to_string(), "testing".to_string()); 40 | assert_eq!(serialized, expected); 41 | } 42 | 43 | /// Now a less trivial example checking whether the headers are being sorted, 44 | /// which is important for things like hashing of requests, which has to be 45 | /// deterministic regardless of the order headers were appended. 46 | #[test] 47 | fn serialize_headers_deterministic() { 48 | let mut headers1 = HeaderMap::new(); 49 | headers1.insert(USER_AGENT, "testing".parse().unwrap()); 50 | headers1.insert(CONTENT_TYPE, "image/png".parse().unwrap()); 51 | let mut headers2 = HeaderMap::new(); 52 | headers2.insert(CONTENT_TYPE, "image/png".parse().unwrap()); 53 | headers2.insert(USER_AGENT, "testing".parse().unwrap()); 54 | 55 | let ser1 = super::serialize_headers(&headers1); 56 | let ser2 = super::serialize_headers(&headers2); 57 | 58 | assert_eq!(ser1, ser2); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/client/direct.rs: -------------------------------------------------------------------------------- 1 | use client::{Client, Response}; 2 | use config::ClientConfig; 3 | use error::Error; 4 | use request::Request; 5 | use std::io::Read; 6 | 7 | /// Just a regular client performing no mocking at all. 8 | /// 9 | /// The idea is that this one can be used in production code, 10 | /// while another client is to be used in testing code. 11 | pub struct DirectClient { 12 | config: ClientConfig, 13 | } 14 | 15 | impl DirectClient { 16 | pub fn new() -> Self { 17 | DirectClient { 18 | config: ClientConfig::default(), 19 | } 20 | } 21 | } 22 | 23 | impl Client for DirectClient { 24 | fn execute(&self, config: Option<&ClientConfig>, request: Request) -> Result { 25 | // Some information potentially useful for debugging. 26 | debug!( 27 | "DirectClient performing {} request of URL: {}", 28 | request.header.method, request.header.url 29 | ); 30 | trace!("request headers: {:?}", request.header.headers); 31 | //trace!("request body: {:?}", request.header.body); 32 | 33 | // Use internal config if none was provided together with the request. 34 | let config = config.unwrap_or_else(|| &self.config); 35 | 36 | // Setup the client instance. 37 | let mut client_builder = ::reqwest::blocking::Client::builder() 38 | .gzip(config.gzip) 39 | .redirect(config.redirect.clone().into()) 40 | .referer(config.referer); 41 | if let Some(timeout) = config.timeout.clone() { 42 | client_builder = client_builder.timeout(timeout); 43 | } 44 | let client = client_builder.build()?; 45 | 46 | // Build the request. 47 | let mut builder = client.request(request.header.method, request.header.url); 48 | if let Some(body) = request.body { 49 | builder = builder.body(::reqwest::blocking::Body::from(body)); 50 | } 51 | 52 | // Send the request. 53 | let mut response = builder.send()?; 54 | 55 | // Extract the response. 56 | Ok(Response { 57 | url: response.url().clone(), 58 | status: response.status().clone(), 59 | headers: response.headers().clone(), 60 | body: { 61 | let mut buf = Vec::::new(); 62 | response.read_to_end(&mut buf)?; 63 | buf 64 | }, 65 | }) 66 | } 67 | 68 | fn config(&self) -> &ClientConfig { 69 | &self.config 70 | } 71 | 72 | fn config_mut(&mut self) -> &mut ClientConfig { 73 | &mut self.config 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defines the main types to be used to mock the HTTP client. 2 | 3 | use config::ClientConfig; 4 | use error::Error; 5 | use request::Request; 6 | use request_builder::RequestBuilder; 7 | use reqwest::{IntoUrl, Method}; 8 | use response::Response; 9 | 10 | /// Provides a unified interface over the different Clients. 11 | /// 12 | /// Write your code generic over this trait and for example in testing you can use a different 13 | /// client than in your normal library's code. 14 | pub trait Client: Sized { 15 | /// Execute a request. 16 | /// 17 | /// If config is `None` the client is to use the internal config otherwise it is to use the 18 | /// provided config here. 19 | fn execute(&self, config: Option<&ClientConfig>, request: Request) -> Result; 20 | 21 | /// Returns a immutable reference to the internal config. 22 | fn config(&self) -> &ClientConfig; 23 | 24 | /// Returns a mutable reference to the internal config. 25 | fn config_mut(&mut self) -> &mut ClientConfig; 26 | 27 | //////////////////////////////////////////////////////// 28 | 29 | /// Convenience method to make a `GET` request to a URL. 30 | fn get<'cl, U: IntoUrl>(&'cl self, url: U) -> RequestBuilder<'cl, Self> { 31 | self.request(Method::GET, url) 32 | } 33 | 34 | /// Convenience method to make a `POST` request to a URL. 35 | fn post<'cl, U: IntoUrl>(&'cl self, url: U) -> RequestBuilder<'cl, Self> { 36 | self.request(Method::POST, url) 37 | } 38 | 39 | /// Convenience method to make a `PUT` request to a URL. 40 | fn put<'cl, U: IntoUrl>(&'cl self, url: U) -> RequestBuilder<'cl, Self> { 41 | self.request(Method::PUT, url) 42 | } 43 | 44 | /// Convenience method to make a `PATCH` request to a URL. 45 | fn patch<'cl, U: IntoUrl>(&'cl self, url: U) -> RequestBuilder<'cl, Self> { 46 | self.request(Method::PATCH, url) 47 | } 48 | 49 | /// Convenience method to make a `DELETE` request to a URL. 50 | fn delete<'cl, U: IntoUrl>(&'cl self, url: U) -> RequestBuilder<'cl, Self> { 51 | self.request(Method::DELETE, url) 52 | } 53 | 54 | /// Convenience method to make a `HEAD` request to a URL. 55 | fn head<'cl, U: IntoUrl>(&'cl self, url: U) -> RequestBuilder<'cl, Self> { 56 | self.request(Method::HEAD, url) 57 | } 58 | 59 | /// Returns a `RequestBuilder` for the given method and URL, which allows for further 60 | /// configuration of the request, like including additional headers, and sending it. 61 | fn request<'cl, U: IntoUrl>(&'cl self, method: Method, url: U) -> RequestBuilder<'cl, Self> { 62 | RequestBuilder::new(self, url, method) 63 | } 64 | } 65 | 66 | mod direct; 67 | pub use self::direct::DirectClient; 68 | 69 | mod replay; 70 | pub use self::replay::{RecordingTarget, ReplayClient}; 71 | 72 | mod stub; 73 | pub use self::stub::{ 74 | RequestStubber, ResponseStubber, StubClient, StubDefault, StubSettings, StubStrictness, 75 | }; 76 | 77 | mod generic; 78 | pub use self::generic::GenericClient; 79 | -------------------------------------------------------------------------------- /src/client/generic.rs: -------------------------------------------------------------------------------- 1 | use client::{Client, Response}; 2 | use client::{DirectClient, RecordingTarget, ReplayClient, StubClient}; 3 | use config::ClientConfig; 4 | use error::Error; 5 | use request::Request; 6 | use std::path::PathBuf; 7 | 8 | enum InnerClient { 9 | Direct(DirectClient), 10 | Replay(ReplayClient), 11 | Stub(StubClient), 12 | } 13 | 14 | /// Provides an interface over the different client types which you can use in your code 15 | /// if you want to avoid it having to be generic over the `Client` trait. 16 | pub struct GenericClient { 17 | inner: InnerClient, 18 | } 19 | 20 | impl GenericClient { 21 | /// Create a `GenericClient` using `DirectClient` internally. 22 | pub fn direct() -> Self { 23 | DirectClient::new().into() 24 | } 25 | 26 | /// Create a `GenericClient` using `ReplayClient` internally, recording one single 27 | /// request to one single replay file. If a differing request is made, the file will be 28 | /// overwritten again. 29 | pub fn replay_file>(replay_file: P) -> Self { 30 | ReplayClient::new(RecordingTarget::File(replay_file.into())).into() 31 | } 32 | 33 | /// Create a `GenericClient` using `ReplayClient` internally, recording multiple requests 34 | /// to a single directory. Each unique request will get its own replay file independent of 35 | /// other requests in the specified directory. 36 | pub fn replay_dir>(replay_dir: P) -> Self { 37 | ReplayClient::new(RecordingTarget::Dir(replay_dir.into())).into() 38 | } 39 | 40 | /// Create a `GenericClient` using `StubClient` internally. 41 | pub fn stub(client: StubClient) -> Self { 42 | client.into() 43 | } 44 | 45 | /// If this is a ReplayClient it will inform the Replay Client that whichever next request is 46 | /// made should be recorded again, even if it has been made exactly this way before. 47 | pub fn force_record_next(&self) { 48 | match self.inner { 49 | InnerClient::Direct(_) | InnerClient::Stub(_) => {} 50 | InnerClient::Replay(ref replay) => replay.force_record_next(), 51 | } 52 | } 53 | 54 | /* 55 | /// Convert the current instance to a `ReplayClient` replaying the file at the provided path. 56 | /// 57 | /// This can also be used to just switch the replay file as each file is only used for one 58 | /// request/response pair. 59 | pub fn replay_file>(&mut self, path: P) { 60 | self.inner = InnerClient::Replay(ReplayClient::new(RecordingTarget::File(path.into()))); 61 | } 62 | */ 63 | } 64 | 65 | impl From for GenericClient { 66 | fn from(c: DirectClient) -> Self { 67 | GenericClient { 68 | inner: InnerClient::Direct(c), 69 | } 70 | } 71 | } 72 | 73 | impl From for GenericClient { 74 | fn from(c: ReplayClient) -> Self { 75 | GenericClient { 76 | inner: InnerClient::Replay(c), 77 | } 78 | } 79 | } 80 | 81 | impl From for GenericClient { 82 | fn from(c: StubClient) -> Self { 83 | GenericClient { 84 | inner: InnerClient::Stub(c), 85 | } 86 | } 87 | } 88 | 89 | impl Client for GenericClient { 90 | fn execute(&self, config: Option<&ClientConfig>, request: Request) -> Result { 91 | match self.inner { 92 | InnerClient::Direct(ref client) => client.execute(config, request), 93 | InnerClient::Replay(ref client) => client.execute(config, request), 94 | InnerClient::Stub(ref client) => client.execute(config, request), 95 | } 96 | } 97 | 98 | fn config(&self) -> &ClientConfig { 99 | match self.inner { 100 | InnerClient::Direct(ref client) => client.config(), 101 | InnerClient::Replay(ref client) => client.config(), 102 | InnerClient::Stub(ref client) => client.config(), 103 | } 104 | } 105 | 106 | fn config_mut(&mut self) -> &mut ClientConfig { 107 | match self.inner { 108 | InnerClient::Direct(ref mut client) => client.config_mut(), 109 | InnerClient::Replay(ref mut client) => client.config_mut(), 110 | InnerClient::Stub(ref mut client) => client.config_mut(), 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/client/stub/builder.rs: -------------------------------------------------------------------------------- 1 | use body::Body; 2 | use client::stub::error::RegisterStubError; 3 | use client::stub::{StubClient, StubRequest, StubResponse}; 4 | use reqwest::header::{HeaderMap, HeaderValue, IntoHeaderName}; 5 | use reqwest::{Method, StatusCode, Url}; 6 | 7 | /// A request stub builder to be used in conjunction with `StubClient`. 8 | /// 9 | /// After you are finished specifying the details of the matching request, call `response()` to 10 | /// return a `ResponseStubber` instance and start specifying the response. Finally use 11 | /// `ResponseStubber::mock()` to register the mock into the client. 12 | #[must_use] 13 | pub struct RequestStubber<'cl> { 14 | client: &'cl mut StubClient, 15 | url: Url, 16 | 17 | _method: Option, 18 | _body: Option, 19 | _headers: Option, 20 | } 21 | 22 | impl<'cl> RequestStubber<'cl> { 23 | pub(super) fn new(client: &'cl mut StubClient, url: Url) -> RequestStubber<'cl> { 24 | RequestStubber { 25 | client: client, 26 | url: url, 27 | _method: None, 28 | _body: None, 29 | _headers: None, 30 | } 31 | } 32 | 33 | /// Set the method of the request. 34 | pub fn method(mut self, method: Method) -> Self { 35 | self._method = Some(method); 36 | self 37 | } 38 | 39 | /// Set the body of the request. 40 | pub fn body>(mut self, body: B) -> Self { 41 | self._body = Some(body.into()); 42 | self 43 | } 44 | 45 | /// Add a header to the request. 46 | pub fn header(mut self, name: HN, value: HeaderValue) -> Self { 47 | self._headers = Some(self._headers.map_or_else(HeaderMap::new, |mut hs| { 48 | hs.insert(name, value); 49 | hs 50 | })); 51 | self 52 | } 53 | 54 | /// Add multiple headers to the request. 55 | pub fn headers(mut self, headers: HeaderMap) -> Self { 56 | self._headers = Some(self._headers.map_or_else(HeaderMap::new, |mut hs| { 57 | hs.extend(headers); 58 | hs 59 | })); 60 | self 61 | } 62 | 63 | /// Stub the response to this request. 64 | pub fn response(self) -> ResponseStubber<'cl> { 65 | ResponseStubber { 66 | client: self.client, 67 | req: StubRequest { 68 | url: self.url, 69 | method: self._method, 70 | body: self._body, 71 | headers: self._headers.map(|hs| ::helper::serialize_headers(&hs)), 72 | }, 73 | 74 | _status_code: StatusCode::OK, 75 | _body: None, 76 | _headers: HeaderMap::new(), 77 | } 78 | } 79 | } 80 | 81 | /// A response stub builder to be used in conjunction with `StubClient`. 82 | #[must_use] 83 | pub struct ResponseStubber<'cl> { 84 | client: &'cl mut StubClient, 85 | req: StubRequest, 86 | 87 | _status_code: StatusCode, 88 | _body: Option, 89 | _headers: HeaderMap, 90 | } 91 | 92 | impl<'cl> ResponseStubber<'cl> { 93 | /// Set the status code of the response. 94 | pub fn status_code(mut self, status: StatusCode) -> Self { 95 | self._status_code = status; 96 | self 97 | } 98 | 99 | /// Set the body of the response. 100 | pub fn body>(mut self, body: B) -> Self { 101 | self._body = Some(body.into()); 102 | self 103 | } 104 | 105 | /// Add a header to the response. 106 | pub fn header(mut self, name: HN, value: HeaderValue) -> Self { 107 | self._headers.insert(name, value); 108 | self 109 | } 110 | 111 | /// Add multiple headers to the response. 112 | pub fn headers(mut self, headers: HeaderMap) -> Self { 113 | self._headers.extend(headers); 114 | self 115 | } 116 | 117 | /// Register the mock in the client. 118 | pub fn mock(self) -> Result<(), RegisterStubError> { 119 | let resp = StubResponse { 120 | status_code: self._status_code, 121 | body: self._body, 122 | headers: self._headers, 123 | }; 124 | self.client.register_stub( 125 | self.req 126 | .try_to_key() 127 | .map_err(|e| RegisterStubError::ReadFile(e))?, 128 | resp, 129 | ) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/helper/mod.rs: -------------------------------------------------------------------------------- 1 | //! Some helper functions for the integration tests. (Mainly the test server.) 2 | //! 3 | //! Specification of the test server: 4 | //! ================================= 5 | //! (square brackets are not printed, only included for clarity) 6 | //! 7 | //! Given a HTTP [METHOD] request of [URI] with [HEADERS] and [BODY], 8 | //! the server will check if BODY is a plaintext integer number (regex [0-9]+). 9 | //! If it is not, error 404 (bad request) will be returned, echoing the body. 10 | //! If it is, the number will be incremented by one and the following output 11 | //! will be generated: 12 | //! 13 | //! body (plaintext): 14 | //! ``` 15 | //! [INCREMENTED NUMBER]\n 16 | //! [METHOD] [URI]\n 17 | //! [HEADERS]\n 18 | //! ``` 19 | 20 | use futures::sync::oneshot; 21 | use futures::{self, Future, Stream}; 22 | use hyper::server::{Http, Service}; 23 | use hyper::{self, Request, Response, StatusCode}; 24 | use regex::Regex; 25 | use std::net::SocketAddr; 26 | use std::thread; 27 | 28 | struct TestServer; 29 | 30 | impl Service for TestServer { 31 | type Request = Request; 32 | type Response = Response; 33 | type Error = hyper::Error; 34 | type Future = Box>; 35 | 36 | fn call(&self, req: Request) -> Self::Future { 37 | // Extract the request headers. 38 | let method = req.method().clone(); 39 | let uri = req.uri().clone(); 40 | let headers = req.headers().clone(); 41 | 42 | // Extract the request body, note that it might consisd of multiple 43 | // chunks, which here should not be the case but has to be handled 44 | // anyway. 45 | let body = req.body().collect().and_then(|chunks| { 46 | // Extract body into a String. 47 | let mut bytes = Vec::new(); 48 | for chunk in chunks { 49 | bytes.extend(chunk); 50 | } 51 | futures::future::ok(String::from_utf8(bytes).expect("Invalid encoding.")) 52 | }); 53 | 54 | // Determine the response. 55 | let response = body.and_then(move |body| { 56 | // Try extracting the number. 57 | let re = Regex::new(r"^(\d+)$").unwrap(); 58 | let mut number: Option = None; 59 | if let Some(caps) = re.captures(body.as_str()) { 60 | if let Some(cap) = caps.get(1) { 61 | number = Some(cap.as_str().parse().unwrap()); 62 | } 63 | } 64 | 65 | // Create the response. 66 | let resp = if let Some(req_num) = number { 67 | let resp_num = req_num + 1; 68 | Response::new() 69 | .with_body(format!("{}\n{} {}\n{:?}", resp_num, method, uri, headers)) 70 | } else { 71 | Response::new() 72 | .with_body(body) 73 | .with_status(StatusCode::BadRequest) 74 | }; 75 | futures::future::ok(resp) 76 | }); 77 | Box::new(response) 78 | } 79 | } 80 | 81 | pub fn run_server(addr: SocketAddr) -> TestServerRunner { 82 | let (stop_tx, stop_rx) = oneshot::channel(); 83 | 84 | thread::spawn(move || { 85 | let server = Http::new().bind(&addr, || Ok(TestServer)).unwrap(); 86 | let stop_rx = stop_rx.map_err(|_| unreachable!()); 87 | server.run_until(stop_rx).unwrap(); 88 | }); 89 | 90 | TestServerRunner { 91 | stop_server: stop_tx, 92 | } 93 | } 94 | 95 | /// This struct is used to destroy the server automatically after running a test. 96 | pub struct TestServerRunner { 97 | stop_server: oneshot::Sender<()>, 98 | } 99 | 100 | impl TestServerRunner { 101 | pub fn terminate(self) { 102 | self.stop_server.send(()).unwrap(); 103 | } 104 | } 105 | 106 | /* 107 | #[test] 108 | fn check_test_server() { 109 | let server = run_server("127.0.0.1:19241".parse().unwrap()); 110 | let client = reqwest::Client::new(); 111 | 112 | // Good request. 113 | let mut resp = client.get("http://127.0.0.1:19241/abc") 114 | .body("42") 115 | .send() 116 | .unwrap(); 117 | assert_eq!(resp.status(), StatusCode::Ok); 118 | let lines: Vec = resp.text().unwrap().lines().map(String::from).collect(); 119 | assert_eq!(lines[0], "43"); 120 | assert_eq!(lines[1], "GET /abc"); 121 | 122 | // Bad request. 123 | let mut resp = client.get("http://127.0.0.1:19241/xyz") 124 | .body("pi") 125 | .send() 126 | .unwrap(); 127 | assert_eq!(resp.status(), StatusCode::BadRequest); 128 | assert_eq!(resp.text().unwrap(), "pi"); 129 | 130 | server.terminate(); 131 | } 132 | */ 133 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use error::Error; 3 | use http::Response as HttpResponse; 4 | use reqwest::header::HeaderMap; 5 | use reqwest::{StatusCode, Url}; 6 | use serde::de::Error as DeError; 7 | use serde::de::{Deserialize, Deserializer, MapAccess, Unexpected, Visitor}; 8 | use serde::ser::{Serialize, SerializeStruct, Serializer}; 9 | use std::fmt; 10 | 11 | #[derive(Clone, Debug, PartialEq)] 12 | pub struct Response { 13 | /// The final url of this response. 14 | pub url: Url, 15 | 16 | /// Status code. 17 | pub status: StatusCode, 18 | 19 | /// Headers 20 | pub headers: HeaderMap, 21 | 22 | /// The response body in binary format. 23 | pub body: Vec, 24 | } 25 | 26 | impl Response { 27 | pub fn body_to_utf8(&self) -> Result { 28 | Ok(String::from_utf8(self.body.clone())?) 29 | } 30 | } 31 | 32 | const N_RESPONSE: &'static str = "Response"; 33 | const F_URL: &'static str = "url"; 34 | const F_STATUS: &'static str = "status"; 35 | const F_HEADERS: &'static str = "headers"; 36 | const F_BODY: &'static str = "body"; 37 | 38 | impl Serialize for Response { 39 | fn serialize(&self, serializer: S) -> Result 40 | where 41 | S: Serializer, 42 | { 43 | let mut res = serializer.serialize_struct(N_RESPONSE, 5)?; 44 | 45 | res.serialize_field(F_URL, self.url.as_ref())?; 46 | // TODO: actually the docs for this are hidden 47 | res.serialize_field(F_STATUS, &u16::from(self.status.clone()))?; 48 | res.serialize_field(F_HEADERS, &::helper::serialize_headers(&self.headers))?; 49 | res.serialize_field(F_BODY, &base64::encode(&self.body))?; 50 | 51 | res.end() 52 | } 53 | } 54 | 55 | #[derive(Deserialize)] 56 | #[serde(field_identifier, rename_all = "lowercase")] 57 | enum Field { 58 | Url, 59 | Status, 60 | Headers, 61 | Body, 62 | } 63 | 64 | struct ResponseVisitor {} 65 | 66 | impl<'de> Visitor<'de> for ResponseVisitor { 67 | type Value = Response; 68 | 69 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 70 | formatter.write_str("struct Response") 71 | } 72 | 73 | fn visit_map(self, mut map: V) -> Result 74 | where 75 | V: MapAccess<'de>, 76 | { 77 | let mut url = None; 78 | let mut status = None; 79 | let mut headers = None; 80 | let mut body = None; 81 | 82 | while let Some(key) = map.next_key()? { 83 | match key { 84 | Field::Url => { 85 | if url.is_some() { 86 | return Err(DeError::duplicate_field(F_URL)); 87 | } 88 | let s: String = map.next_value()?; 89 | url = Some(Url::parse(s.as_ref()).map_err(|_| { 90 | DeError::invalid_value(Unexpected::Str(s.as_ref()), &F_URL) 91 | })?); 92 | } 93 | Field::Status => { 94 | if status.is_some() { 95 | return Err(DeError::duplicate_field(F_STATUS)); 96 | } 97 | let s: u16 = map.next_value()?; 98 | status = Some(StatusCode::from_u16(s).map_err(|_| { 99 | DeError::invalid_value(Unexpected::Unsigned(s as u64), &"StatusCode") 100 | })?); 101 | } 102 | Field::Headers => { 103 | if headers.is_some() { 104 | return Err(DeError::duplicate_field(F_HEADERS)); 105 | } 106 | headers = Some(::helper::deserialize_headers(&map.next_value()?)); 107 | } 108 | Field::Body => { 109 | if body.is_some() { 110 | return Err(DeError::duplicate_field(F_BODY)); 111 | } 112 | let s: String = map.next_value()?; 113 | body = Some(base64::decode(&s).map_err(|_| { 114 | DeError::invalid_value(Unexpected::Str(s.as_ref()), &F_BODY) 115 | })?); 116 | } 117 | } 118 | } 119 | 120 | Ok(Response { 121 | url: url.ok_or_else(|| DeError::missing_field(F_URL))?, 122 | status: status.ok_or_else(|| DeError::missing_field(F_STATUS))?, 123 | headers: headers.ok_or_else(|| DeError::missing_field(F_HEADERS))?, 124 | body: body.ok_or_else(|| DeError::missing_field(F_BODY))?, 125 | }) 126 | } 127 | } 128 | 129 | impl From for HttpResponse> { 130 | fn from(r: Response) -> HttpResponse> { 131 | let mut http_rsp = HttpResponse::builder().status(r.status); 132 | let headers = http_rsp.headers_mut().unwrap(); 133 | for (key, value) in r.headers { 134 | if let Some(k) = key { 135 | headers.append(&k, value); 136 | } 137 | } 138 | http_rsp.body(r.body).unwrap() 139 | } 140 | } 141 | 142 | impl<'de> Deserialize<'de> for Response { 143 | fn deserialize(deserializer: D) -> Result 144 | where 145 | D: Deserializer<'de>, 146 | { 147 | const FIELDS: &'static [&'static str] = &[F_URL, F_STATUS, F_HEADERS, F_BODY]; 148 | deserializer.deserialize_struct(N_RESPONSE, FIELDS, ResponseVisitor {}) 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use super::*; 155 | use reqwest::header::{CONTENT_LENGTH, USER_AGENT}; 156 | 157 | fn dummy_response() -> Response { 158 | 159 | let mut headers = HeaderMap::new(); 160 | headers.insert(CONTENT_LENGTH, 2000.into()); 161 | headers.insert(USER_AGENT, "Testing Code".parse().unwrap()); 162 | 163 | Response { 164 | url: Url::parse("http://example.com/index.html").unwrap(), 165 | status: StatusCode::OK, 166 | headers: headers, 167 | body: vec![2, 4, 8, 16, 32, 64, 42], 168 | } 169 | } 170 | 171 | #[test] 172 | fn serde() { 173 | let resp1 = dummy_response(); 174 | let json = ::serde_json::to_string(&resp1).unwrap(); 175 | 176 | let resp2 = ::serde_json::from_str(json.as_ref()).unwrap(); 177 | assert_eq!(resp1, resp2); 178 | } 179 | 180 | #[test] 181 | fn http_response() { 182 | let resp = dummy_response(); 183 | let http_resp = HttpResponse::>::from(resp); 184 | 185 | assert_eq!(http_resp.status(), http::StatusCode::OK); 186 | assert_eq!(http_resp.headers().get(CONTENT_LENGTH).unwrap().to_str().unwrap(), "2000"); 187 | assert_eq!(http_resp.headers().get(USER_AGENT).unwrap(), "Testing Code"); 188 | assert_eq!(http_resp.body(), &vec![2u8, 4, 8, 16, 32, 64, 42]); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/client/replay.rs: -------------------------------------------------------------------------------- 1 | use client::{Client, DirectClient}; 2 | use config::ClientConfig; 3 | use error::Error; 4 | use request::{Request, RequestMem}; 5 | use response::Response; 6 | 7 | use std::fs::{create_dir_all, File}; 8 | use std::hash::{Hash, Hasher}; 9 | use std::path::PathBuf; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use twox_hash::XxHash; 12 | 13 | /// The version of the storage format. The code is only compatible with files of the same version, 14 | /// everything else will be discarded and recorded again. 15 | const FORMAT_VERSION: u8 = 3; 16 | 17 | /// The recording target. 18 | pub enum RecordingTarget { 19 | /// A single file is used for recording one request, if the request changes the file is 20 | /// replaced by a new one. 21 | File(PathBuf), 22 | 23 | /// A directory is used in which multiple replay files are managed for each request data 24 | /// an individual file is created. 25 | Dir(PathBuf), 26 | } 27 | 28 | impl RecordingTarget { 29 | /// Shorthand to specify `RecordingTarget::File`. 30 | pub fn file>(file: P) -> Self { 31 | RecordingTarget::File(file.into()) 32 | } 33 | 34 | /// Shorthand to specify `RecordingTarget::Dir`. 35 | pub fn dir>(dir: P) -> Self { 36 | RecordingTarget::Dir(dir.into()) 37 | } 38 | } 39 | 40 | /// Records responses to requests and replays them if the request is unchanged. 41 | pub struct ReplayClient { 42 | config: ClientConfig, 43 | target: RecordingTarget, 44 | force_record_next: AtomicBool, 45 | } 46 | 47 | impl ReplayClient { 48 | /// Create a new `ReplayClient` instance reading and writing to the specified target. 49 | pub fn new(target: RecordingTarget) -> Self { 50 | ReplayClient { 51 | config: ClientConfig::default(), 52 | target: target, 53 | force_record_next: AtomicBool::new(false), 54 | } 55 | } 56 | 57 | /// Calling this method ensures that whatever next request is performed it will be recorded 58 | /// again, even the exact same request was already made before. 59 | pub fn force_record_next(&self) { 60 | self.force_record_next.store(true, Ordering::SeqCst); 61 | } 62 | 63 | fn replay_file_path(&self, request: &RequestMem) -> PathBuf { 64 | match self.target { 65 | RecordingTarget::File(ref file) => file.clone(), 66 | RecordingTarget::Dir(ref dir) => { 67 | // TODO: I took this hash function as unlike DefaultHasher it is specified. 68 | // However more evaluation should be done before settling on this 69 | // one as the hasher for the stable release. 70 | let mut hasher = XxHash::with_seed(42); 71 | request.hash(&mut hasher); 72 | let filename = format!("{:x}.json", hasher.finish()); 73 | 74 | dir.join(filename) 75 | } 76 | } 77 | } 78 | 79 | /// The possible results: 80 | /// 81 | /// Err(_) → something went wrong. 82 | /// Ok(None) → no data was stored yet, i. e. the file doesn't exist yet. 83 | /// Ok(Some(_)) → the actual data 84 | fn get_data(&self, request: &RequestMem) -> Result, Error> { 85 | let file = self.replay_file_path(request); 86 | let force_record = self.force_record_next.swap(false, Ordering::SeqCst); 87 | debug!("Checking presence of replay file: {:?}", file); 88 | 89 | if !file.exists() { 90 | debug!("Existing replay file was found."); 91 | Ok(None) 92 | } else if force_record { 93 | debug!("Replay file exists but force record was requested."); 94 | Ok(None) 95 | } else { 96 | use serde_json::Value; 97 | 98 | debug!("Reading existing replay file."); 99 | let f = File::open(&file)?; 100 | let value: Value = ::serde_json::from_reader(f)?; 101 | 102 | // Check the format version. 103 | let format_version = match value { 104 | Value::Object(ref obj) => obj 105 | .get("format_version") 106 | .and_then(|val| val.as_u64()) 107 | .map(|n| n as u8), 108 | _ => None, 109 | }; 110 | 111 | if format_version == Some(FORMAT_VERSION) { 112 | Ok(::serde_json::from_value(value)?) 113 | } else { 114 | debug!( 115 | "Replay file exists but has wrong format version: {:?}", 116 | format_version 117 | ); 118 | Ok(None) 119 | } 120 | } 121 | } 122 | 123 | fn store_data(&self, data: &ReplayData) -> Result<(), Error> { 124 | let file = self.replay_file_path(&data.request); 125 | debug!("Writing replay file at: {:?}", file); 126 | 127 | // Attempt to create the directory of the file if it doesn't exist yet. 128 | if let Some(parent) = file.parent() { 129 | if !parent.exists() { 130 | create_dir_all(parent)?; 131 | } 132 | } 133 | 134 | // Write the file. 135 | let f = File::create(&file)?; 136 | ::serde_json::to_writer(f, data)?; 137 | Ok(()) 138 | } 139 | } 140 | 141 | impl Client for ReplayClient { 142 | fn execute(&self, config: Option<&ClientConfig>, request: Request) -> Result { 143 | let req: RequestMem = request.to_mem()?; 144 | 145 | // Some information potentially useful for debugging. 146 | debug!( 147 | "ReplayClient performing {} request of URL: {}", 148 | req.header.method, req.header.url 149 | ); 150 | trace!("request headers: {:?}", req.header.headers); 151 | trace!("request body: {:?}", req.body); 152 | 153 | // Use internal config if none was provided together with the request. 154 | let config = config.unwrap_or_else(|| &self.config); 155 | 156 | // Check if the request was already performed with this exact arguments, 157 | // if it was just return the existing result otherwise perform the request and store 158 | // the output. 159 | 160 | let data = self.get_data(&req)?; 161 | if let Some(d) = data { 162 | if d.request == req { 163 | return Ok(d.response); 164 | } else { 165 | // TODO better message 166 | info!("reqwest_mock: Request has changed, recording again now."); 167 | } 168 | } 169 | 170 | // We actually have to perform the request and store the response. 171 | let client = DirectClient::new(); 172 | let response = client.execute(Some(config), req.clone().into())?; 173 | 174 | self.store_data(&ReplayData { 175 | request: req, 176 | response: response.clone(), 177 | format_version: FORMAT_VERSION, 178 | })?; 179 | 180 | // Return the response. 181 | Ok(response) 182 | } 183 | 184 | fn config(&self) -> &ClientConfig { 185 | &self.config 186 | } 187 | 188 | fn config_mut(&mut self) -> &mut ClientConfig { 189 | &mut self.config 190 | } 191 | } 192 | 193 | /// The data stored inside of a replay file. 194 | #[derive(Debug, Serialize, Deserialize)] 195 | struct ReplayData { 196 | request: RequestMem, 197 | response: Response, 198 | format_version: u8, 199 | } 200 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use body::Body; 2 | use http::Request as HttpRequest; 3 | use reqwest::header::HeaderMap; 4 | use reqwest::{Method, Url}; 5 | use serde::de::Error as DeError; 6 | use serde::de::{Deserialize, Deserializer, MapAccess, Unexpected, Visitor}; 7 | use serde::ser::{Serialize, SerializeStruct, Serializer}; 8 | use std::fmt; 9 | use std::hash::{Hash, Hasher}; 10 | use std::str::FromStr; 11 | 12 | #[derive(Clone, Debug, PartialEq)] 13 | pub struct RequestHeader { 14 | pub url: Url, 15 | pub method: Method, 16 | pub headers: HeaderMap, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct Request { 21 | pub header: RequestHeader, 22 | pub body: Option, 23 | } 24 | 25 | impl Request { 26 | pub(crate) fn to_mem(self) -> Result { 27 | Ok(RequestMem { 28 | header: self.header, 29 | body: match self.body { 30 | Some(b) => Some(b.try_to_vec()?), 31 | None => None, 32 | }, 33 | }) 34 | } 35 | } 36 | 37 | impl From> for Request where T: Into { 38 | fn from(r: HttpRequest) -> Self { 39 | let header = RequestHeader { 40 | // TODO: Handle error when converting. 41 | // Potentially https://github.com/seanmonstar/reqwest/issues/668 would provide a solution in the future. 42 | url: Url::parse(&format!("{}", r.uri())).unwrap(), 43 | method: r.method().clone(), 44 | headers: r.headers().clone(), 45 | }; 46 | 47 | Request { 48 | header, 49 | body: Some(r.into_body().into()), 50 | } 51 | } 52 | } 53 | 54 | #[derive(Clone, Debug, PartialEq)] 55 | pub struct RequestMem { 56 | pub header: RequestHeader, 57 | pub body: Option>, 58 | } 59 | 60 | impl From for Request { 61 | fn from(r: RequestMem) -> Self { 62 | Request { 63 | header: r.header, 64 | body: r.body.map(Body::from), 65 | } 66 | } 67 | } 68 | 69 | /// We need this so we can generate unique filenames for each request. 70 | impl Hash for RequestMem { 71 | fn hash(&self, state: &mut H) { 72 | self.header.url.hash(state); 73 | self.header.method.hash(state); 74 | ::helper::serialize_headers(&self.header.headers).hash(state); 75 | self.body.hash(state); 76 | } 77 | } 78 | 79 | impl Serialize for RequestMem { 80 | fn serialize(&self, serializer: S) -> Result 81 | where 82 | S: Serializer, 83 | { 84 | let mut req = serializer.serialize_struct("Request", 4)?; 85 | 86 | req.serialize_field("url", self.header.url.as_ref())?; 87 | req.serialize_field("method", self.header.method.as_ref())?; 88 | req.serialize_field("body", &self.body)?; 89 | req.serialize_field( 90 | "headers", 91 | &::helper::serialize_headers(&self.header.headers), 92 | )?; 93 | 94 | req.end() 95 | } 96 | } 97 | 98 | impl<'de> Deserialize<'de> for RequestMem { 99 | fn deserialize(deserializer: D) -> Result 100 | where 101 | D: Deserializer<'de>, 102 | { 103 | #[derive(Deserialize)] 104 | #[serde(field_identifier, rename_all = "lowercase")] 105 | enum Field { 106 | Url, 107 | Method, 108 | Body, 109 | Headers, 110 | } 111 | 112 | struct RequestVisitor {} 113 | 114 | impl<'de> Visitor<'de> for RequestVisitor { 115 | type Value = RequestMem; 116 | 117 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 118 | formatter.write_str("struct Request") 119 | } 120 | 121 | fn visit_map(self, mut map: V) -> Result 122 | where 123 | V: MapAccess<'de>, 124 | { 125 | let mut url = None; 126 | let mut method = None; 127 | let mut body = None; 128 | let mut headers = None; 129 | 130 | while let Some(key) = map.next_key()? { 131 | match key { 132 | Field::Url => { 133 | if url.is_some() { 134 | return Err(DeError::duplicate_field("url")); 135 | } 136 | let s: String = map.next_value()?; 137 | url = Some(Url::parse(s.as_ref()).map_err(|_| { 138 | DeError::invalid_value(Unexpected::Str(s.as_ref()), &"url") 139 | })?); 140 | } 141 | Field::Method => { 142 | if method.is_some() { 143 | return Err(DeError::duplicate_field("method")); 144 | } 145 | let s: String = map.next_value()?; 146 | method = Some(Method::from_str(s.as_ref()).map_err(|_| { 147 | DeError::invalid_value(Unexpected::Str(s.as_ref()), &"method") 148 | })?); 149 | } 150 | Field::Body => { 151 | if body.is_some() { 152 | return Err(DeError::duplicate_field("body")); 153 | } 154 | body = map.next_value()?; 155 | } 156 | Field::Headers => { 157 | if headers.is_some() { 158 | return Err(DeError::duplicate_field("headers")); 159 | } 160 | headers = Some(::helper::deserialize_headers(&map.next_value()?)); 161 | } 162 | } 163 | } 164 | 165 | Ok(RequestMem { 166 | header: RequestHeader { 167 | url: url.ok_or_else(|| DeError::missing_field("url"))?, 168 | method: method.ok_or_else(|| DeError::missing_field("method"))?, 169 | headers: headers.ok_or_else(|| DeError::missing_field("headers"))?, 170 | }, 171 | body: body, 172 | }) 173 | } 174 | } 175 | 176 | const FIELDS: &'static [&'static str] = &["url", "method", "body"]; 177 | deserializer.deserialize_struct("Request", FIELDS, RequestVisitor {}) 178 | } 179 | } 180 | 181 | #[cfg(test)] 182 | mod tests { 183 | use super::*; 184 | 185 | #[test] 186 | fn serde() { 187 | use reqwest::header::{CONTENT_LENGTH, USER_AGENT}; 188 | 189 | let mut headers = HeaderMap::new(); 190 | headers.insert(CONTENT_LENGTH, 2000.into()); 191 | headers.insert(USER_AGENT, "Testing Code".parse().unwrap()); 192 | 193 | let req1 = RequestMem { 194 | header: RequestHeader { 195 | url: Url::parse("https://example.com").unwrap(), 196 | method: Method::GET, 197 | headers: headers, 198 | }, 199 | body: Some(vec![2, 4, 11, 32, 99, 1, 4, 5]), 200 | }; 201 | 202 | let json = ::serde_json::to_string(&req1).unwrap(); 203 | let req2 = ::serde_json::from_str(json.as_ref()).unwrap(); 204 | assert_eq!(req1, req2); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/client/stub/mod.rs: -------------------------------------------------------------------------------- 1 | use body::Body; 2 | use client::Client; 3 | use config::ClientConfig; 4 | use error::Error; 5 | use request::{Request, RequestHeader}; 6 | use reqwest::header::HeaderMap; 7 | use reqwest::{Method, StatusCode, Url}; 8 | use response::Response; 9 | use std::collections::{BTreeMap, HashMap}; 10 | 11 | mod settings; 12 | pub use self::settings::{StubDefault, StubSettings, StubStrictness}; 13 | 14 | mod builder; 15 | pub use self::builder::{RequestStubber, ResponseStubber}; 16 | 17 | mod error; 18 | pub use self::error::RegisterStubError; 19 | // TODO should not be public 20 | pub use self::error::FieldError; 21 | 22 | #[derive(Hash, PartialEq, Eq)] 23 | struct StubKey { 24 | url: Url, 25 | method: Option, 26 | body: Option>, 27 | headers: Option>, 28 | } 29 | 30 | struct StubRequest { 31 | url: Url, 32 | method: Option, 33 | body: Option, 34 | headers: Option>, 35 | } 36 | 37 | impl StubRequest { 38 | fn try_to_key(self) -> Result { 39 | Ok(StubKey { 40 | url: self.url, 41 | method: self.method, 42 | body: match self.body { 43 | Some(b) => Some(b.try_to_vec()?), 44 | None => None, 45 | }, 46 | headers: self.headers, 47 | }) 48 | } 49 | } 50 | 51 | struct StubResponse { 52 | status_code: StatusCode, 53 | body: Option, 54 | headers: HeaderMap, 55 | } 56 | 57 | /// A client which allows you to stub out the response to a request explicitly. 58 | /// 59 | /// # Examples 60 | /// ``` 61 | /// use reqwest_mock::{Client, Method, StubClient, StubDefault, StubSettings, StubStrictness, Url}; 62 | /// 63 | /// let mut client = StubClient::new(StubSettings { 64 | /// // If a request without a corresponding stub is made we want an error 65 | /// // to be returned when our code executes the request. 66 | /// default: StubDefault::Error, 67 | /// 68 | /// // We want the `StubClient` to compare actual requests and provided 69 | /// // mocks by their method and their url. 70 | /// strictness: StubStrictness::MethodUrl, 71 | /// }); 72 | /// 73 | /// // Mock a request. 74 | /// client 75 | /// .stub(Url::parse("http://example.com/mocking").unwrap()) 76 | /// .method(Method::GET) 77 | /// .response() 78 | /// .body("Mocking is fun!") 79 | /// .mock(); 80 | /// 81 | /// let response = client.get("http://example.com/mocking").send().unwrap(); 82 | /// assert_eq!(response.body_to_utf8().unwrap(), "Mocking is fun!".to_string()); 83 | /// ``` 84 | pub struct StubClient { 85 | config: ClientConfig, 86 | stubs: HashMap, 87 | settings: StubSettings, 88 | } 89 | 90 | impl StubClient { 91 | /// Create a new instance of `StubClient`. 92 | /// 93 | /// Please consult [StubSettings](struct.StubSettings.html) for more information about the 94 | /// possible settings. 95 | pub fn new(stub_settings: StubSettings) -> Self { 96 | StubClient { 97 | config: ClientConfig::default(), 98 | stubs: HashMap::new(), 99 | settings: stub_settings, 100 | } 101 | } 102 | 103 | /// Provide a stub for a request to the provided url. 104 | /// 105 | /// This will return a [RequestStubber](struct.RequestStubber.html), which in a first step will 106 | /// allow you to specify the full details of the request. Make sure that they match the 107 | /// [StubStrictness](struct.StubStrictness.html) provided in the settings. 108 | /// 109 | /// After you are finished specifying the details of the matching request, call `response()` to 110 | /// return a `ResponseStubber` instance and start specifying the response. Finally use 111 | /// `ResponseStubber::mock()` to register the mock into the client. 112 | pub fn stub<'cl>(&'cl mut self, url: Url) -> RequestStubber<'cl> { 113 | RequestStubber::new(self, url) 114 | } 115 | 116 | /// Return the appropriate `StubKey` for the provided request. 117 | fn stub_key(&self, header: &RequestHeader, body: &Option>) -> StubKey { 118 | match self.settings.strictness { 119 | StubStrictness::Full => StubKey { 120 | url: header.url.clone(), 121 | method: Some(header.method.clone()), 122 | body: body.clone(), 123 | headers: Some(::helper::serialize_headers(&header.headers)), 124 | }, 125 | StubStrictness::BodyMethodUrl => StubKey { 126 | url: header.url.clone(), 127 | method: Some(header.method.clone()), 128 | body: body.clone(), 129 | headers: None, 130 | }, 131 | StubStrictness::HeadersMethodUrl => StubKey { 132 | url: header.url.clone(), 133 | method: Some(header.method.clone()), 134 | body: None, 135 | headers: Some(::helper::serialize_headers(&header.headers)), 136 | }, 137 | StubStrictness::MethodUrl => StubKey { 138 | url: header.url.clone(), 139 | method: Some(header.method.clone()), 140 | body: None, 141 | headers: None, 142 | }, 143 | StubStrictness::Url => StubKey { 144 | url: header.url.clone(), 145 | method: None, 146 | body: None, 147 | headers: None, 148 | }, 149 | } 150 | } 151 | 152 | pub(self) fn register_stub( 153 | &mut self, 154 | key: StubKey, 155 | value: StubResponse, 156 | ) -> Result<(), RegisterStubError> { 157 | // Check if stub key contains the nescessary fields. 158 | macro_rules! validate_sk_field { 159 | (Some $field:ident $strictness:path) => { 160 | if key.$field.is_none() { 161 | return Err(RegisterStubError::MissingField(FieldError { 162 | field_name: stringify!($field), 163 | strictness: stringify!($strictness), 164 | })); 165 | } 166 | }; 167 | (None $field:ident $strictness:path) => { 168 | if key.$field.is_some() { 169 | return Err(RegisterStubError::UnescessaryField(FieldError { 170 | field_name: stringify!($field), 171 | strictness: stringify!($strictness), 172 | })); 173 | } 174 | }; 175 | } 176 | 177 | macro_rules! validate_sk_fields { 178 | ( $strictness:path; $($sn:tt $field:ident),* ) 179 | => ( $( validate_sk_field!($sn $field $strictness); )* ) 180 | } 181 | 182 | match self.settings.strictness { 183 | StubStrictness::Full => { 184 | validate_sk_fields!(StubStrictness::Full; Some method, Some body, Some headers); 185 | } 186 | StubStrictness::BodyMethodUrl => { 187 | validate_sk_fields!(StubStrictness::BodyMethodUrl; Some method, Some body, None headers); 188 | } 189 | StubStrictness::HeadersMethodUrl => { 190 | validate_sk_fields!(StubStrictness::HeadersMethodUrl; Some method, None body, Some headers); 191 | } 192 | StubStrictness::MethodUrl => { 193 | validate_sk_fields!(StubStrictness::MethodUrl; Some method, None body, None headers); 194 | } 195 | StubStrictness::Url => { 196 | validate_sk_fields!(StubStrictness::Url; None method, None body, None headers); 197 | } 198 | } 199 | 200 | // Register the response. 201 | let response = Response { 202 | url: key.url.clone(), 203 | status: value.status_code, 204 | headers: value.headers, 205 | body: value 206 | .body 207 | .map(|b| b.try_to_vec()) 208 | .unwrap_or_else(|| Ok(Vec::new())) 209 | .map_err(|e| RegisterStubError::ReadFile(e))?, 210 | }; 211 | self.stubs.insert(key, response); 212 | Ok(()) 213 | } 214 | } 215 | 216 | impl Client for StubClient { 217 | fn execute(&self, config: Option<&ClientConfig>, request: Request) -> Result { 218 | // Check if there is a recorded stub for the request. 219 | let header = request.header; 220 | let body = match request.body { 221 | Some(b) => Some(b.try_to_vec()?), 222 | None => None, 223 | }; 224 | 225 | let key = self.stub_key(&header, &body); 226 | match self.stubs.get(&key) { 227 | Some(resp) => Ok(resp.clone()), 228 | None => { 229 | match self.settings.default { 230 | StubDefault::Panic => { 231 | // TODO provide more diagonistics using log crate. 232 | panic!( 233 | "Requested {}, without having provided a stub for it.", 234 | header.url 235 | ); 236 | } 237 | StubDefault::Error => { 238 | // TODO provide more diagonistics using log crate. 239 | Err(format!( 240 | "Requested {}, without having provided a stub for it.", 241 | header.url 242 | ) 243 | .into()) 244 | } 245 | StubDefault::PerformRequest => { 246 | use client::DirectClient; 247 | let client = DirectClient::new(); 248 | let request = Request { 249 | header: header, 250 | body: body.map(Body::from), 251 | }; 252 | client.execute(config, request) 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | fn config(&self) -> &ClientConfig { 260 | &self.config 261 | } 262 | 263 | fn config_mut(&mut self) -> &mut ClientConfig { 264 | &mut self.config 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------