use anyhow::{Context, Result, bail};
use reqwest::{
Client, Method, Response,
header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::time::Duration;
use url::Url;
use crate::rt;
#[derive(Clone, Debug, Default)]
pub struct HttpBuilder {
timeout: Option<Duration>,
danger_accept_invalid_certs: bool,
danger_accept_invalid_hostnames: bool,
proxy: Option<Url>,
default_headers: Option<HeaderMap>,
}
impl HttpBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = Some(duration);
self
}
pub fn danger_accept_invalid_certs(mut self, on: bool) -> Self {
self.danger_accept_invalid_certs = on;
self
}
pub fn danger_accept_invalid_hostnames(mut self, on: bool) -> Self {
self.danger_accept_invalid_hostnames = on;
self
}
pub fn insecure_tls(mut self, on: bool) -> Self {
self.danger_accept_invalid_certs = on;
self.danger_accept_invalid_hostnames = on;
self
}
pub fn proxy(mut self, url: Option<Url>) -> Self {
self.proxy = url;
self
}
pub fn default_headers(mut self, headers: HeaderMap) -> Self {
self.default_headers = Some(headers);
self
}
pub fn build(self) -> Result<Http> {
let mut builder = Client::builder().use_rustls_tls();
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
if self.danger_accept_invalid_certs || self.danger_accept_invalid_hostnames {
bail!("insecure TLS is not permitted");
}
if let Some(proxy_url) = self.proxy {
let proxy = reqwest::Proxy::all(proxy_url.as_str())
.with_context(|| format!("invalid proxy url: {proxy_url}"))?;
builder = builder.proxy(proxy);
}
if let Some(headers) = self.default_headers {
builder = builder.default_headers(headers);
}
Http::from_builder(builder)
}
}
#[derive(Clone)]
pub struct Http {
client: Client,
}
impl Http {
pub fn new(timeout: Duration) -> Result<Self> {
Self::builder().timeout(timeout).build()
}
pub fn from_builder(builder: reqwest::ClientBuilder) -> Result<Self> {
Ok(Self {
client: builder.build().context("failed to build HTTP client")?,
})
}
pub fn builder() -> HttpBuilder {
HttpBuilder::new()
}
pub fn request(&self, method: Method, url: impl AsRef<str>) -> HttpRequest {
let url = url.as_ref();
let builder = self.client.request(method, url);
HttpRequest { builder }
}
pub fn get(&self, url: impl AsRef<str>) -> HttpRequest {
self.request(Method::GET, url)
}
pub fn post(&self, url: impl AsRef<str>) -> HttpRequest {
self.request(Method::POST, url)
}
pub fn put(&self, url: impl AsRef<str>) -> HttpRequest {
self.request(Method::PUT, url)
}
pub fn delete(&self, url: impl AsRef<str>) -> HttpRequest {
self.request(Method::DELETE, url)
}
pub fn client(&self) -> &Client {
&self.client
}
}
pub struct HttpRequest {
builder: reqwest::RequestBuilder,
}
impl HttpRequest {
pub fn bearer_auth(mut self, token: impl AsRef<str>) -> Self {
self.builder = self.builder.bearer_auth(token.as_ref());
self
}
pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.builder = self.builder.header(name, value);
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.builder = self.builder.headers(headers);
self
}
pub fn json(mut self, value: &impl Serialize) -> Self {
self.builder = self.builder.json(value);
self
}
pub fn body(mut self, value: impl Into<reqwest::Body>) -> Self {
self.builder = self.builder.body(value);
self
}
pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Self {
self.builder = self.builder.query(query);
self
}
pub fn form(mut self, value: &impl Serialize) -> Self {
self.builder = self.builder.form(value);
self
}
pub fn send(self) -> Result<HttpResponse> {
rt::sync_await(async {
let response = self.builder.send().await?;
Ok(HttpResponse { inner: response })
})
}
pub fn send_json<T: DeserializeOwned>(self) -> Result<T> {
let response = self.send()?;
response.json()
}
pub fn send_text(self) -> Result<String> {
let response = self.send()?;
response.text()
}
}
pub struct HttpResponse {
inner: Response,
}
impl HttpResponse {
pub fn status(&self) -> reqwest::StatusCode {
self.inner.status()
}
pub fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
pub fn into_inner(self) -> Response {
self.inner
}
pub fn json<T: DeserializeOwned>(self) -> Result<T> {
rt::sync_await(async {
self.inner
.json::<T>()
.await
.context("failed to decode JSON")
})
}
pub fn text(self) -> Result<String> {
rt::sync_await(async {
self.inner
.text()
.await
.context("failed to read body as text")
})
}
}