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 let blob = part.blob()?;
99
100 if let Some(file_name) = &part.metadata().file_name {
101 form.append_with_blob_and_filename(name, &blob, &file_name)
102 } else {
103 form.append_with_blob(name, &blob)
104 }
105 .map_err(crate::error::wasm)
106 .map_err(crate::error::builder)?;
107 }
108 Ok(form)
109 }
110}
111
112impl fmt::Debug for Form {
113 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114 self.inner.fmt_fields("Form", f)
115 }
116}
117
118impl Part {
121 pub fn text<T>(value: T) -> Part
123 where
124 T: Into<Cow<'static, str>>,
125 {
126 let body = match value.into() {
127 Cow::Borrowed(slice) => Body::from(slice),
128 Cow::Owned(string) => Body::from(string),
129 };
130 Part::new(body)
131 }
132
133 pub fn bytes<T>(value: T) -> Part
135 where
136 T: Into<Cow<'static, [u8]>>,
137 {
138 let body = match value.into() {
139 Cow::Borrowed(slice) => Body::from(slice),
140 Cow::Owned(vec) => Body::from(vec),
141 };
142 Part::new(body)
143 }
144
145 pub fn stream<T: Into<Body>>(value: T) -> Part {
147 Part::new(value.into())
148 }
149
150 fn new(value: Body) -> Part {
151 Part {
152 meta: PartMetadata::new(),
153 value: value.into_part(),
154 }
155 }
156
157 pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
159 Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
160 }
161
162 fn mime(self, mime: Mime) -> Part {
164 self.with_inner(move |inner| inner.mime(mime))
165 }
166
167 pub fn file_name<T>(self, filename: T) -> Part
169 where
170 T: Into<Cow<'static, str>>,
171 {
172 self.with_inner(move |inner| inner.file_name(filename))
173 }
174
175 fn with_inner<F>(self, func: F) -> Self
176 where
177 F: FnOnce(PartMetadata) -> PartMetadata,
178 {
179 Part {
180 meta: func(self.meta),
181 value: self.value,
182 }
183 }
184
185 fn blob(&self) -> crate::Result<web_sys::Blob> {
186 use web_sys::Blob;
187 use web_sys::BlobPropertyBag;
188 let mut properties = BlobPropertyBag::new();
189 if let Some(mime) = &self.meta.mime {
190 properties.type_(mime.as_ref());
191 }
192
193 let js_value = self.value.to_js_value()?;
196 Blob::new_with_u8_array_sequence_and_options(&js_value, &properties)
197 .map_err(crate::error::wasm)
198 .map_err(crate::error::builder)
199 }
200}
201
202impl fmt::Debug for Part {
203 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
204 let mut dbg = f.debug_struct("Part");
205 dbg.field("value", &self.value);
206 self.meta.fmt_fields(&mut dbg);
207 dbg.finish()
208 }
209}
210
211impl PartProps for Part {
212 fn metadata(&self) -> &PartMetadata {
213 &self.meta
214 }
215}
216
217impl<P: PartProps> FormParts<P> {
220 pub(crate) fn new() -> Self {
221 FormParts { fields: Vec::new() }
222 }
223
224 pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
226 where
227 T: Into<Cow<'static, str>>,
228 {
229 self.fields.push((name.into(), part));
230 self
231 }
232}
233
234impl<P: fmt::Debug> FormParts<P> {
235 pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
236 f.debug_struct(ty_name)
237 .field("parts", &self.fields)
238 .finish()
239 }
240}
241
242impl PartMetadata {
245 pub(crate) fn new() -> Self {
246 PartMetadata {
247 mime: None,
248 file_name: None,
249 headers: HeaderMap::default(),
250 }
251 }
252
253 pub(crate) fn mime(mut self, mime: Mime) -> Self {
254 self.mime = Some(mime);
255 self
256 }
257
258 pub(crate) fn file_name<T>(mut self, filename: T) -> Self
259 where
260 T: Into<Cow<'static, str>>,
261 {
262 self.file_name = Some(filename.into());
263 self
264 }
265}
266
267impl PartMetadata {
268 pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
269 &self,
270 debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
271 ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
272 debug_struct
273 .field("mime", &self.mime)
274 .field("file_name", &self.file_name)
275 .field("headers", &self.headers)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281
282 use wasm_bindgen_test::*;
283
284 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
285
286 #[wasm_bindgen_test]
287 async fn test_multipart_js() {
288 use super::{Form, Part};
289 use js_sys::Uint8Array;
290 use wasm_bindgen::JsValue;
291 use web_sys::{File, FormData};
292
293 let text_file_name = "test.txt";
294 let text_file_type = "text/plain";
295 let text_content = "TEST";
296 let text_part = Part::text(text_content)
297 .file_name(text_file_name)
298 .mime_str(text_file_type)
299 .expect("invalid mime type");
300
301 let binary_file_name = "binary.bin";
302 let binary_file_type = "application/octet-stream";
303 let binary_content = vec![0u8, 42];
304 let binary_part = Part::bytes(binary_content.clone())
305 .file_name(binary_file_name)
306 .mime_str(binary_file_type)
307 .expect("invalid mime type");
308
309 let text_name = "text part";
310 let binary_name = "binary part";
311 let form = Form::new()
312 .part(text_name, text_part)
313 .part(binary_name, binary_part);
314
315 let mut init = web_sys::RequestInit::new();
316 init.method("POST");
317 init.body(Some(
318 form.to_form_data()
319 .expect("could not convert to FormData")
320 .as_ref(),
321 ));
322
323 let js_req = web_sys::Request::new_with_str_and_init("", &init)
324 .expect("could not create JS request");
325
326 let form_data_promise = js_req.form_data().expect("could not get form_data promise");
327
328 let form_data = crate::wasm::promise::<FormData>(form_data_promise)
329 .await
330 .expect("could not get body as form data");
331
332 let text_file = File::from(form_data.get(text_name));
334 assert_eq!(text_file.name(), text_file_name);
335 assert_eq!(text_file.type_(), text_file_type);
336
337 let text_promise = text_file.text();
338 let text = crate::wasm::promise::<JsValue>(text_promise)
339 .await
340 .expect("could not get text body as text");
341 assert_eq!(
342 text.as_string().expect("text is not a string"),
343 text_content
344 );
345
346 let binary_file = File::from(form_data.get(binary_name));
348 assert_eq!(binary_file.name(), binary_file_name);
349 assert_eq!(binary_file.type_(), binary_file_type);
350
351 let binary_array_buffer_promise = binary_file.array_buffer();
352 let array_buffer = crate::wasm::promise::<JsValue>(binary_array_buffer_promise)
353 .await
354 .expect("could not get request body as array buffer");
355
356 let binary = Uint8Array::new(&array_buffer).to_vec();
357
358 assert_eq!(binary, binary_content);
359 }
360}