// use std::{marker::PhantomData, ops::Deref, sync::RwLock}; 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 { self.inner.client() } pub async fn send(&self, request: reqwest::RequestBuilder) -> Result { match &self.mock { Some(mock) => mock.send(request).await, None => self.inner.send(request).await, } } } #[derive(Debug)] pub struct MockNet { inner: InnerNet, } impl Deref for MockNet { type Target = InnerNet; fn deref(&self) -> &Self::Target { &self.inner } } impl From for Net { fn from(mock_net: MockNet) -> Self { Self { inner: InnerNet::::new(), 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: reqwest::RequestBuilder) -> Result { request.send().await.map_err(Error::from) } } impl InnerNet { pub fn client(&self) -> reqwest::Client { Default::default() } } impl InnerNet { const fn new() -> Self { Self { _type: PhantomData, plans: RwLock::new(vec![]), } } pub async fn send(&self, request: reqwest::RequestBuilder) -> Result { let request = request.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)), } } /// 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 fn on(&self, request: reqwest::Request) -> OnRequest { OnRequest { net: self, request, 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: reqwest::Response) -> Result<()> { self.net._on(self.request, response, self.match_on) } }