// use std::{marker::PhantomData, sync::RwLock}; use reqwest::Client; use super::{Error, Result}; pub trait NetType {} #[derive(Debug)] pub struct Mocked; impl NetType for Mocked {} #[derive(Debug)] pub struct Unmocked; impl NetType for Unmocked {} type Plans = Vec; #[derive(Debug, PartialEq, Eq)] pub enum MatchOn { Method, Url, Body, Headers, } #[derive(Debug)] struct Plan { request: reqwest::Request, response: reqwest::Response, match_on: Vec, } #[derive(Debug)] pub struct Net { inner: InnerNet, mock: Option>, } impl Net { // constructors pub(super) const fn new() -> Self { Self { inner: InnerNet::::new(), mock: None, } } pub(super) const fn mock() -> MockNet { MockNet { inner: InnerNet::::new(), } } } impl Net { // public interface pub fn client(&self) -> reqwest::Client { Default::default() } 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 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), } } } #[derive(Debug)] pub struct MockNet { inner: InnerNet, } impl MockNet { pub fn client(&self) -> Client { Default::default() } 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() } pub async fn send( &self, request: impl Into, ) -> Result { self.inner.send(request).await } 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), } } } #[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: impl Into) -> OnRequest { OnRequest { net: self, request: request.into(), match_on: vec![MatchOn::Method, MatchOn::Url], } } 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()) } } pub struct OnRequest<'net> { net: &'net InnerNet, request: reqwest::Request, match_on: Vec, } impl<'net> OnRequest<'net> { pub fn match_on(self, match_on: Vec) -> Self { Self { net: self.net, request: self.request, match_on, } } pub fn respond(self, response: impl Into) -> Result<()> { self.net._on(self.request, response.into(), self.match_on) } }