Add multipart for WASM (#966)

This commit is contained in:
Enno Boland
2020-07-08 22:10:23 +02:00
committed by GitHub
parent af9fc5c9d8
commit a800202384
7 changed files with 335 additions and 13 deletions

View File

@@ -66,6 +66,7 @@ url = "2.1"
bytes = "0.5"
serde = "1.0"
serde_urlencoded = "0.6.1"
mime_guess = "2.0"
## json
serde_json = { version = "1.0", optional = true }
@@ -79,7 +80,6 @@ hyper = { version = "0.13.4", default-features = false, features = ["tcp"] }
lazy_static = "1.4"
log = "0.4"
mime = "0.3.7"
mime_guess = "2.0"
percent-encoding = "2.1"
tokio = { version = "0.2.5", default-features = false, features = ["tcp", "time"] }
pin-project-lite = "0.1.1"
@@ -141,6 +141,9 @@ features = [
"RequestMode",
"Response",
"Window",
"FormData",
"Blob",
"BlobPropertyBag",
]
[[example]]

View File

@@ -307,5 +307,5 @@ if_hyper! {
if_wasm! {
mod wasm;
pub use self::wasm::{Body, Client, ClientBuilder, Request, RequestBuilder, Response};
pub use self::wasm::{multipart, Body, Client, ClientBuilder, Request, RequestBuilder, Response};
}

View File

@@ -1,6 +1,9 @@
use super::multipart::Form;
/// dox
use bytes::Bytes;
use std::fmt;
use js_sys::Uint8Array;
use wasm_bindgen::JsValue;
/// The body of a `Request`.
///
@@ -9,39 +12,73 @@ use std::fmt;
/// passing many things (like a string or vector of bytes).
///
/// [builder]: ./struct.RequestBuilder.html#method.body
pub struct Body(Bytes);
pub struct Body {
inner: Inner,
}
enum Inner {
Bytes(Bytes),
Multipart(Form),
}
impl Body {
pub(crate) fn bytes(&self) -> &Bytes {
&self.0
pub(crate) fn to_js_value(&self) -> crate::Result<JsValue> {
match &self.inner {
Inner::Bytes(body_bytes) => {
let body_bytes: &[u8] = body_bytes.as_ref();
let body_array: Uint8Array = body_bytes.into();
let js_value: &JsValue = body_array.as_ref();
Ok(js_value.to_owned())
}
Inner::Multipart(form) => {
let form_data = form.to_form_data()?;
let js_value: &JsValue = form_data.as_ref();
Ok(js_value.to_owned())
}
}
}
#[inline]
pub(crate) fn from_form(f: Form) -> Body {
Self {
inner: Inner::Multipart(f),
}
}
}
impl From<Bytes> for Body {
#[inline]
fn from(bytes: Bytes) -> Body {
Body(bytes)
Body {
inner: Inner::Bytes(bytes),
}
}
}
impl From<Vec<u8>> for Body {
#[inline]
fn from(vec: Vec<u8>) -> Body {
Body(vec.into())
Body {
inner: Inner::Bytes(vec.into()),
}
}
}
impl From<&'static [u8]> for Body {
#[inline]
fn from(s: &'static [u8]) -> Body {
Body(Bytes::from_static(s))
Body {
inner: Inner::Bytes(Bytes::from_static(s)),
}
}
}
impl From<String> for Body {
#[inline]
fn from(s: String) -> Body {
Body(s.into())
Body {
inner: Inner::Bytes(s.into()),
}
}
}

View File

@@ -1,5 +1,4 @@
use http::Method;
use js_sys::Uint8Array;
use std::future::Future;
use wasm_bindgen::UnwrapThrowExt as _;
use url::Url;
@@ -133,9 +132,7 @@ async fn fetch(req: Request) -> crate::Result<Response> {
}
if let Some(body) = req.body() {
let body_bytes: &[u8] = body.bytes();
let body_array: Uint8Array = body_bytes.into();
init.body(Some(&body_array.into()));
init.body(Some(&body.to_js_value()?.as_ref().as_ref()));
}
let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)

View File

@@ -4,6 +4,8 @@ mod body;
mod client;
mod request;
mod response;
/// TODO
pub mod multipart;
pub use self::body::Body;
pub use self::client::{Client, ClientBuilder};

275
src/wasm/multipart.rs Normal file
View File

@@ -0,0 +1,275 @@
//! 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>,
}
/// 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,
}
}
/// 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 Multipart 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 {}

View File

@@ -190,6 +190,14 @@ impl RequestBuilder {
self
}
/// TODO
pub fn multipart(mut self, multipart: super::multipart::Form) -> RequestBuilder {
if let Ok(ref mut req) = self.request {
*req.body_mut() = Some(Body::from_form(multipart))
}
self
}
/// Add a `Header` to this Request.
pub fn header<K, V>(mut self, key: K, value: V) -> RequestBuilder
where