diff --git a/examples/pagination.rs b/examples/pagination.rs index 6ff9c3d..bc7566c 100644 --- a/examples/pagination.rs +++ b/examples/pagination.rs @@ -1,7 +1,6 @@ use futures::StreamExt; -use rest_client::Client; -use rest_client::{PaginatedRequest, Paginator, QueryPaginator, Request, RequestBody}; -use rest_client::{PaginationState, PaginationType}; +use rest_client::pagination::{PaginatedRequest, PaginationState, PaginationType, QueryPaginator}; +use rest_client::{Client, Request, RequestData}; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use stream_flatten_iters::TryStreamExt; @@ -50,21 +49,23 @@ struct PassengersWrapper { } impl Request for GetPassengers { - type Body = Self; + type Data = Self; type Response = PassengersWrapper; fn endpoint(&self) -> Cow { "/v1/passenger".into() } - fn body(&self) -> RequestBody<&Self> { - RequestBody::Query(self) + fn data(&self) -> RequestData<&Self> { + RequestData::Query(self) } } impl PaginatedRequest for GetPassengers { - fn paginator(&self) -> Box> { - Box::new(QueryPaginator::new(get_next_url)) + type Paginator = QueryPaginator; + + fn paginator(&self) -> Self::Paginator { + QueryPaginator::new(get_next_url) } } diff --git a/src/client.rs b/src/client.rs index dcc96ac..07687d4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,5 +1,5 @@ use crate::error::{Error, Result}; -use crate::pagination::{PaginatedRequest, PaginationState, PaginationType}; +use crate::pagination::{PaginatedRequest, PaginationState, PaginationType, Paginator}; use crate::request::{Request, RequestBuilderExt}; use futures::prelude::*; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; @@ -8,7 +8,7 @@ use std::convert::TryFrom; use std::sync::Arc; #[derive(Clone)] -enum Authentication { +enum Authorization { Bearer(String), Basic(String, String), Query(Vec<(String, String)>), @@ -18,12 +18,12 @@ enum Authentication { /// The main client used for making requests. /// /// `Client` stores an async Reqwest client as well as the associated -/// base url for the REST server. +/// base url and possible authorization details for the REST server. #[derive(Clone)] pub struct Client { inner: Arc, base_url: String, - auth: Option, + auth: Option, } impl Client { @@ -40,13 +40,13 @@ impl Client { /// Enable bearer authentication for the client pub fn bearer_auth(mut self, token: S) -> Self { - self.auth = Some(Authentication::Bearer(token.to_string())); + self.auth = Some(Authorization::Bearer(token.to_string())); self } /// Enable basic authentication for the client pub fn basic_auth(mut self, user: S, pass: S) -> Self { - self.auth = Some(Authentication::Basic(user.to_string(), pass.to_string())); + self.auth = Some(Authorization::Basic(user.to_string(), pass.to_string())); self } @@ -56,7 +56,7 @@ impl Client { .into_iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); - self.auth = Some(Authentication::Query(pairs)); + self.auth = Some(Authorization::Query(pairs)); self } @@ -71,7 +71,7 @@ impl Client { HeaderValue::from_str(&v).expect("Failed to create HeaderValue"), ); } - self.auth = Some(Authentication::Header(map)); + self.auth = Some(Authorization::Header(map)); self } @@ -84,14 +84,14 @@ impl Client { .inner .request(R::METHOD, &url) .headers(request.headers()) - .request_body(request.body()); + .request_data(request.data()); let req = match &self.auth { None => req, - Some(Authentication::Bearer(token)) => req.bearer_auth(token), - Some(Authentication::Basic(user, pass)) => req.basic_auth(user, Some(pass)), - Some(Authentication::Query(pairs)) => req.query(&pairs), - Some(Authentication::Header(pairs)) => req.headers(pairs.clone()), + Some(Authorization::Bearer(token)) => req.bearer_auth(token), + Some(Authorization::Basic(user, pass)) => req.basic_auth(user, Some(pass)), + Some(Authorization::Query(pairs)) => req.query(&pairs), + Some(Authorization::Header(pairs)) => req.headers(pairs.clone()), }; req.build().map_err(From::from) } @@ -127,11 +127,11 @@ impl Client { requests: I, ) -> impl Stream> + Unpin + 'a where - I: Iterator + 'a, + I: IntoIterator + 'a, R: Request + 'a, { Box::pin( - stream::iter(requests) + stream::iter(requests.into_iter()) .map(move |r| self.send(r).map_into()) .filter_map(|x| x), ) diff --git a/src/lib.rs b/src/lib.rs index 32370e7..788f9de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,15 @@ //! rest-client is a library for building strongly typed REST clients, with built-in capabilites //! for authentication, various request and response types and pagination. //! -//! Inspired heavily by [ring-api](https://github.com/H2CO3/ring_api) +//! Originally inspired by [ring-api](https://github.com/H2CO3/ring_api) mod client; mod error; -mod pagination; +pub mod pagination; mod request; pub use client::Client; pub use error::Error; -pub use pagination::*; pub use request::*; +pub use reqwest::header; pub use reqwest::Method; +pub use reqwest::StatusCode; diff --git a/src/pagination.rs b/src/pagination.rs index dc89fb4..f168803 100644 --- a/src/pagination.rs +++ b/src/pagination.rs @@ -1,10 +1,14 @@ use crate::Request; #[derive(Debug, Clone)] +/// The type of pagination used for the resource. pub enum PaginationType { + /// Pagination by one or multiple query parameters. Query(Vec<(String, String)>), } +/// Base trait for paginators. Paginators can use the previous pagination state +/// and the response from the previous request to create a new pagination state. pub trait Paginator { fn next( &self, @@ -13,47 +17,53 @@ pub trait Paginator { ) -> PaginationState; } +/// Trait for any request that requires pagination. pub trait PaginatedRequest: Request { - fn paginator(&self) -> Box>; + /// The paginator used for the request. + type Paginator: Paginator; + + /// Return the associated paginator. + fn paginator(&self) -> Self::Paginator; } #[derive(Clone, Debug)] -pub enum PaginationState { +/// The current pagination state. +pub enum PaginationState { + /// State associated with the initial request. Start(Option), + /// State associated with continuing pagination. Next(T), + /// State denoting that the last page has been reached. End, } -impl Default for PaginationState { +impl Default for PaginationState { fn default() -> PaginationState { PaginationState::Start(None) } } -pub struct QueryPaginator -where - F: Fn(&PaginationState, &T) -> Option>, -{ - f: F, +/// A paginator that implements pagination through one or more query parameters. +#[allow(clippy::type_complexity)] +pub struct QueryPaginator { + f: Box, &T) -> Option>>, _phantom: std::marker::PhantomData, } -impl QueryPaginator -where - F: Fn(&PaginationState, &T) -> Option>, -{ - pub fn new(f: F) -> Self { +impl QueryPaginator { + pub fn new< + F: 'static + Fn(&PaginationState, &T) -> Option>, + >( + f: F, + ) -> Self { Self { - f, + f: Box::new(f), _phantom: std::marker::PhantomData, } } } -impl Paginator for QueryPaginator -where - F: Fn(&PaginationState, &T) -> Option>, -{ +impl Paginator for QueryPaginator { fn next( &self, prev: &PaginationState, diff --git a/src/request.rs b/src/request.rs index 94831e4..2ff6f88 100644 --- a/src/request.rs +++ b/src/request.rs @@ -2,35 +2,52 @@ use reqwest::{header::HeaderMap, Method, RequestBuilder}; use serde::{Deserialize, Deserializer, Serialize}; use std::borrow::Cow; -pub enum RequestBody { - None, - Query(T), +/// Additional data to be sent along with the request. +pub enum RequestData { + /// No additional data. + Empty, + /// HTTP form data. + Form(T), + /// JSON data. Json(T), + /// Query data. + Query(T), } -impl Default for RequestBody { +impl Default for RequestData { fn default() -> Self { - RequestBody::None + RequestData::Empty } } +/// The base-trait for requests sent by the client. The trait specifies the full life-cycle of the +/// request, including the endpoint, headers, data, method and eventual response. pub trait Request { - type Body: Serialize; + /// The type of additional data sent with the request. Usually, this will be `()` or `Self`. + type Data: Serialize; + /// The type of the response from the server. type Response: for<'de> Deserialize<'de> + Unpin; + /// The HTTP method for the request. const METHOD: Method = Method::GET; + /// The endpoint to which the request will be sent. The base url is set in the client, and the + /// endpoint method returns the specific resource endpoint. fn endpoint(&self) -> Cow; + /// Any additional headers that should be sent with the request. Note that common headers such + /// as authorization headers should be set on the client directly. fn headers(&self) -> HeaderMap { Default::default() } - fn body(&self) -> RequestBody<&Self::Body> { + /// The formatted request data. + fn data(&self) -> RequestData<&Self::Data> { Default::default() } } #[derive(Debug)] +/// Struct symbolizing an empty response from the server. pub struct EmptyResponse; impl<'de> Deserialize<'de> for EmptyResponse { fn deserialize(_deserializer: D) -> Result @@ -42,15 +59,16 @@ impl<'de> Deserialize<'de> for EmptyResponse { } pub(crate) trait RequestBuilderExt: Sized { - fn request_body(self, body: RequestBody) -> Self; + fn request_data(self, body: RequestData) -> Self; } impl RequestBuilderExt for RequestBuilder { - fn request_body(self, body: RequestBody) -> Self { + fn request_data(self, body: RequestData) -> Self { match body { - RequestBody::None => self, - RequestBody::Json(value) => self.json(&value), - RequestBody::Query(value) => self.query(&value), + RequestData::Empty => self, + RequestData::Form(value) => self.form(&value), + RequestData::Json(value) => self.json(&value), + RequestData::Query(value) => self.query(&value), } } } diff --git a/tests/integration/authentication/basic.rs b/tests/integration/authorization/basic.rs similarity index 69% rename from tests/integration/authentication/basic.rs rename to tests/integration/authorization/basic.rs index 2fd2f02..e23f26c 100644 --- a/tests/integration/authentication/basic.rs +++ b/tests/integration/authorization/basic.rs @@ -1,19 +1,8 @@ -use rest_client::{Client, EmptyResponse, Request}; -use std::borrow::Cow; +use crate::utils::EmptyHello; +use rest_client::Client; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -struct EmptyHello; - -impl Request for EmptyHello { - type Body = (); - type Response = EmptyResponse; - - fn endpoint(&self) -> Cow { - "/hello".into() - } -} - #[tokio::test] async fn basic_auth() { let server = MockServer::start().await; diff --git a/tests/integration/authentication/bearer.rs b/tests/integration/authorization/bearer.rs similarity index 67% rename from tests/integration/authentication/bearer.rs rename to tests/integration/authorization/bearer.rs index 285eab6..cc77dc7 100644 --- a/tests/integration/authentication/bearer.rs +++ b/tests/integration/authorization/bearer.rs @@ -1,19 +1,8 @@ -use rest_client::{Client, EmptyResponse, Request}; -use std::borrow::Cow; +use crate::utils::EmptyHello; +use rest_client::Client; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -struct EmptyHello; - -impl Request for EmptyHello { - type Body = (); - type Response = EmptyResponse; - - fn endpoint(&self) -> Cow { - "/hello".into() - } -} - #[tokio::test] async fn bearer_auth() { let server = MockServer::start().await; diff --git a/tests/integration/authentication/header.rs b/tests/integration/authorization/header.rs similarity index 70% rename from tests/integration/authentication/header.rs rename to tests/integration/authorization/header.rs index 2f912f8..eb6376b 100644 --- a/tests/integration/authentication/header.rs +++ b/tests/integration/authorization/header.rs @@ -1,19 +1,8 @@ -use rest_client::{Client, EmptyResponse, Request}; -use std::borrow::Cow; +use crate::utils::EmptyHello; +use rest_client::Client; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -struct EmptyHello; - -impl Request for EmptyHello { - type Body = (); - type Response = EmptyResponse; - - fn endpoint(&self) -> Cow { - "/hello".into() - } -} - #[tokio::test] async fn header_auth() { let server = MockServer::start().await; diff --git a/tests/integration/authentication/mod.rs b/tests/integration/authorization/mod.rs similarity index 100% rename from tests/integration/authentication/mod.rs rename to tests/integration/authorization/mod.rs diff --git a/tests/integration/authentication/query.rs b/tests/integration/authorization/query.rs similarity index 70% rename from tests/integration/authentication/query.rs rename to tests/integration/authorization/query.rs index 22a6ab2..3e6b399 100644 --- a/tests/integration/authentication/query.rs +++ b/tests/integration/authorization/query.rs @@ -1,19 +1,8 @@ -use rest_client::{Client, EmptyResponse, Request}; -use std::borrow::Cow; +use crate::utils::EmptyHello; +use rest_client::Client; use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; -struct EmptyHello; - -impl Request for EmptyHello { - type Body = (); - type Response = EmptyResponse; - - fn endpoint(&self) -> Cow { - "/hello".into() - } -} - #[tokio::test] async fn query_auth() { let server = MockServer::start().await; diff --git a/tests/integration/data.rs b/tests/integration/data.rs new file mode 100644 index 0000000..0b7d771 --- /dev/null +++ b/tests/integration/data.rs @@ -0,0 +1,108 @@ +use crate::utils::{FormHello, JsonHello, NameGreeting, QueryHello}; +use rest_client::Client; +use serde_json::json; +use wiremock::matchers::{body_json, body_string, header, method, path, query_param}; +use wiremock::{Mock, MockServer, Request as MockRequest, ResponseTemplate}; + +#[tokio::test] +async fn query() { + let server = MockServer::start().await; + let uri = server.uri(); + let client = Client::new(&uri); + + Mock::given(method("GET")) + .and(path("/hello")) + .and(query_param("name", "world")) + .respond_with(|req: &MockRequest| { + let name = req + .url + .query_pairs() + .find(|(k, _)| k == "name") + .map(|(_, v)| v) + .unwrap(); + let body = NameGreeting { + message: format!("Hello, {}!", name), + }; + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&server) + .await; + + let response = client + .send(&QueryHello { + name: "world".into(), + }) + .await + .unwrap(); + assert_eq!( + response, + NameGreeting { + message: "Hello, world!".into(), + } + ); +} + +#[tokio::test] +async fn json() { + let server = MockServer::start().await; + let uri = server.uri(); + let client = Client::new(&uri); + + Mock::given(method("GET")) + .and(path("/hello")) + .and(body_json(json!({"name": "world"}))) + .respond_with(|_: &MockRequest| { + let body = NameGreeting { + message: "Hello, world!".into(), + }; + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&server) + .await; + + let response = client + .send(&JsonHello { + name: "world".into(), + }) + .await + .unwrap(); + assert_eq!( + response, + NameGreeting { + message: "Hello, world!".into(), + } + ); +} + +#[tokio::test] +async fn form() { + let server = MockServer::start().await; + let uri = server.uri(); + let client = Client::new(&uri); + + Mock::given(method("GET")) + .and(path("/hello")) + .and(header("content-type", "application/x-www-form-urlencoded")) + .and(body_string("name=world")) + .respond_with(|req: &MockRequest| { + let body = NameGreeting { + message: "Hello, world!".into(), + }; + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&server) + .await; + + let response = client + .send(&FormHello { + name: "world".into(), + }) + .await + .unwrap(); + assert_eq!( + response, + NameGreeting { + message: "Hello, world!".into(), + } + ); +} diff --git a/tests/integration/empty_response.rs b/tests/integration/empty_response.rs index 6753ab5..2621144 100644 --- a/tests/integration/empty_response.rs +++ b/tests/integration/empty_response.rs @@ -1,19 +1,8 @@ -use rest_client::{Client, EmptyResponse, Request}; -use std::borrow::Cow; +use crate::utils::EmptyHello; +use rest_client::Client; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -struct EmptyHello; - -impl Request for EmptyHello { - type Body = (); - type Response = EmptyResponse; - - fn endpoint(&self) -> Cow { - "/hello".into() - } -} - #[tokio::test] async fn empty_response() { let server = MockServer::start().await; diff --git a/tests/integration/errors.rs b/tests/integration/errors.rs new file mode 100644 index 0000000..cdb4393 --- /dev/null +++ b/tests/integration/errors.rs @@ -0,0 +1,38 @@ +use crate::utils::EmptyHello; +use rest_client::{Client, Error, StatusCode}; +use wiremock::matchers::any; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +#[tokio::test] +async fn client_error() { + let server = MockServer::start().await; + let uri = server.uri(); + let client = Client::new(&uri); + + Mock::given(any()) + .respond_with(ResponseTemplate::new(404)) + .mount(&server) + .await; + + assert!(matches!( + client.send(&EmptyHello).await.unwrap_err(), + Error::ClientError(status, msg) if (status == StatusCode::NOT_FOUND && msg == String::new()) + )); +} + +#[tokio::test] +async fn server_error() { + let server = MockServer::start().await; + let uri = server.uri(); + let client = Client::new(&uri); + + Mock::given(any()) + .respond_with(ResponseTemplate::new(500)) + .mount(&server) + .await; + + assert!(matches!( + client.send(&EmptyHello).await.unwrap_err(), + Error::ServerError(status, msg) if (status == StatusCode::INTERNAL_SERVER_ERROR && msg == String::new()) + )); +} diff --git a/tests/integration/main.rs b/tests/integration/main.rs index bf8c696..16c0909 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -1,3 +1,7 @@ -mod authentication; +mod authorization; +mod data; mod empty_response; -mod query; +mod errors; +mod multiple_queries; +mod pagination; +mod utils; diff --git a/tests/integration/multiple_queries.rs b/tests/integration/multiple_queries.rs new file mode 100644 index 0000000..393dc6c --- /dev/null +++ b/tests/integration/multiple_queries.rs @@ -0,0 +1,53 @@ +use crate::utils::{NameGreeting, QueryHello}; +use futures::StreamExt; +use rest_client::Client; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, Request as MockRequest, ResponseTemplate}; + +#[tokio::test] +async fn query_multiple() { + let server = MockServer::start().await; + let uri = server.uri(); + let client = Client::new(&uri); + + Mock::given(method("GET")) + .and(path("/hello")) + .respond_with(|req: &MockRequest| { + let name = req + .url + .query_pairs() + .find(|(k, _)| k == "name") + .map(|(_, v)| v) + .unwrap(); + let body = NameGreeting { + message: format!("Hello, {}!", name), + }; + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&server) + .await; + + let reqs = &[ + QueryHello { + name: "world".into(), + }, + QueryHello { + name: "again".into(), + }, + ]; + + let mut response = client.send_all(reqs); + assert_eq!( + response.next().await.unwrap().unwrap(), + NameGreeting { + message: "Hello, world!".into(), + } + ); + assert_eq!( + response.next().await.unwrap().unwrap(), + NameGreeting { + message: "Hello, again!".into(), + } + ); + assert!(response.next().await.is_none()); +} diff --git a/tests/integration/pagination.rs b/tests/integration/pagination.rs new file mode 100644 index 0000000..f7fbbe3 --- /dev/null +++ b/tests/integration/pagination.rs @@ -0,0 +1,103 @@ +use crate::utils::matchers::MissingQuery; +use futures::StreamExt; +use rest_client::pagination::*; +use rest_client::{Client, Request, RequestData}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use wiremock::matchers::{method, path, query_param}; +use wiremock::{Mock, MockServer, Request as MockRequest, ResponseTemplate}; + +#[derive(Serialize)] +struct PaginationRequest { + page: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +struct PaginationResponse { + next_page: Option, + data: String, +} + +impl Request for PaginationRequest { + type Data = Self; + type Response = PaginationResponse; + + fn endpoint(&self) -> Cow { + "/page".into() + } + + fn data(&self) -> RequestData<&Self> { + RequestData::Query(self) + } +} + +impl PaginatedRequest for PaginationRequest { + type Paginator = QueryPaginator; + fn paginator(&self) -> Self::Paginator { + QueryPaginator::new(|_, r: &PaginationResponse| { + r.next_page + .map(|page| vec![("page".into(), format!("{}", page))]) + }) + } +} + +#[tokio::test] +async fn query() { + let server = MockServer::start().await; + let uri = server.uri(); + let client = Client::new(&uri); + + Mock::given(method("GET")) + .and(path("/page")) + .and(MissingQuery::new("page")) + .respond_with(|_: &MockRequest| { + let body = PaginationResponse { + next_page: Some(1), + data: "First!".into(), + }; + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path("/page")) + .and(query_param("page", "1")) + .respond_with(|_: &MockRequest| { + let body = PaginationResponse { + next_page: Some(2), + data: "Second!".into(), + }; + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path("/page")) + .and(query_param("page", "2")) + .respond_with(|_: &MockRequest| { + let body = PaginationResponse { + next_page: None, + data: "Last!".into(), + }; + ResponseTemplate::new(200).set_body_json(body) + }) + .mount(&server) + .await; + + let mut response = client.send_paginated(&PaginationRequest { page: None }); + assert_eq!( + response.next().await.unwrap().unwrap().data, + "First!".to_string() + ); + assert_eq!( + response.next().await.unwrap().unwrap().data, + "Second!".to_string() + ); + assert_eq!( + response.next().await.unwrap().unwrap().data, + "Last!".to_string() + ); + assert!(response.next().await.is_none()); +} diff --git a/tests/integration/query.rs b/tests/integration/query.rs deleted file mode 100644 index 9af2206..0000000 --- a/tests/integration/query.rs +++ /dev/null @@ -1,66 +0,0 @@ -use rest_client::{Client, Request, RequestBody}; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use wiremock::matchers::{method, path, query_param}; -use wiremock::{Mock, MockServer, Request as MockRequest, ResponseTemplate}; - -#[derive(Serialize)] -struct QueryHello { - name: String, -} - -#[derive(Deserialize, Serialize, Debug, PartialEq)] -struct NameGreeting { - message: String, -} - -impl Request for QueryHello { - type Body = Self; - type Response = NameGreeting; - - fn endpoint(&self) -> Cow { - "/hello".into() - } - - fn body(&self) -> RequestBody<&Self> { - RequestBody::Query(&self) - } -} - -#[tokio::test] -async fn query() { - let server = MockServer::start().await; - let uri = server.uri(); - let client = Client::new(&uri); - - Mock::given(method("GET")) - .and(path("/hello")) - .and(query_param("name", "Sebastian")) - .respond_with(|req: &MockRequest| { - let name = req - .url - .query_pairs() - .find(|(k, _)| k == "name") - .map(|(_, v)| v) - .unwrap(); - let body = NameGreeting { - message: format!("Hello, {}!", name), - }; - ResponseTemplate::new(200).set_body_json(body) - }) - .mount(&server) - .await; - - let response = client - .send(&QueryHello { - name: "Sebastian".into(), - }) - .await - .unwrap(); - assert_eq!( - response, - NameGreeting { - message: "Hello, Sebastian!".into(), - } - ); -} diff --git a/tests/integration/utils/matchers.rs b/tests/integration/utils/matchers.rs new file mode 100644 index 0000000..2d73ced --- /dev/null +++ b/tests/integration/utils/matchers.rs @@ -0,0 +1,19 @@ +use wiremock::{Match, Request}; + +pub struct MissingQuery<'a>(&'a str); + +impl<'a> MissingQuery<'a> { + pub fn new(query: &'a str) -> Self { + Self(query) + } +} + +impl Match for MissingQuery<'_> { + fn matches(&self, request: &Request) -> bool { + request + .url + .query_pairs() + .find(|(k, _)| k == self.0) + .is_none() + } +} diff --git a/tests/integration/utils/mod.rs b/tests/integration/utils/mod.rs new file mode 100644 index 0000000..2f423d5 --- /dev/null +++ b/tests/integration/utils/mod.rs @@ -0,0 +1,75 @@ +use rest_client::{EmptyResponse, Request, RequestData}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; + +pub mod matchers; + +pub struct EmptyHello; + +impl Request for EmptyHello { + type Data = (); + type Response = EmptyResponse; + + fn endpoint(&self) -> Cow { + "/hello".into() + } +} + +#[derive(Serialize)] +pub struct QueryHello { + pub name: String, +} + +#[derive(Serialize)] +pub struct JsonHello { + pub name: String, +} + +#[derive(Serialize)] +pub struct FormHello { + pub name: String, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct NameGreeting { + pub message: String, +} + +impl Request for QueryHello { + type Data = Self; + type Response = NameGreeting; + + fn endpoint(&self) -> Cow { + "/hello".into() + } + + fn data(&self) -> RequestData<&Self> { + RequestData::Query(&self) + } +} + +impl Request for JsonHello { + type Data = Self; + type Response = NameGreeting; + + fn endpoint(&self) -> Cow { + "/hello".into() + } + + fn data(&self) -> RequestData<&Self> { + RequestData::Json(&self) + } +} + +impl Request for FormHello { + type Data = Self; + type Response = NameGreeting; + + fn endpoint(&self) -> Cow { + "/hello".into() + } + + fn data(&self) -> RequestData<&Self> { + RequestData::Form(&self) + } +}