Add multipart for WASM (#966)
This commit is contained in:
@@ -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]]
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
275
src/wasm/multipart.rs
Normal 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 {}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user