use std::fmt::Display;
use std::{str::FromStr, time::Instant};
use crate::assertion::{Assertion, AssertionResult, Hand, UnprocessableReason};
use crate::dsl::{Part, Predicate};
use crate::error::Result;
use crate::{assert::Assert, grillon::LogSettings};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use reqwest::{Body, Client};
use serde_json::Value;
use url::Url;
const METHODS_NO_BODY: &[Method] = &[
Method::CONNECT,
Method::HEAD,
Method::GET,
Method::OPTIONS,
Method::TRACE,
];
pub struct BasicAuth {
username: String,
password: Option<String>,
}
pub struct BearerToken(String);
pub trait RequestHeaders {
fn to_header_map(&self) -> Result<HeaderMap>;
}
impl RequestHeaders for Vec<(HeaderName, HeaderValue)> {
fn to_header_map(&self) -> Result<HeaderMap> {
let mut map = HeaderMap::new();
for (key, value) in self {
map.append(key, value.clone());
}
Ok(map)
}
}
impl RequestHeaders for Vec<(&str, &str)> {
fn to_header_map(&self) -> Result<HeaderMap> {
let mut map = HeaderMap::new();
for (key, value) in self {
map.append(HeaderName::from_str(key)?, HeaderValue::from_str(value)?);
}
Ok(map)
}
}
impl RequestHeaders for HeaderMap {
fn to_header_map(&self) -> Result<HeaderMap> {
Ok(self.clone())
}
}
pub struct Request<'c> {
pub method: Method,
pub url: Url,
pub headers: Result<HeaderMap>,
pub payload: Option<Body>,
pub client: &'c Client,
pub log_settings: &'c LogSettings,
pub basic_auth: Option<BasicAuth>,
pub bearer_auth: Option<BearerToken>,
}
impl Request<'_> {
pub fn headers<H: RequestHeaders>(mut self, headers: H) -> Self {
self.headers = headers.to_header_map();
self
}
pub fn payload(mut self, json: Value) -> Self {
if METHODS_NO_BODY.contains(&self.method) {
println!(
"{} does not support HTTP body. No payload will be sent.",
self.method
);
return self;
}
self.payload = Some(Body::from(json.to_string()));
self
}
pub fn basic_auth<U, P>(mut self, username: U, password: Option<P>) -> Self
where
U: AsRef<str> + Display,
P: AsRef<str> + Display,
{
self.basic_auth = Some(BasicAuth {
username: username.to_string(),
password: password.map(|pwd| pwd.to_string()),
});
self
}
pub fn bearer_auth<T>(mut self, token: T) -> Self
where
T: AsRef<str> + Display,
{
self.bearer_auth = Some(BearerToken(token.to_string()));
self
}
pub async fn assert(self) -> Assert {
let headers = match self.headers {
Ok(headers) => headers,
Err(err) => {
let assertion = Assertion {
part: Part::Headers,
predicate: Predicate::NoPredicate,
left: Hand::Empty::<Value>,
right: Hand::Empty,
result: AssertionResult::Unprocessable(
UnprocessableReason::InvalidHttpRequestHeaders(err.to_string()),
),
};
assertion.assert(self.log_settings);
return Assert::new(None::<reqwest::Response>, None, self.log_settings.clone())
.await;
}
};
let mut req = self
.client
.request(self.method, self.url)
.body(self.payload.unwrap_or_default())
.headers(headers);
if let Some(basic_auth) = self.basic_auth {
let BasicAuth { username, password } = basic_auth;
req = req.basic_auth(username, password);
}
if let Some(bearer_auth) = self.bearer_auth {
let BearerToken(token) = bearer_auth;
req = req.bearer_auth(token);
}
let now = Instant::now();
let response = match req.send().await {
Ok(response) => response,
Err(err) => {
let assertion = Assertion {
part: Part::NoPart,
predicate: Predicate::NoPredicate,
left: Hand::Empty::<Value>,
right: Hand::Empty,
result: AssertionResult::Unprocessable(
UnprocessableReason::HttpRequestFailure(err.to_string()),
),
};
assertion.assert(self.log_settings);
return Assert::new(None::<reqwest::Response>, None, self.log_settings.clone())
.await;
}
};
let response_time_ms = now.elapsed().as_millis() as u64;
Assert::new(
Some(response),
Some(response_time_ms),
self.log_settings.clone(),
)
.await
}
}