// use std::{marker::PhantomData, sync::RwLock}; use reqwest::Client; use super::{Error, Result}; /// Marker trait used to identify whether a [Net] is mocked or not. pub trait NetType {} /// Marker struct that indicates that a [Net] is a mock. #[derive(Debug)] pub struct Mocked; impl NetType for Mocked {} /// Market struct that indicates that a [Net] is not mocked. #[derive(Debug)] pub struct Unmocked; impl NetType for Unmocked {} /// A list of planned requests and responses type Plans = Vec; /// The different ways to match a request. #[derive(Debug, PartialEq, Eq)] pub enum MatchOn { /// The request must have a specific HTTP Request Method. Method, /// The request must have a specific URL. Url, /// The request must have a specify HTTP Body. Body, /// The request must have a specific set of HTTP Headers. Headers, } /// A planned request and the response to return /// /// Contains a list of the criteria that a request must meet before being considered a match. #[derive(Debug)] struct Plan { request: reqwest::Request, response: reqwest::Response, match_on: Vec, } /// An abstraction for the network #[derive(Debug)] pub struct Net { inner: InnerNet, mock: Option>, } impl Net { /// Creates a new unmocked [Net] for creating real network requests. pub(super) const fn new() -> Self { Self { inner: InnerNet::::new(), mock: None, } } /// Creats a new [MockNet] for use in tests. pub(super) const fn mock() -> MockNet { MockNet { inner: InnerNet::::new(), } } } impl Net { /// Helper to create a default [reqwest::Client]. /// /// # Example /// /// ```rust /// # use kxio::net::Result; /// let net = kxio::net::new(); /// let client = net.client(); /// let request = client.get("https://hyper.rs"); /// ``` pub fn client(&self) -> reqwest::Client { Default::default() } /// Constructs the Request and sends it to the target URL, returning a /// future Response. /// /// # Errors /// /// This method fails if there was an error while sending request, /// redirect loop was detected or redirect limit was exhausted. /// /// # Example /// /// ```no_run /// # use kxio::net::Result; /// # async fn run() -> Result<()> { /// let net = kxio::net::new(); /// let request = net.client().get("https://hyper.rs"); /// let response = net.send(request).await?; /// # Ok(()) /// # } /// ``` pub async fn send( &self, request: impl Into, ) -> Result { match &self.mock { Some(mock) => mock.send(request).await, None => self.inner.send(request).await, } } } impl Default for Net { fn default() -> Self { Self { inner: InnerNet::::new(), mock: None, } } } impl TryFrom for MockNet { type Error = super::Error; fn try_from(net: Net) -> std::result::Result { match net.mock { Some(inner_mock) => Ok(MockNet { inner: inner_mock }), None => Err(Self::Error::NetIsNotAMock), } } } /// A struct for defining the expected requests and their responses that should be made /// during a test. /// /// When the [MockNet] goes out of scope it will verify that all expected requests were consumed, /// otherwise it will `panic`. /// /// # Example /// /// ```rust /// # use kxio::net::Result; /// # fn run() -> Result<()> { /// let mock_net = kxio::net::mock(); /// let client = mock_net.client(); /// // define an expected requet, and the response that should be returned /// mock_net.on(client.get("https://hyper.rs")) /// .respond_with_body(mock_net.response().status(200).body("Ok"))?; /// let net: kxio::net::Net = mock_net.into(); /// // use 'net' in your program, by passing it as a reference /// /// // In some rare cases you don't want to assert that all expected requests were made. /// // You should recover the `MockNet` from the `Net` and `MockNet::reset` it. /// let mock_net = kxio::net::MockNet::try_from(net)?; /// mock_net.reset(); // only if explicitly needed /// # Ok(()) /// # } /// ``` #[derive(Debug)] pub struct MockNet { inner: InnerNet, } impl MockNet { /// Helper to create a default [reqwest::Client]. /// /// # Example /// /// ```rust /// let mock_net = kxio::net::mock(); /// let client = mock_net.client(); /// let request = client.get("https://hyper.rs"); /// ``` pub fn client(&self) -> Client { Default::default() } /// Specify an expected request. /// /// # Example /// /// ```rust /// # use kxio::net::Result; /// # fn run() -> Result<()> { /// let mock_net = kxio::net::mock(); /// let client = mock_net.client(); /// mock_net.on(client.get("https://hyper.rs")) /// .respond_with_body(mock_net.response().status(200).body("Ok"))?; /// # Ok(()) /// # } /// ``` pub fn on(&self, request: impl Into) -> OnRequest { self.inner.on(request) } /// Creates a [http::response::Builder] to be extended and returned by a mocked network request. pub fn response(&self) -> http::response::Builder { Default::default() } /// Constructs the request and matches it against the expected requests. /// /// The first on it finds where it matches against its criteria will be taken ("consumed") and /// its response returned. /// /// The order the expected requests are scanned is the order they were declared. As each /// expected request is successfully matched against, it is removed and can't be matched /// against again. pub async fn send( &self, request: impl Into, ) -> Result { self.inner.send(request).await } /// Clears all the expected requests and responses from the [MockNet]. /// /// When the [MockNet] goes out of scope it will assert that all expected requests and /// responses were consumed. If there are any left unconsumed, then it will `panic`. /// /// # Example /// /// ```rust /// # use kxio::net::Result; /// # fn run() -> Result<()> { /// # let mock_net = kxio::net::mock(); /// # let net: kxio::net::Net = mock_net.into(); /// let mock_net = kxio::net::MockNet::try_from(net)?; /// mock_net.reset(); // only if explicitly needed /// # Ok(()) /// # } /// ``` pub fn reset(&self) -> Result<()> { self.inner.reset() } } impl From for Net { fn from(mock_net: MockNet) -> Self { Self { inner: InnerNet::::new(), // keep the original `inner` around to allow it's Drop impelmentation to run when we go // out of scope at the end of the test mock: Some(mock_net.inner), } } } /// Part of the inner workings of [Net] and [MockNet]. /// /// Holds the list of expected requests for [MockNet]. #[derive(Debug)] pub struct InnerNet { _type: PhantomData, plans: RwLock, } impl InnerNet { const fn new() -> Self { Self { _type: PhantomData, plans: RwLock::new(vec![]), } } pub async fn send( &self, request: impl Into, ) -> Result { request.into().send().await.map_err(Error::from) } } impl InnerNet { const fn new() -> Self { Self { _type: PhantomData, plans: RwLock::new(vec![]), } } pub async fn send( &self, request: impl Into, ) -> Result { let request = request.into().build()?; let read_plans = self.plans.read().map_err(|_| Error::RwLockLocked)?; let index = read_plans.iter().position(|plan| { // METHOD (if plan.match_on.contains(&MatchOn::Method) { plan.request.method() == request.method() } else { true }) // URL && (if plan.match_on.contains(&MatchOn::Url) { plan.request.url() == request.url() } else { true }) // BODY && (if plan.match_on.contains(&MatchOn::Body) { match (plan.request.body(), request.body()) { (None, None) => true, (Some(plan), Some(req)) => plan.as_bytes().eq(&req.as_bytes()), _ => false, } } else { true }) // HEADERS && (if plan.match_on.contains(&MatchOn::Headers) { plan.request.headers() == request.headers() } else { true }) }); drop(read_plans); match index { Some(i) => { let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?; Ok(write_plans.remove(i).response) } None => Err(Error::UnexpectedMockRequest(request)), } } pub fn on(&self, request_builder: impl Into) -> OnRequest { match request_builder.into().build() { Ok(request) => OnRequest::Valid { net: self, request, match_on: vec![MatchOn::Method, MatchOn::Url], }, Err(err) => OnRequest::Error(err.into()), } } fn _on( &self, request: reqwest::Request, response: reqwest::Response, match_on: Vec, ) -> Result<()> { let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?; write_plans.push(Plan { request, response, match_on, }); Ok(()) } pub fn reset(&self) -> Result<()> { let mut write_plans = self.plans.write().map_err(|_| Error::RwLockLocked)?; write_plans.clear(); Ok(()) } } impl Drop for InnerNet { fn drop(&mut self) { let Ok(read_plans) = self.plans.read() else { return; }; assert!(read_plans.is_empty()) } } /// Intermediate struct used while declaring an expected request and its response. pub enum OnRequest<'net> { Valid { net: &'net InnerNet, request: reqwest::Request, match_on: Vec, }, Error(super::Error), } impl<'net> OnRequest<'net> { /// Specify the criteria that a request should be compared against. /// /// Given an candidate request, it will be matched against the current request if it has the /// same HTTP Method and Url by default. i.e. if this method is not called. /// /// All criteria must be met for the match to be made. /// /// Calling this method replaces the default criteria with the new criteria. pub fn match_on(self, match_on: Vec) -> Self { if let OnRequest::Valid { net, request, match_on: _, } = self { Self::Valid { net, request, match_on, } } else { self } } /// Constructs the response to be returned when a request matched the criteria. /// /// The response will have an empty HTTP Body. /// /// Each request and response can only be matched once each. pub fn respond(self, response: impl Into) -> Result<()> { match self { OnRequest::Valid { net: _, request: _, match_on: _, } => self.respond_with_body(response.into().body("")), OnRequest::Error(error) => Err(error), } } /// Constructs the response to be returned when a request matched the criteria. /// /// Each request and response can only be matched once each. pub fn respond_with_body( self, body_result: std::result::Result, http::Error>, ) -> Result<()> where T: Into, { match body_result { Ok(response) => match self { OnRequest::Valid { net, request, match_on, } => net._on(request, response.into(), match_on), OnRequest::Error(error) => Err(error), }, Err(err) => Err(err.into()), } } }