Add multipart for WASM (#966)
This commit is contained in:
@@ -66,6 +66,7 @@ url = "2.1"
|
|||||||
bytes = "0.5"
|
bytes = "0.5"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_urlencoded = "0.6.1"
|
serde_urlencoded = "0.6.1"
|
||||||
|
mime_guess = "2.0"
|
||||||
## json
|
## json
|
||||||
serde_json = { version = "1.0", optional = true }
|
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"
|
lazy_static = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3.7"
|
mime = "0.3.7"
|
||||||
mime_guess = "2.0"
|
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
tokio = { version = "0.2.5", default-features = false, features = ["tcp", "time"] }
|
tokio = { version = "0.2.5", default-features = false, features = ["tcp", "time"] }
|
||||||
pin-project-lite = "0.1.1"
|
pin-project-lite = "0.1.1"
|
||||||
@@ -141,6 +141,9 @@ features = [
|
|||||||
"RequestMode",
|
"RequestMode",
|
||||||
"Response",
|
"Response",
|
||||||
"Window",
|
"Window",
|
||||||
|
"FormData",
|
||||||
|
"Blob",
|
||||||
|
"BlobPropertyBag",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
|
|||||||
@@ -307,5 +307,5 @@ if_hyper! {
|
|||||||
if_wasm! {
|
if_wasm! {
|
||||||
mod 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
|
/// dox
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use js_sys::Uint8Array;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
/// The body of a `Request`.
|
/// The body of a `Request`.
|
||||||
///
|
///
|
||||||
@@ -9,39 +12,73 @@ use std::fmt;
|
|||||||
/// passing many things (like a string or vector of bytes).
|
/// passing many things (like a string or vector of bytes).
|
||||||
///
|
///
|
||||||
/// [builder]: ./struct.RequestBuilder.html#method.body
|
/// [builder]: ./struct.RequestBuilder.html#method.body
|
||||||
pub struct Body(Bytes);
|
pub struct Body {
|
||||||
|
inner: Inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Inner {
|
||||||
|
Bytes(Bytes),
|
||||||
|
Multipart(Form),
|
||||||
|
}
|
||||||
|
|
||||||
impl Body {
|
impl Body {
|
||||||
pub(crate) fn bytes(&self) -> &Bytes {
|
pub(crate) fn to_js_value(&self) -> crate::Result<JsValue> {
|
||||||
&self.0
|
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 {
|
impl From<Bytes> for Body {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(bytes: Bytes) -> Body {
|
fn from(bytes: Bytes) -> Body {
|
||||||
Body(bytes)
|
Body {
|
||||||
|
inner: Inner::Bytes(bytes),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<u8>> for Body {
|
impl From<Vec<u8>> for Body {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(vec: Vec<u8>) -> Body {
|
fn from(vec: Vec<u8>) -> Body {
|
||||||
Body(vec.into())
|
Body {
|
||||||
|
inner: Inner::Bytes(vec.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'static [u8]> for Body {
|
impl From<&'static [u8]> for Body {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(s: &'static [u8]) -> Body {
|
fn from(s: &'static [u8]) -> Body {
|
||||||
Body(Bytes::from_static(s))
|
Body {
|
||||||
|
inner: Inner::Bytes(Bytes::from_static(s)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Body {
|
impl From<String> for Body {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(s: String) -> Body {
|
fn from(s: String) -> Body {
|
||||||
Body(s.into())
|
Body {
|
||||||
|
inner: Inner::Bytes(s.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use http::Method;
|
use http::Method;
|
||||||
use js_sys::Uint8Array;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use wasm_bindgen::UnwrapThrowExt as _;
|
use wasm_bindgen::UnwrapThrowExt as _;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -133,9 +132,7 @@ async fn fetch(req: Request) -> crate::Result<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(body) = req.body() {
|
if let Some(body) = req.body() {
|
||||||
let body_bytes: &[u8] = body.bytes();
|
init.body(Some(&body.to_js_value()?.as_ref().as_ref()));
|
||||||
let body_array: Uint8Array = body_bytes.into();
|
|
||||||
init.body(Some(&body_array.into()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
|
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 client;
|
||||||
mod request;
|
mod request;
|
||||||
mod response;
|
mod response;
|
||||||
|
/// TODO
|
||||||
|
pub mod multipart;
|
||||||
|
|
||||||
pub use self::body::Body;
|
pub use self::body::Body;
|
||||||
pub use self::client::{Client, ClientBuilder};
|
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
|
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.
|
/// Add a `Header` to this Request.
|
||||||
pub fn header<K, V>(mut self, key: K, value: V) -> RequestBuilder
|
pub fn header<K, V>(mut self, key: K, value: V) -> RequestBuilder
|
||||||
where
|
where
|
||||||
|
|||||||
Reference in New Issue
Block a user