use std::fmt;
use std::sync::Arc;
use crate::middleware::{Middleware, Next};
use crate::protocol::{EffectSender, HttpResult, ProtocolRequestBuilder};
use crate::{Config, RawResponse, Request, RequestBuilder, Result};
use http::Method;
use url::Url;
pub struct Client {
config: Config,
effect_sender: Arc<dyn EffectSender + Send + Sync>,
#[allow(clippy::rc_buffer)]
middleware: Arc<Vec<Arc<dyn Middleware>>>,
}
impl Clone for Client {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
effect_sender: Arc::clone(&self.effect_sender),
middleware: Arc::new(self.middleware.iter().cloned().collect()),
}
}
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Client {{}}")
}
}
impl Client {
#[cfg(test)]
pub(crate) fn new<Sender>(sender: Sender) -> Self
where
Sender: EffectSender + Send + Sync + 'static,
{
Self {
config: Config::default(),
effect_sender: Arc::new(sender),
middleware: Arc::new(vec![]),
}
}
#[cfg(test)]
pub(crate) fn new_with_config<Sender>(sender: Sender, config: Config) -> Self
where
Sender: EffectSender + Send + Sync + 'static,
{
Self {
config,
effect_sender: Arc::new(sender),
middleware: Arc::new(vec![]),
}
}
#[allow(dead_code)]
pub(crate) fn with(mut self, middleware: impl Middleware) -> Self {
let m = Arc::get_mut(&mut self.middleware)
.expect("Registering middleware is not possible after the Client has been used");
m.push(Arc::new(middleware));
self
}
pub async fn send(&self, request: impl Into<Request>) -> Result<RawResponse> {
let mut request: Request = request.into();
for name in self.config.headers.keys() {
if request.header(name).is_none() {
for value in self.config.headers.get_all(name) {
request.append_header(name.clone(), value.clone());
}
}
}
let middleware = self.middleware.clone();
let mw_stack = match request.take_middleware() {
Some(req_mw) => {
let mut mw = Vec::with_capacity(middleware.len() + req_mw.len());
mw.extend(middleware.iter().cloned());
mw.extend(req_mw);
Arc::new(mw)
}
None => middleware,
};
let next = Next::new(&mw_stack, &|request, client| {
Box::pin(async move {
let request = request
.into_protocol_request()
.expect("Failed to create request");
match client.effect_sender.send(request).await {
HttpResult::Ok(response) => response.try_into(),
HttpResult::Err(e) => Err(e),
}
})
});
let client = Self {
config: self.config.clone(),
effect_sender: Arc::clone(&self.effect_sender),
middleware: Arc::new(vec![]),
};
let response = next.run(request, client).await?;
Ok(response)
}
pub async fn recv_bytes(&self, request: impl Into<Request>) -> Result<Vec<u8>> {
let mut response = self.send(request.into()).await?;
response.body_bytes()
}
pub async fn recv_string(&self, request: impl Into<Request>) -> Result<String> {
let mut response = self.send(request.into()).await?;
response.body_string()
}
pub async fn recv_json<T: serde::de::DeserializeOwned>(
&self,
request: impl Into<Request>,
) -> Result<T> {
let mut response = self.send(request.into()).await?;
response.body_json::<T>()
}
pub async fn recv_form<T: serde::de::DeserializeOwned>(
&self,
request: impl Into<Request>,
) -> Result<T> {
let mut response = self.send(request.into()).await?;
response.body_form::<T>()
}
pub fn get(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::GET, self.url(uri), self.clone())
}
pub fn head(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::HEAD, self.url(uri), self.clone())
}
pub fn post(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::POST, self.url(uri), self.clone())
}
pub fn put(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::PUT, self.url(uri), self.clone())
}
pub fn delete(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::DELETE, self.url(uri), self.clone())
}
pub fn connect(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::CONNECT, self.url(uri), self.clone())
}
pub fn options(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::OPTIONS, self.url(uri), self.clone())
}
pub fn trace(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::TRACE, self.url(uri), self.clone())
}
pub fn patch(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(Method::PATCH, self.url(uri), self.clone())
}
pub fn request(&self, verb: Method, uri: impl AsRef<str>) -> RequestBuilder<()> {
RequestBuilder::new_for_middleware(verb, self.url(uri), self.clone())
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn config(&self) -> &Config {
&self.config
}
fn url(&self, uri: impl AsRef<str>) -> Url {
self.config.base_url.as_ref().map_or_else(
|| uri.as_ref().parse().unwrap(),
|base| base.join(uri.as_ref()).unwrap(),
)
}
}
#[cfg(test)]
mod client_tests {
use super::Client;
use crate::protocol::{HttpRequest, HttpResponse};
use crate::testing::FakeShell;
#[futures_test::test]
async fn an_http_get() {
let shell = FakeShell::default();
shell.provide_response(HttpResponse::ok().body("Hello World!").build());
let client = Client::new(shell.clone());
let mut response = client.get("https://example.com").await.unwrap();
assert_eq!(response.body_string().unwrap(), "Hello World!");
assert_eq!(
shell.take_requests_received(),
vec![HttpRequest::get("https://example.com/").build()]
);
}
#[futures_test::test]
async fn config_headers_are_sent_with_every_request() {
let shell = FakeShell::default();
shell.provide_response(HttpResponse::ok().build());
let config = crate::Config::default()
.add_header("x-api-key", "secret")
.unwrap();
let client = Client::new_with_config(shell.clone(), config);
client.get("https://example.com").await.unwrap();
let reqs = shell.take_requests_received();
assert_eq!(reqs.len(), 1);
assert!(
reqs[0]
.headers
.iter()
.any(|h| h.name == "x-api-key" && h.value == "secret"),
"x-api-key config header must appear in the outgoing request"
);
}
#[futures_test::test]
async fn per_request_header_takes_precedence_over_config_header() {
let shell = FakeShell::default();
shell.provide_response(HttpResponse::ok().build());
let config = crate::Config::default()
.add_header("x-version", "config-value")
.unwrap();
let client = Client::new_with_config(shell.clone(), config);
let mut req =
crate::Request::new(http::Method::GET, "https://example.com".parse().unwrap());
req.insert_header("x-version", http::HeaderValue::from_static("request-value"));
client.send(req).await.unwrap();
let reqs = shell.take_requests_received();
let values: Vec<&str> = reqs[0]
.headers
.iter()
.filter(|h| h.name == "x-version")
.map(|h| h.value.as_str())
.collect();
assert_eq!(values, ["request-value"], "per-request header must win");
}
#[futures_test::test]
async fn recv_bytes_returns_body() {
let shell = FakeShell::default();
shell.provide_response(HttpResponse::ok().body("bytes").build());
let client = Client::new(shell);
let bytes = client
.recv_bytes(crate::Request::new(
http::Method::GET,
"https://example.com".parse().unwrap(),
))
.await
.unwrap();
assert_eq!(bytes, b"bytes");
}
#[futures_test::test]
async fn recv_string_returns_body() {
let shell = FakeShell::default();
shell.provide_response(HttpResponse::ok().body("hello").build());
let client = Client::new(shell);
let text = client
.recv_string(crate::Request::new(
http::Method::GET,
"https://example.com".parse().unwrap(),
))
.await
.unwrap();
assert_eq!(text, "hello");
}
#[futures_test::test]
async fn recv_json_deserializes_body() {
#[derive(serde::Deserialize, PartialEq, Debug)]
struct Payload {
value: u32,
}
let shell = FakeShell::default();
shell.provide_response(
HttpResponse::ok()
.header("content-type", "application/json")
.json(serde_json::json!({"value": 42}))
.build(),
);
let client = Client::new(shell);
let payload: Payload = client
.recv_json(crate::Request::new(
http::Method::GET,
"https://example.com".parse().unwrap(),
))
.await
.unwrap();
assert_eq!(payload, Payload { value: 42 });
}
#[futures_test::test]
async fn recv_form_deserializes_body() {
#[derive(serde::Deserialize, PartialEq, Debug)]
struct Payload {
key: String,
}
let shell = FakeShell::default();
shell.provide_response(HttpResponse::ok().body("key=val").build());
let client = Client::new(shell);
let payload: Payload = client
.recv_form(crate::Request::new(
http::Method::GET,
"https://example.com".parse().unwrap(),
))
.await
.unwrap();
assert_eq!(payload, Payload { key: "val".into() });
}
}