use http::Method;
pub use http::StatusCode;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};
use url::Url;
use std::{collections::HashMap, path::PathBuf, time::Duration};
pub use reqwest::header;
use header::{HeaderName, HeaderValue};
#[derive(Deserialize)]
#[serde(untagged)]
enum SerdeDuration {
Seconds(u64),
Duration(Duration),
}
fn deserialize_duration<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Duration>, D::Error> {
if let Some(duration) = Option::<SerdeDuration>::deserialize(deserializer)? {
Ok(Some(match duration {
SerdeDuration::Seconds(s) => Duration::from_secs(s),
SerdeDuration::Duration(d) => d,
}))
} else {
Ok(None)
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientBuilder {
pub max_redirections: Option<usize>,
#[serde(deserialize_with = "deserialize_duration", default)]
pub connect_timeout: Option<Duration>,
}
impl ClientBuilder {
pub fn new() -> Self {
Default::default()
}
#[must_use]
pub fn max_redirections(mut self, max_redirections: usize) -> Self {
self.max_redirections = Some(max_redirections);
self
}
#[must_use]
pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
self.connect_timeout.replace(connect_timeout);
self
}
pub fn build(self) -> crate::api::Result<Client> {
let mut client_builder = reqwest::Client::builder();
if let Some(max_redirections) = self.max_redirections {
client_builder = client_builder.redirect(if max_redirections == 0 {
reqwest::redirect::Policy::none()
} else {
reqwest::redirect::Policy::limited(max_redirections)
});
}
if let Some(connect_timeout) = self.connect_timeout {
client_builder = client_builder.connect_timeout(connect_timeout);
}
let client = client_builder.build()?;
Ok(Client(client))
}
}
#[derive(Debug, Clone)]
pub struct Client(reqwest::Client);
impl Client {
pub async fn send(&self, mut request: HttpRequestBuilder) -> crate::api::Result<Response> {
let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;
let mut request_builder = self.0.request(method, request.url.as_str());
if let Some(query) = request.query {
request_builder = request_builder.query(&query);
}
if let Some(timeout) = request.timeout {
request_builder = request_builder.timeout(timeout);
}
if let Some(body) = request.body {
request_builder = match body {
Body::Bytes(data) => request_builder.body(bytes::Bytes::from(data)),
Body::Text(text) => request_builder.body(bytes::Bytes::from(text)),
Body::Json(json) => request_builder.json(&json),
Body::Form(form_body) => {
#[allow(unused_variables)]
fn send_form(
request_builder: reqwest::RequestBuilder,
headers: &mut Option<HeaderMap>,
form_body: FormBody,
) -> crate::api::Result<reqwest::RequestBuilder> {
#[cfg(feature = "http-multipart")]
if matches!(
headers
.as_ref()
.and_then(|h| h.0.get("content-type"))
.map(|v| v.as_bytes()),
Some(b"multipart/form-data")
) {
headers.as_mut().map(|h| h.0.remove("content-type"));
let mut multipart = reqwest::multipart::Form::new();
for (name, part) in form_body.0 {
let part = match part {
FormPart::File {
file,
mime,
file_name,
} => {
let bytes: Vec<u8> = file.try_into()?;
let mut part = reqwest::multipart::Part::bytes(bytes);
if let Some(mime) = mime {
part = part.mime_str(&mime)?;
}
if let Some(file_name) = file_name {
part = part.file_name(file_name);
}
part
}
FormPart::Text(value) => reqwest::multipart::Part::text(value),
};
multipart = multipart.part(name, part);
}
return Ok(request_builder.multipart(multipart));
}
let mut form = Vec::new();
for (name, part) in form_body.0 {
match part {
FormPart::File { file, .. } => {
let bytes: Vec<u8> = file.try_into()?;
form.push((name, serde_json::to_string(&bytes)?))
}
FormPart::Text(value) => form.push((name, value)),
}
}
Ok(request_builder.form(&form))
}
send_form(request_builder, &mut request.headers, form_body)?
}
};
}
if let Some(headers) = request.headers {
request_builder = request_builder.headers(headers.0);
}
let http_request = request_builder.build()?;
let response = self.0.execute(http_request).await?;
Ok(Response(
request.response_type.unwrap_or(ResponseType::Json),
response,
))
}
}
#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u16)]
#[non_exhaustive]
pub enum ResponseType {
Json = 1,
Text,
Binary,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum FilePart {
Path(PathBuf),
Contents(Vec<u8>),
}
impl TryFrom<FilePart> for Vec<u8> {
type Error = crate::api::Error;
fn try_from(file: FilePart) -> crate::api::Result<Self> {
let bytes = match file {
FilePart::Path(path) => std::fs::read(path)?,
FilePart::Contents(bytes) => bytes,
};
Ok(bytes)
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum FormPart {
Text(String),
#[serde(rename_all = "camelCase")]
File {
file: FilePart,
mime: Option<String>,
file_name: Option<String>,
},
}
#[derive(Debug, Deserialize)]
pub struct FormBody(pub(crate) indexmap::IndexMap<String, FormPart>);
impl FormBody {
pub fn new(data: HashMap<String, FormPart>) -> Self {
Self(indexmap::IndexMap::from_iter(data))
}
pub fn new_ordered(data: indexmap::IndexMap<String, FormPart>) -> Self {
Self(data)
}
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", content = "payload")]
#[non_exhaustive]
pub enum Body {
Form(FormBody),
Json(Value),
Text(String),
Bytes(Vec<u8>),
}
#[derive(Debug, Default)]
pub struct HeaderMap(header::HeaderMap);
impl<'de> Deserialize<'de> for HeaderMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map = HashMap::<String, String>::deserialize(deserializer)?;
let mut headers = header::HeaderMap::default();
for (key, value) in map {
if let (Ok(key), Ok(value)) = (
header::HeaderName::from_bytes(key.as_bytes()),
header::HeaderValue::from_str(&value),
) {
headers.insert(key, value);
} else {
return Err(serde::de::Error::custom(format!(
"invalid header `{key}` `{value}`"
)));
}
}
Ok(Self(headers))
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpRequestBuilder {
pub method: String,
pub url: Url,
pub query: Option<HashMap<String, String>>,
pub headers: Option<HeaderMap>,
pub body: Option<Body>,
#[serde(deserialize_with = "deserialize_duration", default)]
pub timeout: Option<Duration>,
pub response_type: Option<ResponseType>,
}
impl HttpRequestBuilder {
pub fn new(method: impl Into<String>, url: impl AsRef<str>) -> crate::api::Result<Self> {
Ok(Self {
method: method.into(),
url: Url::parse(url.as_ref())?,
query: None,
headers: None,
body: None,
timeout: None,
response_type: None,
})
}
#[must_use]
pub fn query(mut self, query: HashMap<String, String>) -> Self {
self.query = Some(query);
self
}
pub fn header<K, V>(mut self, key: K, value: V) -> crate::api::Result<Self>
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<http::Error>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
{
let key: Result<HeaderName, http::Error> = key.try_into().map_err(Into::into);
let value: Result<HeaderValue, http::Error> = value.try_into().map_err(Into::into);
self
.headers
.get_or_insert_with(Default::default)
.0
.insert(key?, value?);
Ok(self)
}
#[must_use]
pub fn headers(mut self, headers: header::HeaderMap) -> Self {
self.headers.replace(HeaderMap(headers));
self
}
#[must_use]
pub fn body(mut self, body: Body) -> Self {
self.body = Some(body);
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout.replace(timeout);
self
}
#[must_use]
pub fn response_type(mut self, response_type: ResponseType) -> Self {
self.response_type = Some(response_type);
self
}
}
#[derive(Debug)]
pub struct Response(ResponseType, reqwest::Response);
impl Response {
pub fn status(&self) -> StatusCode {
self.1.status()
}
pub fn headers(&self) -> &header::HeaderMap {
self.1.headers()
}
pub async fn bytes(self) -> crate::api::Result<RawResponse> {
let status = self.status().as_u16();
let data = self.1.bytes().await?.to_vec();
Ok(RawResponse { status, data })
}
#[allow(dead_code)]
pub(crate) fn bytes_stream(
self,
) -> impl futures_util::Stream<Item = crate::api::Result<bytes::Bytes>> {
use futures_util::StreamExt;
self.1.bytes_stream().map(|res| res.map_err(Into::into))
}
pub async fn read(self) -> crate::api::Result<ResponseData> {
let url = self.1.url().clone();
let mut headers = HashMap::new();
let mut raw_headers = HashMap::new();
for (name, value) in self.1.headers() {
headers.insert(
name.as_str().to_string(),
String::from_utf8(value.as_bytes().to_vec())?,
);
raw_headers.insert(
name.as_str().to_string(),
self
.1
.headers()
.get_all(name)
.into_iter()
.map(|v| String::from_utf8(v.as_bytes().to_vec()).map_err(Into::into))
.collect::<crate::api::Result<Vec<String>>>()?,
);
}
let status = self.1.status().as_u16();
let data = match self.0 {
ResponseType::Json => self.1.json().await?,
ResponseType::Text => Value::String(self.1.text().await?),
ResponseType::Binary => serde_json::to_value(&self.1.bytes().await?)?,
};
Ok(ResponseData {
url,
status,
headers,
raw_headers,
data,
})
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct RawResponse {
pub status: u16,
pub data: Vec<u8>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ResponseData {
pub url: Url,
pub status: u16,
pub headers: HashMap<String, String>,
pub raw_headers: HashMap<String, Vec<String>>,
pub data: Value,
}
#[cfg(test)]
mod test {
use super::ClientBuilder;
use quickcheck::{Arbitrary, Gen};
impl Arbitrary for ClientBuilder {
fn arbitrary(g: &mut Gen) -> Self {
Self {
max_redirections: Option::arbitrary(g),
connect_timeout: Option::arbitrary(g),
}
}
}
}