use crate::{
collection::Authentication,
http::{BodyStream, HttpMethod, RenderedBody, RequestBuildErrorKind},
};
use bytes::BytesMut;
use futures::TryStreamExt;
use itertools::Itertools;
use reqwest::header::{self, HeaderMap, HeaderName, HeaderValue};
use slumber_template::StreamSource;
pub struct CurlBuilder {
groups: Vec<Vec<String>>,
}
impl CurlBuilder {
pub fn new(method: HttpMethod) -> Self {
Self {
groups: vec![vec!["curl".into(), format!("-X{method}")]],
}
}
pub fn url(
mut self,
mut url: reqwest::Url,
query: &[(String, String)],
) -> Self {
if !query.is_empty() {
url.query_pairs_mut().extend_pairs(query);
}
self.groups[0].extend(["--url".into(), format!("'{url}'")]);
self
}
pub fn headers(
mut self,
headers: &HeaderMap,
) -> Result<Self, RequestBuildErrorKind> {
for (name, value) in headers {
self = self.header(name, value)?;
}
Ok(self)
}
pub fn header(
mut self,
name: &HeaderName,
value: &HeaderValue,
) -> Result<Self, RequestBuildErrorKind> {
let value = as_text(value.as_bytes())?;
self.groups
.push(vec!["--header".into(), format!("'{name}: {value}'")]);
Ok(self)
}
pub fn authentication(
mut self,
authentication: &Authentication<String>,
) -> Self {
match authentication {
Authentication::Basic { username, password } => {
self.groups.push(vec![
"--user".into(),
format!(
"'{username}:{password}'",
password = password.as_deref().unwrap_or_default()
),
]);
self
}
Authentication::Bearer { token } => self
.header(
&header::AUTHORIZATION,
&HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
)
.unwrap(),
}
}
pub async fn body(
mut self,
body: RenderedBody,
) -> Result<Self, RequestBuildErrorKind> {
match body {
RenderedBody::Raw(bytes) => {
let body = as_text(&bytes)?;
self.groups.push(vec!["--data".into(), format!("'{body}'")]);
}
RenderedBody::Stream(BodyStream {
source: Some(StreamSource::File { path }),
..
}) => {
self.groups.push(vec![
"--data".into(),
format!("'@{path}'", path = path.to_string_lossy()),
]);
}
RenderedBody::Stream(stream) => {
let bytes = stream
.stream
.try_collect::<BytesMut>()
.await
.map_err(RequestBuildErrorKind::BodyStream)?;
let body = as_text(&bytes)?;
self.groups.push(vec!["--data".into(), format!("'{body}'")]);
}
RenderedBody::Json(json) => {
self.groups
.push(vec!["--json".into(), format!("'{json:#}'")]);
}
RenderedBody::FormUrlencoded(form) => {
for (field, value) in form {
self.groups.push(vec![
"--data-urlencode".into(),
format!("'{field}={value}'"),
]);
}
}
RenderedBody::FormMultipart(form) => {
for (field, stream) in form {
let argument = if let Some(StreamSource::File { path }) =
stream.source
{
let path = path.to_string_lossy();
format!("'{field}=@{path}'")
} else {
let bytes = stream
.stream
.try_collect::<BytesMut>()
.await
.map_err(RequestBuildErrorKind::BodyStream)?;
let text = as_text(&bytes)?;
format!("'{field}={text}'")
};
self.groups.push(vec!["-F".into(), argument]);
}
}
}
Ok(self)
}
pub fn build(self) -> String {
const ARG_SEPARATOR: &str = " ";
const GROUP_SEPARATOR: &str = " \\\n ";
self.groups
.into_iter()
.map(|group| group.join(ARG_SEPARATOR))
.join(GROUP_SEPARATOR)
}
}
fn as_text(bytes: &[u8]) -> Result<&str, RequestBuildErrorKind> {
std::str::from_utf8(bytes).map_err(RequestBuildErrorKind::CurlInvalidUtf8)
}