blob: 909875c0a00aaff874f8ab6efb81d45a40ce89da [file] [log] [blame]
//! multipart/form-data
use std::borrow::Cow;
use std::fmt;
use http::HeaderMap;
use mime_guess::Mime;
use web_sys::FormData;
use super::Body;
/// An async multipart/form-data request.
pub struct Form {
inner: FormParts<Part>,
}
impl Form {
pub(crate) fn is_empty(&self) -> bool {
self.inner.fields.is_empty()
}
}
/// A field in a multipart form.
pub struct Part {
meta: PartMetadata,
value: Body,
}
pub(crate) struct FormParts<P> {
pub(crate) fields: Vec<(Cow<'static, str>, P)>,
}
pub(crate) struct PartMetadata {
mime: Option<Mime>,
file_name: Option<Cow<'static, str>>,
pub(crate) headers: HeaderMap,
}
pub(crate) trait PartProps {
fn metadata(&self) -> &PartMetadata;
}
// ===== impl Form =====
impl Default for Form {
fn default() -> Self {
Self::new()
}
}
impl Form {
/// Creates a new async Form without any content.
pub fn new() -> Form {
Form {
inner: FormParts::new(),
}
}
/// Add a data field with supplied name and value.
///
/// # Examples
///
/// ```
/// let form = reqwest::multipart::Form::new()
/// .text("username", "seanmonstar")
/// .text("password", "secret");
/// ```
pub fn text<T, U>(self, name: T, value: U) -> Form
where
T: Into<Cow<'static, str>>,
U: Into<Cow<'static, str>>,
{
self.part(name, Part::text(value))
}
/// Adds a customized Part.
pub fn part<T>(self, name: T, part: Part) -> Form
where
T: Into<Cow<'static, str>>,
{
self.with_inner(move |inner| inner.part(name, part))
}
fn with_inner<F>(self, func: F) -> Self
where
F: FnOnce(FormParts<Part>) -> FormParts<Part>,
{
Form {
inner: func(self.inner),
}
}
pub(crate) fn to_form_data(&self) -> crate::Result<FormData> {
let form = FormData::new()
.map_err(crate::error::wasm)
.map_err(crate::error::builder)?;
for (name, part) in self.inner.fields.iter() {
let blob = part.blob()?;
if let Some(file_name) = &part.metadata().file_name {
form.append_with_blob_and_filename(name, &blob, &file_name)
} else {
form.append_with_blob(name, &blob)
}
.map_err(crate::error::wasm)
.map_err(crate::error::builder)?;
}
Ok(form)
}
}
impl fmt::Debug for Form {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.inner.fmt_fields("Form", f)
}
}
// ===== impl Part =====
impl Part {
/// Makes a text parameter.
pub fn text<T>(value: T) -> Part
where
T: Into<Cow<'static, str>>,
{
let body = match value.into() {
Cow::Borrowed(slice) => Body::from(slice),
Cow::Owned(string) => Body::from(string),
};
Part::new(body)
}
/// Makes a new parameter from arbitrary bytes.
pub fn bytes<T>(value: T) -> Part
where
T: Into<Cow<'static, [u8]>>,
{
let body = match value.into() {
Cow::Borrowed(slice) => Body::from(slice),
Cow::Owned(vec) => Body::from(vec),
};
Part::new(body)
}
/// Makes a new parameter from an arbitrary stream.
pub fn stream<T: Into<Body>>(value: T) -> Part {
Part::new(value.into())
}
fn new(value: Body) -> Part {
Part {
meta: PartMetadata::new(),
value: value.into_part(),
}
}
/// Tries to set the mime of this part.
pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
}
// Re-export when mime 0.4 is available, with split MediaType/MediaRange.
fn mime(self, mime: Mime) -> Part {
self.with_inner(move |inner| inner.mime(mime))
}
/// Sets the filename, builder style.
pub fn file_name<T>(self, filename: T) -> Part
where
T: Into<Cow<'static, str>>,
{
self.with_inner(move |inner| inner.file_name(filename))
}
fn with_inner<F>(self, func: F) -> Self
where
F: FnOnce(PartMetadata) -> PartMetadata,
{
Part {
meta: func(self.meta),
value: self.value,
}
}
fn blob(&self) -> crate::Result<web_sys::Blob> {
use web_sys::Blob;
use web_sys::BlobPropertyBag;
let mut properties = BlobPropertyBag::new();
if let Some(mime) = &self.meta.mime {
properties.type_(mime.as_ref());
}
// BUG: the return value of to_js_value() is not valid if
// it is a MultipartForm variant.
let js_value = self.value.to_js_value()?;
Blob::new_with_u8_array_sequence_and_options(&js_value, &properties)
.map_err(crate::error::wasm)
.map_err(crate::error::builder)
}
}
impl fmt::Debug for Part {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut dbg = f.debug_struct("Part");
dbg.field("value", &self.value);
self.meta.fmt_fields(&mut dbg);
dbg.finish()
}
}
impl PartProps for Part {
fn metadata(&self) -> &PartMetadata {
&self.meta
}
}
// ===== impl FormParts =====
impl<P: PartProps> FormParts<P> {
pub(crate) fn new() -> Self {
FormParts { fields: Vec::new() }
}
/// Adds a customized Part.
pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
where
T: Into<Cow<'static, str>>,
{
self.fields.push((name.into(), part));
self
}
}
impl<P: fmt::Debug> FormParts<P> {
pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct(ty_name)
.field("parts", &self.fields)
.finish()
}
}
// ===== impl PartMetadata =====
impl PartMetadata {
pub(crate) fn new() -> Self {
PartMetadata {
mime: None,
file_name: None,
headers: HeaderMap::default(),
}
}
pub(crate) fn mime(mut self, mime: Mime) -> Self {
self.mime = Some(mime);
self
}
pub(crate) fn file_name<T>(mut self, filename: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.file_name = Some(filename.into());
self
}
}
impl PartMetadata {
pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
&self,
debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
debug_struct
.field("mime", &self.mime)
.field("file_name", &self.file_name)
.field("headers", &self.headers)
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn test_multipart_js() {
use super::{Form, Part};
use js_sys::Uint8Array;
use wasm_bindgen::JsValue;
use web_sys::{File, FormData};
let text_file_name = "test.txt";
let text_file_type = "text/plain";
let text_content = "TEST";
let text_part = Part::text(text_content)
.file_name(text_file_name)
.mime_str(text_file_type)
.expect("invalid mime type");
let binary_file_name = "binary.bin";
let binary_file_type = "application/octet-stream";
let binary_content = vec![0u8, 42];
let binary_part = Part::bytes(binary_content.clone())
.file_name(binary_file_name)
.mime_str(binary_file_type)
.expect("invalid mime type");
let text_name = "text part";
let binary_name = "binary part";
let form = Form::new()
.part(text_name, text_part)
.part(binary_name, binary_part);
let mut init = web_sys::RequestInit::new();
init.method("POST");
init.body(Some(
form.to_form_data()
.expect("could not convert to FormData")
.as_ref(),
));
let js_req = web_sys::Request::new_with_str_and_init("", &init)
.expect("could not create JS request");
let form_data_promise = js_req.form_data().expect("could not get form_data promise");
let form_data = crate::wasm::promise::<FormData>(form_data_promise)
.await
.expect("could not get body as form data");
// check text part
let text_file = File::from(form_data.get(text_name));
assert_eq!(text_file.name(), text_file_name);
assert_eq!(text_file.type_(), text_file_type);
let text_promise = text_file.text();
let text = crate::wasm::promise::<JsValue>(text_promise)
.await
.expect("could not get text body as text");
assert_eq!(
text.as_string().expect("text is not a string"),
text_content
);
// check binary part
let binary_file = File::from(form_data.get(binary_name));
assert_eq!(binary_file.name(), binary_file_name);
assert_eq!(binary_file.type_(), binary_file_type);
let binary_array_buffer_promise = binary_file.array_buffer();
let array_buffer = crate::wasm::promise::<JsValue>(binary_array_buffer_promise)
.await
.expect("could not get request body as array buffer");
let binary = Uint8Array::new(&array_buffer).to_vec();
assert_eq!(binary, binary_content);
}
}