1use std::borrow::Cow;
3use std::fmt;
4
5use http::HeaderMap;
6use mime_guess::Mime;
7use web_sys::FormData;
8
9use super::Body;
10
11pub struct Form {
13 inner: FormParts<Part>,
14}
15
16impl Form {
17 pub(crate) fn is_empty(&self) -> bool {
18 self.inner.fields.is_empty()
19 }
20}
21
22pub struct Part {
24 meta: PartMetadata,
25 value: Body,
26}
27
28pub(crate) struct FormParts<P> {
29 pub(crate) fields: Vec<(Cow<'static, str>, P)>,
30}
31
32pub(crate) struct PartMetadata {
33 mime: Option<Mime>,
34 file_name: Option<Cow<'static, str>>,
35 pub(crate) headers: HeaderMap,
36}
37
38pub(crate) trait PartProps {
39 fn metadata(&self) -> &PartMetadata;
40}
41
42impl Default for Form {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl Form {
51 pub fn new() -> Form {
53 Form {
54 inner: FormParts::new(),
55 }
56 }
57
58 pub fn text<T, U>(self, name: T, value: U) -> Form
68 where
69 T: Into<Cow<'static, str>>,
70 U: Into<Cow<'static, str>>,
71 {
72 self.part(name, Part::text(value))
73 }
74
75 pub fn part<T>(self, name: T, part: Part) -> Form
77 where
78 T: Into<Cow<'static, str>>,
79 {
80 self.with_inner(move |inner| inner.part(name, part))
81 }
82
83 fn with_inner<F>(self, func: F) -> Self
84 where
85 F: FnOnce(FormParts<Part>) -> FormParts<Part>,
86 {
87 Form {
88 inner: func(self.inner),
89 }
90 }
91
92 pub(crate) fn to_form_data(&self) -> crate::Result<FormData> {
93 let form = FormData::new()
94 .map_err(crate::error::wasm)
95 .map_err(crate::error::builder)?;
96
97 for (name, part) in self.inner.fields.iter() {
98 part.append_to_form(name, &form)
99 .map_err(crate::error::wasm)
100 .map_err(crate::error::builder)?;
101 }
102 Ok(form)
103 }
104}
105
106impl fmt::Debug for Form {
107 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108 self.inner.fmt_fields("Form", f)
109 }
110}
111
112impl Part {
115 pub fn text<T>(value: T) -> Part
117 where
118 T: Into<Cow<'static, str>>,
119 {
120 let body = match value.into() {
121 Cow::Borrowed(slice) => Body::from(slice),
122 Cow::Owned(string) => Body::from(string),
123 };
124 Part::new(body)
125 }
126
127 pub fn bytes<T>(value: T) -> Part
129 where
130 T: Into<Cow<'static, [u8]>>,
131 {
132 let body = match value.into() {
133 Cow::Borrowed(slice) => Body::from(slice),
134 Cow::Owned(vec) => Body::from(vec),
135 };
136 Part::new(body)
137 }
138
139 pub fn stream<T: Into<Body>>(value: T) -> Part {
141 Part::new(value.into())
142 }
143
144 fn new(value: Body) -> Part {
145 Part {
146 meta: PartMetadata::new(),
147 value: value.into_part(),
148 }
149 }
150
151 pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
153 Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
154 }
155
156 fn mime(self, mime: Mime) -> Part {
158 self.with_inner(move |inner| inner.mime(mime))
159 }
160
161 pub fn file_name<T>(self, filename: T) -> Part
163 where
164 T: Into<Cow<'static, str>>,
165 {
166 self.with_inner(move |inner| inner.file_name(filename))
167 }
168
169 pub fn headers(self, headers: HeaderMap) -> Part {
171 self.with_inner(move |inner| inner.headers(headers))
172 }
173
174 fn with_inner<F>(self, func: F) -> Self
175 where
176 F: FnOnce(PartMetadata) -> PartMetadata,
177 {
178 Part {
179 meta: func(self.meta),
180 value: self.value,
181 }
182 }
183
184 fn append_to_form(
185 &self,
186 name: &str,
187 form: &web_sys::FormData,
188 ) -> Result<(), wasm_bindgen::JsValue> {
189 let single = self
190 .value
191 .as_single()
192 .expect("A part's body can't be multipart itself");
193
194 let mut mime_type = self.metadata().mime.as_ref();
195
196 if let super::body::Single::Text(text) = single {
200 if mime_type.is_none() || mime_type == Some(&mime_guess::mime::TEXT_PLAIN) {
201 if self.metadata().file_name.is_none() {
202 return form.append_with_str(name, text);
203 }
204 } else {
205 mime_type = Some(&mime_guess::mime::TEXT_PLAIN);
206 }
207 }
208
209 let blob = self.blob(mime_type)?;
210
211 if let Some(file_name) = &self.metadata().file_name {
212 form.append_with_blob_and_filename(name, &blob, file_name)
213 } else {
214 form.append_with_blob(name, &blob)
215 }
216 }
217
218 fn blob(&self, mime_type: Option<&Mime>) -> crate::Result<web_sys::Blob> {
219 use web_sys::Blob;
220 use web_sys::BlobPropertyBag;
221 let mut properties = BlobPropertyBag::new();
222 if let Some(mime) = mime_type {
223 properties.type_(mime.as_ref());
224 }
225
226 let js_value = self
227 .value
228 .as_single()
229 .expect("A part's body can't be set to a multipart body")
230 .to_js_value();
231
232 let body_array = js_sys::Array::new();
233 body_array.push(&js_value);
234
235 Blob::new_with_u8_array_sequence_and_options(body_array.as_ref(), &properties)
236 .map_err(crate::error::wasm)
237 .map_err(crate::error::builder)
238 }
239}
240
241impl fmt::Debug for Part {
242 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
243 let mut dbg = f.debug_struct("Part");
244 dbg.field("value", &self.value);
245 self.meta.fmt_fields(&mut dbg);
246 dbg.finish()
247 }
248}
249
250impl PartProps for Part {
251 fn metadata(&self) -> &PartMetadata {
252 &self.meta
253 }
254}
255
256impl<P: PartProps> FormParts<P> {
259 pub(crate) fn new() -> Self {
260 FormParts { fields: Vec::new() }
261 }
262
263 pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
265 where
266 T: Into<Cow<'static, str>>,
267 {
268 self.fields.push((name.into(), part));
269 self
270 }
271}
272
273impl<P: fmt::Debug> FormParts<P> {
274 pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
275 f.debug_struct(ty_name)
276 .field("parts", &self.fields)
277 .finish()
278 }
279}
280
281impl PartMetadata {
284 pub(crate) fn new() -> Self {
285 PartMetadata {
286 mime: None,
287 file_name: None,
288 headers: HeaderMap::default(),
289 }
290 }
291
292 pub(crate) fn mime(mut self, mime: Mime) -> Self {
293 self.mime = Some(mime);
294 self
295 }
296
297 pub(crate) fn file_name<T>(mut self, filename: T) -> Self
298 where
299 T: Into<Cow<'static, str>>,
300 {
301 self.file_name = Some(filename.into());
302 self
303 }
304
305 pub(crate) fn headers<T>(mut self, headers: T) -> Self
306 where
307 T: Into<HeaderMap>,
308 {
309 self.headers = headers.into();
310 self
311 }
312}
313
314impl PartMetadata {
315 pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
316 &self,
317 debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
318 ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
319 debug_struct
320 .field("mime", &self.mime)
321 .field("file_name", &self.file_name)
322 .field("headers", &self.headers)
323 }
324}
325
326#[cfg(test)]
327mod tests {
328
329 use wasm_bindgen_test::*;
330
331 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
332
333 #[wasm_bindgen_test]
334 async fn test_multipart_js() {
335 use super::{Form, Part};
336 use js_sys::Uint8Array;
337 use wasm_bindgen::JsValue;
338 use web_sys::{File, FormData};
339
340 let text_file_name = "test.txt";
341 let text_file_type = "text/plain";
342 let text_content = "TEST";
343 let text_part = Part::text(text_content)
344 .file_name(text_file_name)
345 .mime_str(text_file_type)
346 .expect("invalid mime type");
347
348 let binary_file_name = "binary.bin";
349 let binary_file_type = "application/octet-stream";
350 let binary_content = vec![0u8, 42];
351 let binary_part = Part::bytes(binary_content.clone())
352 .file_name(binary_file_name)
353 .mime_str(binary_file_type)
354 .expect("invalid mime type");
355
356 let string_name = "string";
357 let string_content = "CONTENT";
358 let string_part = Part::text(string_content);
359
360 let text_name = "text part";
361 let binary_name = "binary part";
362 let form = Form::new()
363 .part(text_name, text_part)
364 .part(binary_name, binary_part)
365 .part(string_name, string_part);
366
367 let mut init = web_sys::RequestInit::new();
368 init.method("POST");
369 init.body(Some(
370 form.to_form_data()
371 .expect("could not convert to FormData")
372 .as_ref(),
373 ));
374
375 let js_req = web_sys::Request::new_with_str_and_init("", &init)
376 .expect("could not create JS request");
377
378 let form_data_promise = js_req.form_data().expect("could not get form_data promise");
379
380 let form_data = crate::wasm::promise::<FormData>(form_data_promise)
381 .await
382 .expect("could not get body as form data");
383
384 let text_file = File::from(form_data.get(text_name));
386 assert_eq!(text_file.name(), text_file_name);
387 assert_eq!(text_file.type_(), text_file_type);
388
389 let text_promise = text_file.text();
390 let text = crate::wasm::promise::<JsValue>(text_promise)
391 .await
392 .expect("could not get text body as text");
393 assert_eq!(
394 text.as_string().expect("text is not a string"),
395 text_content
396 );
397
398 let binary_file = File::from(form_data.get(binary_name));
400 assert_eq!(binary_file.name(), binary_file_name);
401 assert_eq!(binary_file.type_(), binary_file_type);
402
403 let string = form_data
405 .get(string_name)
406 .as_string()
407 .expect("content is not a string");
408 assert_eq!(string, string_content);
409
410 let binary_array_buffer_promise = binary_file.array_buffer();
411 let array_buffer = crate::wasm::promise::<JsValue>(binary_array_buffer_promise)
412 .await
413 .expect("could not get request body as array buffer");
414
415 let binary = Uint8Array::new(&array_buffer).to_vec();
416
417 assert_eq!(binary, binary_content);
418 }
419}