use std::{
cell::RefCell,
collections::HashMap,
fmt::{Debug, Display},
marker::PhantomData,
ops::Deref,
rc::Rc,
sync::Arc,
};
use bytes::Bytes;
use derive_more::derive::{Display, From};
use http::StatusCode;
use reqwest::{Client, RequestBuilder};
use tokio::sync::Mutex;
use url::Url;
use crate::net::{Request, Response};
use super::{Error, Result};
type Plans = Vec<Plan>;
#[derive(Debug)]
struct Plan {
match_request: Vec<MatchRequest>,
response: reqwest::Response,
}
impl Plan {
fn matches(&self, request: &Request) -> bool {
self.match_request.iter().all(|criteria| match criteria {
MatchRequest::Method(method) => request.method() == http::Method::from(method),
MatchRequest::Url(uri) => request.url() == uri,
MatchRequest::Header { name, value } => {
request
.headers()
.iter()
.any(|(request_header_name, request_header_value)| {
let Ok(request_header_value) = request_header_value.to_str() else {
return false;
};
request_header_name.as_str() == name && request_header_value == value
})
}
MatchRequest::Body(body) => {
request.body().and_then(reqwest::Body::as_bytes) == Some(body)
}
})
}
}
impl Display for Plan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for m in &self.match_request {
write!(f, "{m} ")?;
}
writeln!(f, "=> {:?}", self.response)
}
}
#[derive(Debug, Clone, Default)]
pub struct Net {
plans: Option<Arc<Mutex<RefCell<Plans>>>>,
}
impl Net {
pub(super) const fn new() -> Self {
Self { plans: None }
}
}
impl Net {
#[must_use]
pub fn client(&self) -> Client {
Default::default()
}
pub async fn send(&self, request: impl Into<RequestBuilder>) -> Result<Response> {
let Some(plans) = &self.plans else {
return request.into().send().await.map_err(Error::from);
};
let request = request.into().build()?;
eprintln!(
"? {} {} {:?}",
request.method(),
request.url(),
request.headers()
);
let index = plans
.lock()
.await
.deref()
.borrow()
.iter()
.position(|plan| plan.matches(&request));
match index {
Some(i) => {
let plan = plans.lock().await.borrow_mut().remove(i);
eprintln!("- matched: {plan}");
let response = plan.response;
if response.status().is_success() {
Ok(response)
} else {
Err(crate::net::Error::ResponseError { response })
}
}
None => Err(Error::UnexpectedMockRequest(request)),
}
}
#[must_use]
pub fn delete(&self, url: impl Into<String>) -> ReqBuilder {
ReqBuilder::new(self, NetMethod::Delete, url)
}
#[must_use]
pub fn get(&self, url: impl Into<String>) -> ReqBuilder {
ReqBuilder::new(self, NetMethod::Get, url)
}
#[must_use]
pub fn head(&self, url: impl Into<String>) -> ReqBuilder {
ReqBuilder::new(self, NetMethod::Head, url)
}
#[must_use]
pub fn patch(&self, url: impl Into<String>) -> ReqBuilder {
ReqBuilder::new(self, NetMethod::Patch, url)
}
#[must_use]
pub fn post(&self, url: impl Into<String>) -> ReqBuilder {
ReqBuilder::new(self, NetMethod::Post, url)
}
#[must_use]
pub fn put(&self, url: impl Into<String>) -> ReqBuilder {
ReqBuilder::new(self, NetMethod::Put, url)
}
}
impl MockNet {
pub async fn try_from(net: Net) -> std::result::Result<Self, super::Error> {
match &net.plans {
Some(plans) => Ok(MockNet {
plans: Rc::new(RefCell::new(plans.lock().await.take())),
}),
None => Err(super::Error::NetIsNotAMock),
}
}
}
#[derive(Debug, Clone, Display, PartialEq, Eq)]
pub enum NetMethod {
Delete,
Get,
Head,
Patch,
Post,
Put,
}
impl From<&NetMethod> for http::Method {
fn from(value: &NetMethod) -> Self {
match value {
NetMethod::Delete => http::Method::DELETE,
NetMethod::Get => http::Method::GET,
NetMethod::Head => http::Method::HEAD,
NetMethod::Patch => http::Method::PATCH,
NetMethod::Post => http::Method::POST,
NetMethod::Put => http::Method::PUT,
}
}
}
pub struct ReqBuilder<'net> {
net: &'net Net,
url: String,
method: NetMethod,
headers: Vec<(String, String)>,
body: Option<Bytes>,
}
impl<'net> ReqBuilder<'net> {
#[must_use]
fn new(net: &'net Net, method: NetMethod, url: impl Into<String>) -> Self {
Self {
net,
url: url.into(),
method,
headers: vec![],
body: None,
}
}
pub async fn send(self) -> Result<Response> {
let client = self.net.client();
let mut req = match self.method {
NetMethod::Delete => client.delete(self.url),
NetMethod::Get => client.get(self.url),
NetMethod::Head => client.head(self.url),
NetMethod::Patch => client.patch(self.url),
NetMethod::Post => client.post(self.url),
NetMethod::Put => client.put(self.url),
};
for (name, value) in self.headers.into_iter() {
req = req.header(name, value);
}
if let Some(bytes) = self.body {
req = req.body(bytes);
}
self.net.send(req).await
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers.extend(headers);
self
}
#[must_use]
pub fn body(mut self, bytes: impl Into<Bytes>) -> Self {
self.body = Some(bytes.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct MockNet {
plans: Rc<RefCell<Plans>>,
}
impl MockNet {
pub fn client(&self) -> Client {
Default::default()
}
#[must_use]
pub fn on(&self) -> WhenRequest<WhenBuildRequest> {
WhenRequest::new(self)
}
fn _when(&self, plan: Plan) {
self.plans.borrow_mut().push(plan);
}
pub fn reset(&self) {
self.plans.take();
}
}
impl From<MockNet> for Net {
fn from(mock_net: MockNet) -> Self {
Self {
plans: Some(Arc::new(Mutex::new(RefCell::new(mock_net.plans.take())))),
}
}
}
impl Drop for MockNet {
fn drop(&mut self) {
let unused = self.plans.take();
if unused.is_empty() {
return; }
panic_with_unused_plans(unused);
}
}
impl Drop for Net {
fn drop(&mut self) {
if let Some(plans) = &self.plans {
let unused = plans.try_lock().expect("lock plans").take();
if unused.is_empty() {
return; }
panic_with_unused_plans(unused);
}
}
}
fn panic_with_unused_plans(unused: Vec<Plan>) {
eprintln!("These requests were expected, but not made:");
for plan in unused {
eprintln!("- {plan}");
}
panic!("There were expected requests that were not made.");
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatchRequest {
Method(NetMethod),
Url(Url),
Header { name: String, value: String },
Body(bytes::Bytes),
}
impl Display for MatchRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Method(method) => write!(f, "{method}"),
Self::Url(url) => write!(f, "{url}"),
Self::Header { name, value } => write!(f, "({name}: {value})"),
Self::Body(body) => write!(f, "Body: {body:?}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RespondWith {
Status(StatusCode),
Header { name: String, value: String },
Body(bytes::Bytes),
}
#[derive(Clone, Debug, Display, From)]
pub enum MockError {
#[display("url parse: {}", 0)]
UrlParse(#[from] url::ParseError),
}
impl std::error::Error for MockError {}
pub trait WhenState {}
pub struct WhenBuildRequest;
impl WhenState for WhenBuildRequest {}
pub struct WhenBuildResponse;
impl WhenState for WhenBuildResponse {}
#[derive(Debug, Clone)]
pub struct WhenRequest<'net, State>
where
State: WhenState,
{
_state: PhantomData<State>,
net: &'net MockNet,
match_on: Vec<MatchRequest>,
respond_with: Vec<RespondWith>,
error: Option<MockError>,
}
impl<'net> WhenRequest<'net, WhenBuildRequest> {
fn new(net: &'net MockNet) -> Self {
Self {
_state: PhantomData,
net,
match_on: vec![],
respond_with: vec![],
error: None,
}
}
#[must_use]
pub fn get(self, url: impl Into<String>) -> Self {
self._url(NetMethod::Get, url)
}
#[must_use]
pub fn post(self, url: impl Into<String>) -> Self {
self._url(NetMethod::Post, url)
}
#[must_use]
pub fn put(self, url: impl Into<String>) -> Self {
self._url(NetMethod::Put, url)
}
#[must_use]
pub fn delete(self, url: impl Into<String>) -> Self {
self._url(NetMethod::Delete, url)
}
#[must_use]
pub fn head(self, url: impl Into<String>) -> Self {
self._url(NetMethod::Head, url)
}
#[must_use]
pub fn patch(self, url: impl Into<String>) -> Self {
self._url(NetMethod::Patch, url)
}
fn _url(mut self, method: NetMethod, url: impl Into<String>) -> Self {
self.match_on.push(MatchRequest::Method(method));
match Url::parse(&url.into()) {
Ok(url) => {
self.match_on.push(MatchRequest::Url(url));
}
Err(err) => {
self.error.replace(err.into());
}
}
self
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.match_on.push(MatchRequest::Header {
name: name.into(),
value: value.into(),
});
self
}
#[must_use]
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
for (name, value) in headers {
self.match_on.push(MatchRequest::Header { name, value });
}
self
}
#[must_use]
pub fn body(mut self, body: impl Into<bytes::Bytes>) -> Self {
self.match_on.push(MatchRequest::Body(body.into()));
self
}
#[must_use]
pub fn respond(self, status: StatusCode) -> WhenRequest<'net, WhenBuildResponse> {
WhenRequest::<WhenBuildResponse> {
_state: PhantomData,
net: self.net,
match_on: self.match_on,
respond_with: vec![RespondWith::Status(status)],
error: self.error,
}
}
}
impl<'net> WhenRequest<'net, WhenBuildResponse> {
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
let name = name.into();
let value = value.into();
self.respond_with.push(RespondWith::Header { name, value });
self
}
#[must_use]
pub fn headers(mut self, headers: impl Into<HashMap<String, String>>) -> Self {
let h: HashMap<String, String> = headers.into();
for (name, value) in h.into_iter() {
self.respond_with.push(RespondWith::Header { name, value });
}
self
}
pub fn body(mut self, body: impl Into<bytes::Bytes>) -> Result<()> {
self.respond_with.push(RespondWith::Body(body.into()));
self.mock()
}
pub fn mock(self) -> Result<()> {
if let Some(error) = self.error {
return Err(crate::net::Error::InvalidMock(error));
}
let mut builder = http::response::Builder::default();
let mut response_body = None;
for part in self.respond_with {
builder = match part {
RespondWith::Status(status) => builder.status(status),
RespondWith::Header { name, value } => builder.header(name, value),
RespondWith::Body(body) => {
response_body.replace(body);
builder
}
}
}
let body = response_body.unwrap_or_default();
let response = builder.body(body)?;
self.net._when(Plan {
match_request: self.match_on,
response: response.into(),
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_normal<T: Sized + Send + Sync + Unpin>() {}
#[test]
fn normal_types() {
is_normal::<Net>();
is_normal::<MatchRequest>();
is_normal::<Plan>();
}
#[test]
fn plan_display() {
let plan = Plan {
match_request: vec![
MatchRequest::Method(NetMethod::Put),
MatchRequest::Header {
name: "alpha".into(),
value: "1".into(),
},
MatchRequest::Body("req body".into()),
],
response: http::response::Builder::default()
.status(204)
.header("foo", "bar")
.header("baz", "buck")
.body("contents")
.expect("body")
.into(),
};
let result = plan.to_string();
let expected = [
"Put",
"(alpha: 1)",
"Body: b\"req body\"",
"=>",
"Response {",
"url: \"http://no.url.provided.local/\",",
"status: 204,",
"headers: {\"foo\": \"bar\", \"baz\": \"buck\"}",
"}\n",
]
.join(" ");
assert_eq!(result, expected);
}
}