use crate::{errors::DeboaError, request::DeboaRequest};
#[cfg(feature = "multipart")]
use async_fn_stream::StreamEmitter;
use bytes::Bytes;
#[cfg(feature = "multipart")]
use bytes::BytesMut;
#[cfg(feature = "multipart")]
use futures::StreamExt;
use http::Method;
#[cfg(feature = "multipart")]
use http_body::Frame;
use http_body_util::Full;
use hyper_body_utils::HttpBody;
#[cfg(feature = "multipart")]
use rand::distr::{Alphanumeric, SampleString};
#[cfg(feature = "multipart")]
use std::{
fmt::Write,
io::{Error, ErrorKind::InvalidData},
pin::Pin,
};
use url::Url;
use urlencoding::encode;
pub(self) const ENCODED_FORM_TYPE: &str = "application/x-www-form-urlencoded";
#[cfg(feature = "multipart")]
pub(self) const MULTIPART_FORM_TYPE: &str = "multipart/form-data";
#[cfg(feature = "multipart")]
pub(self) const CRLF: &str = "\r\n";
#[derive(Debug, Clone)]
pub struct EncodedForm {
fields: Vec<(String, String)>,
}
impl EncodedForm {
#[inline]
pub fn builder() -> Self {
Self { fields: Vec::new() }
}
#[inline]
pub fn content_type(&self) -> &str {
ENCODED_FORM_TYPE
}
#[inline]
pub fn field(mut self, key: &str, value: &str) -> Self {
self.fields
.push((key.into(), value.into()));
self
}
#[inline]
pub fn build(self) -> Bytes {
self.fields
.iter()
.map(|(key, value)| format!("{}={}", key, encode(value)))
.collect::<Vec<String>>()
.join("&")
.into_bytes()
.into()
}
}
impl From<EncodedForm> for HttpBody {
#[inline]
fn from(val: EncodedForm) -> Self {
HttpBody::Standard(Full::new(val.build()))
}
}
impl TryFrom<(Url, Method, EncodedForm)> for DeboaRequest {
type Error = DeboaError;
fn try_from(value: (Url, Method, EncodedForm)) -> Result<Self, Self::Error> {
DeboaRequest::from(value.0)?
.method(value.1)
.bytes(
&value
.2
.build()
.to_vec(),
)
.build()
}
}
#[cfg(feature = "multipart")]
pub struct MimeStream {
name: String,
file_name: String,
mime_type: String,
inner: Pin<Box<dyn futures::Stream<Item = Result<Bytes, std::io::Error>>>>,
}
#[cfg(feature = "multipart")]
pub enum Part {
String((String, String)),
Stream(MimeStream),
}
#[cfg(feature = "multipart")]
async fn render_string(
emitter: &StreamEmitter<Result<Frame<Bytes>, std::io::Error>>,
key: &str,
value: &str,
) {
let mut bytes = BytesMut::new();
match write!(
&mut bytes,
"Content-Disposition: form-data; name=\"{key}\"{CRLF}{CRLF}{}{CRLF}",
&value
) {
Ok(()) => {
emitter
.emit(Ok(Frame::data(bytes.into())))
.await
}
Err(e) => {
emitter
.emit(Err(std::io::Error::new(InvalidData, e)))
.await
}
};
}
#[cfg(feature = "multipart")]
async fn render_stream(
emitter: &StreamEmitter<Result<Frame<Bytes>, std::io::Error>>,
stream: &mut MimeStream,
) {
let mut bytes = BytesMut::new();
match write!(
&mut bytes,
"Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"{CRLF}Content-Type: {}{CRLF}{CRLF}",
stream.name, stream.file_name, stream.mime_type
) {
Ok(()) => {
emitter
.emit(Ok(Frame::data(bytes.into())))
.await
}
Err(e) => {
emitter
.emit(Err(std::io::Error::new(InvalidData, e)))
.await
}
}
while let Some(chunk) = stream
.inner
.next()
.await
{
emitter
.emit(chunk.map(|val| Frame::data(val)))
.await;
}
emitter
.emit(Ok(Frame::data(CRLF.into())))
.await;
}
#[cfg(feature = "multipart")]
impl Part {
pub fn string(key: &str, value: &str) -> Part {
Part::String((key.into(), value.into()))
}
pub fn stream(stream: MimeStream) -> Part {
Part::Stream(stream)
}
pub async fn render(&mut self, emitter: &StreamEmitter<Result<Frame<Bytes>, std::io::Error>>) {
match self {
Part::String((key, value)) => {
render_string(emitter, key, value).await;
}
Part::Stream(stream) => {
render_stream(emitter, stream).await;
}
}
}
}
#[cfg(all(feature = "multipart", feature = "multi-threaded"))]
pub trait MultiPartFormStream:
futures::Stream<Item = std::result::Result<Frame<Bytes>, std::io::Error>> + Send + Sync
{
}
#[cfg(all(feature = "multipart", feature = "multi-threaded"))]
impl<T> MultiPartFormStream for T where
T: futures::Stream<Item = std::result::Result<Frame<Bytes>, std::io::Error>> + Send + Sync
{
}
#[cfg(all(feature = "multipart", feature = "single-threaded"))]
pub trait MultiPartFormStream:
futures::Stream<Item = std::result::Result<Frame<Bytes>, std::io::Error>>
{
}
#[cfg(all(feature = "multipart", feature = "single-threaded"))]
impl<T> MultiPartFormStream for T where
T: futures::Stream<Item = std::result::Result<Frame<Bytes>, std::io::Error>>
{
}
#[cfg(feature = "multipart")]
pub struct MultiPartForm {
fields: Vec<Part>,
boundary: String,
}
#[cfg(feature = "multipart")]
impl MultiPartForm {
#[inline]
pub fn builder() -> Self {
let boundary = Alphanumeric.sample_string(&mut rand::rng(), 10);
Self { fields: Vec::new(), boundary: format!("DeboaFormBdry{}", boundary) }
}
#[inline]
pub fn content_type(&self) -> String {
format!("{}; boundary={}", MULTIPART_FORM_TYPE, self.boundary)
}
#[inline]
pub fn boundary(&self) -> String {
self.boundary
.to_string()
}
#[inline]
pub fn field(mut self, name: &str, value: &str) -> Self {
self.fields
.push(Part::string(name, value));
self
}
#[inline]
pub fn stream<S>(mut self, name: &str, file_name: &str, mime_type: &str, stream: S) -> Self
where
S: futures::Stream<Item = Result<Bytes, std::io::Error>> + 'static,
{
self.fields
.push(Part::stream(MimeStream {
name: name.into(),
file_name: file_name.into(),
mime_type: mime_type.into(),
inner: Box::pin(stream),
}));
self
}
pub async fn build(mut self) -> impl futures::Stream<Item = Result<Frame<Bytes>, Error>> {
let boundary = self.boundary;
async_fn_stream::fn_stream(|emitter| async move {
let mut bytes = BytesMut::new();
match write!(&mut bytes, "--{}{CRLF}", &boundary) {
Ok(()) => {
emitter
.emit(Ok(Frame::data(bytes.into())))
.await
}
Err(e) => {
emitter
.emit(Err(std::io::Error::new(InvalidData, e)))
.await
}
}
let count = self.fields.len();
for (index, part) in self
.fields
.iter_mut()
.enumerate()
{
part.render(&emitter)
.await;
let mut bytes = BytesMut::new();
let ending = if index != count - 1 { CRLF } else { "--\r\n" };
match write!(&mut bytes, "--{}{ending}", &boundary) {
Ok(()) => {
emitter
.emit(Ok(Frame::data(bytes.into())))
.await
}
Err(e) => {
emitter
.emit(Err(std::io::Error::new(InvalidData, e)))
.await
}
}
}
})
}
}