Skip to main content

webx_core/form/
de.rs

1//! A `serde::Deserializer` over a parsed [`MultipartForm`].
2//!
3//! This is what lets ordinary `#[derive(Deserialize)]` request structs bind
4//! `multipart/form-data` without a second derive or a parallel binder: the
5//! framework hands the derive a map of field name to value, where text parts
6//! become strings and file parts become a handle to the parsed [`FormFile`].
7//!
8//! File handles are resolved through a task-local scope installed by
9//! [`crate::route::bind::bind_form_request`], so a `FormFile` produced this way
10//! keeps its own reference-counted ownership of the uploaded bytes and stays
11//! valid after the form itself is dropped. A JSON body can never satisfy that
12//! handle, which is what keeps `FormFile` from becoming an arbitrary-file-read
13//! primitive.
14//!
15//! Route parameters are merged in as text values, so a route such as
16//! `POST /api/users/{id}/avatar` binds `{id}` alongside the uploaded file.
17
18use super::{ActiveForm, FormFile, MultipartForm};
19use serde::de::{
20    self, DeserializeSeed, EnumAccess, Error as _, MapAccess, SeqAccess, VariantAccess, Visitor,
21};
22use serde::forward_to_deserialize_any;
23use serde::Deserializer;
24use std::collections::HashMap;
25use std::fmt;
26
27/// Key used inside the `FormFile` metadata map to carry the form handle.
28const FILE_HANDLE_KEY: &str = "$webx_form_file";
29
30/// Error produced while binding a `multipart/form-data` request.
31#[derive(Debug, Clone)]
32pub struct FormError(String);
33
34impl FormError {
35    /// The human-readable reason binding failed.
36    pub fn message(&self) -> &str {
37        &self.0
38    }
39}
40
41impl fmt::Display for FormError {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(&self.0)
44    }
45}
46
47impl std::error::Error for FormError {}
48
49impl de::Error for FormError {
50    fn custom<T: fmt::Display>(msg: T) -> Self {
51        FormError(msg.to_string())
52    }
53}
54
55/// One value slot in the form, in insertion order.
56enum ValueRef<'a> {
57    Text(&'a str),
58    /// A file part: its index into [`MultipartForm::files`].
59    File(usize),
60}
61
62struct Group<'a> {
63    name: &'a str,
64    values: Vec<ValueRef<'a>>,
65}
66
67/// Deserializer over a [`MultipartForm`].
68///
69/// Build one with [`FormDeserializer::new`] and pass it to `T::deserialize`.
70/// Prefer [`crate::route::bind::bind_form_request`], which also installs the
71/// task-local scope that file handles need.
72pub struct FormDeserializer<'a> {
73    groups: Vec<Group<'a>>,
74}
75
76impl<'a> FormDeserializer<'a> {
77    /// Build a deserializer over `form`.
78    ///
79    /// `route_params` are merged in as text values and take precedence over
80    /// same-named form fields, mirroring how JSON body binding overlays path
81    /// parameters.
82    pub fn new(form: &'a MultipartForm, route_params: &'a HashMap<String, String>) -> Self {
83        let mut groups: Vec<Group<'a>> = Vec::new();
84        let mut index: HashMap<&'a str, usize> = HashMap::new();
85
86        {
87            let mut push =
88                |groups: &mut Vec<Group<'a>>, name: &'a str, value: ValueRef<'a>| match index
89                    .get(name)
90                {
91                    Some(&at) => groups[at].values.push(value),
92                    None => {
93                        index.insert(name, groups.len());
94                        groups.push(Group {
95                            name,
96                            values: vec![value],
97                        });
98                    }
99                };
100
101            for (name, value) in route_params {
102                push(&mut groups, name.as_str(), ValueRef::Text(value.as_str()));
103            }
104            for field in form.fields() {
105                if route_params.contains_key(&field.name) {
106                    continue;
107                }
108                push(
109                    &mut groups,
110                    field.name.as_str(),
111                    ValueRef::Text(field.value.as_str()),
112                );
113            }
114            for (position, file) in form.files().iter().enumerate() {
115                let name = file.field_name();
116                if route_params.contains_key(name) {
117                    continue;
118                }
119                push(&mut groups, name, ValueRef::File(position));
120            }
121        }
122
123        Self { groups }
124    }
125}
126
127impl<'de, 'a> Deserializer<'de> for FormDeserializer<'a> {
128    type Error = FormError;
129
130    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
131        self.deserialize_map(visitor)
132    }
133
134    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
135        visitor.visit_map(FormMapAccess {
136            groups: self.groups,
137            index: 0,
138        })
139    }
140
141    fn deserialize_struct<V: Visitor<'de>>(
142        self,
143        _name: &'static str,
144        _fields: &'static [&'static str],
145        visitor: V,
146    ) -> Result<V::Value, FormError> {
147        self.deserialize_map(visitor)
148    }
149
150    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
151        visitor.visit_some(self)
152    }
153
154    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
155        visitor.visit_unit()
156    }
157
158    fn deserialize_unit_struct<V: Visitor<'de>>(
159        self,
160        _name: &'static str,
161        visitor: V,
162    ) -> Result<V::Value, FormError> {
163        visitor.visit_unit()
164    }
165
166    fn deserialize_newtype_struct<V: Visitor<'de>>(
167        self,
168        _name: &'static str,
169        visitor: V,
170    ) -> Result<V::Value, FormError> {
171        visitor.visit_newtype_struct(self)
172    }
173
174    forward_to_deserialize_any! {
175        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
176        bytes byte_buf seq tuple tuple_struct enum identifier ignored_any
177    }
178}
179
180struct FormMapAccess<'a> {
181    groups: Vec<Group<'a>>,
182    index: usize,
183}
184
185impl<'de, 'a> MapAccess<'de> for FormMapAccess<'a> {
186    type Error = FormError;
187
188    fn next_key_seed<K: DeserializeSeed<'de>>(
189        &mut self,
190        seed: K,
191    ) -> Result<Option<K::Value>, FormError> {
192        if self.index >= self.groups.len() {
193            return Ok(None);
194        }
195        let key = seed.deserialize(StrDeserializer(self.groups[self.index].name))?;
196        Ok(Some(key))
197    }
198
199    fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value, FormError> {
200        let values = &self.groups[self.index].values;
201        self.index += 1;
202        seed.deserialize(FormValueDeserializer { values })
203    }
204}
205
206struct FormValueDeserializer<'a> {
207    values: &'a [ValueRef<'a>],
208}
209
210impl<'a> FormValueDeserializer<'a> {
211    /// First text value for this key.
212    ///
213    /// Repeated text fields bind to the first value for scalar targets, which
214    /// matches how browsers submit checkboxes and how other web frameworks
215    /// bind repeated keys.
216    fn first_text(&self) -> Result<&'a str, FormError> {
217        self.values
218            .iter()
219            .find_map(|value| match value {
220                ValueRef::Text(text) => Some(*text),
221                ValueRef::File(_) => None,
222            })
223            .ok_or_else(|| FormError::custom("expected a text field, found a file part"))
224    }
225}
226
227macro_rules! parse_text {
228    ($method:ident, $visit:ident, $ty:ty, $label:literal) => {
229        fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
230            let text = self.first_text()?;
231            let value: $ty = text.parse().map_err(|_| {
232                FormError::custom(format!(
233                    "cannot parse {text:?} as {} for {}",
234                    $label,
235                    stringify!($method)
236                ))
237            })?;
238            visitor.$visit(value)
239        }
240    };
241}
242
243impl<'de, 'a> Deserializer<'de> for FormValueDeserializer<'a> {
244    type Error = FormError;
245
246    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
247        match self.values {
248            [] => visitor.visit_unit(),
249            [ValueRef::Text(text)] => visitor.visit_str(text),
250            [ValueRef::File(position)] => visitor.visit_map(FormFileAccess::single(*position)),
251            many => visitor.visit_seq(FormSeqAccess {
252                values: many,
253                index: 0,
254            }),
255        }
256    }
257
258    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
259        if self.values.is_empty() {
260            visitor.visit_none()
261        } else {
262            visitor.visit_some(self)
263        }
264    }
265
266    fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
267        visitor.visit_seq(FormSeqAccess {
268            values: self.values,
269            index: 0,
270        })
271    }
272
273    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
274        match self.values {
275            [ValueRef::File(position)] => visitor.visit_map(FormFileAccess::single(*position)),
276            _ => Err(FormError::custom(
277                "expected a single file part for this field",
278            )),
279        }
280    }
281
282    fn deserialize_struct<V: Visitor<'de>>(
283        self,
284        _name: &'static str,
285        _fields: &'static [&'static str],
286        visitor: V,
287    ) -> Result<V::Value, FormError> {
288        self.deserialize_map(visitor)
289    }
290
291    fn deserialize_newtype_struct<V: Visitor<'de>>(
292        self,
293        _name: &'static str,
294        visitor: V,
295    ) -> Result<V::Value, FormError> {
296        visitor.visit_newtype_struct(self)
297    }
298
299    fn deserialize_enum<V: Visitor<'de>>(
300        self,
301        _name: &'static str,
302        _variants: &'static [&'static str],
303        visitor: V,
304    ) -> Result<V::Value, FormError> {
305        visitor.visit_enum(FormEnumAccess(self.first_text()?))
306    }
307
308    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
309        visitor.visit_unit()
310    }
311
312    fn deserialize_unit_struct<V: Visitor<'de>>(
313        self,
314        _name: &'static str,
315        visitor: V,
316    ) -> Result<V::Value, FormError> {
317        visitor.visit_unit()
318    }
319
320    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
321        visitor.visit_str(self.first_text()?)
322    }
323
324    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
325        visitor.visit_str(self.first_text()?)
326    }
327
328    fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
329        visitor.visit_str(self.first_text()?)
330    }
331
332    fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
333        visitor.visit_bytes(self.first_text()?.as_bytes())
334    }
335
336    fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
337        visitor.visit_byte_buf(self.first_text()?.as_bytes().to_vec())
338    }
339
340    parse_text!(deserialize_bool, visit_bool, bool, "a boolean");
341    parse_text!(deserialize_i8, visit_i8, i8, "an integer");
342    parse_text!(deserialize_i16, visit_i16, i16, "an integer");
343    parse_text!(deserialize_i32, visit_i32, i32, "an integer");
344    parse_text!(deserialize_i64, visit_i64, i64, "an integer");
345    parse_text!(deserialize_u8, visit_u8, u8, "an integer");
346    parse_text!(deserialize_u16, visit_u16, u16, "an integer");
347    parse_text!(deserialize_u32, visit_u32, u32, "an integer");
348    parse_text!(deserialize_u64, visit_u64, u64, "an integer");
349    parse_text!(deserialize_f32, visit_f32, f32, "a number");
350    parse_text!(deserialize_f64, visit_f64, f64, "a number");
351
352    fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
353        let text = self.first_text()?;
354        let mut chars = text.chars();
355        match (chars.next(), chars.next()) {
356            (Some(ch), None) => visitor.visit_char(ch),
357            _ => Err(FormError::custom(format!(
358                "expected a single character, got {text:?}"
359            ))),
360        }
361    }
362
363    forward_to_deserialize_any! {
364        i128 u128 tuple tuple_struct ignored_any
365    }
366}
367
368struct FormSeqAccess<'a> {
369    values: &'a [ValueRef<'a>],
370    index: usize,
371}
372
373impl<'de, 'a> SeqAccess<'de> for FormSeqAccess<'a> {
374    type Error = FormError;
375
376    fn next_element_seed<T: DeserializeSeed<'de>>(
377        &mut self,
378        seed: T,
379    ) -> Result<Option<T::Value>, FormError> {
380        if self.index >= self.values.len() {
381            return Ok(None);
382        }
383        let slice = &self.values[self.index..self.index + 1];
384        self.index += 1;
385        seed.deserialize(FormValueDeserializer { values: slice })
386            .map(Some)
387    }
388
389    fn size_hint(&self) -> Option<usize> {
390        Some(self.values.len() - self.index)
391    }
392}
393
394// ---------------------------------------------------------------------------
395// FormFile serde representation
396// ---------------------------------------------------------------------------
397
398/// Single-entry map carrying the form-local handle for one file part.
399struct FormFileAccess {
400    position: usize,
401    index: usize,
402}
403
404impl FormFileAccess {
405    fn single(position: usize) -> Self {
406        Self { position, index: 0 }
407    }
408}
409
410impl<'de> MapAccess<'de> for FormFileAccess {
411    type Error = FormError;
412
413    fn next_key_seed<K: DeserializeSeed<'de>>(
414        &mut self,
415        seed: K,
416    ) -> Result<Option<K::Value>, FormError> {
417        if self.index > 0 {
418            return Ok(None);
419        }
420        seed.deserialize(StrDeserializer(FILE_HANDLE_KEY)).map(Some)
421    }
422
423    fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value, FormError> {
424        self.index += 1;
425        seed.deserialize(U64Deserializer(self.position as u64))
426    }
427
428    fn size_hint(&self) -> Option<usize> {
429        Some(1 - self.index.min(1))
430    }
431}
432
433/// Deserializer for a bare `u64`.
434struct U64Deserializer(u64);
435
436impl<'de> Deserializer<'de> for U64Deserializer {
437    type Error = FormError;
438
439    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
440        visitor.visit_u64(self.0)
441    }
442
443    forward_to_deserialize_any! {
444        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
445        bytes byte_buf option unit unit_struct newtype_struct seq tuple tuple_struct
446        map struct enum identifier ignored_any
447    }
448}
449
450/// String deserializer used for map keys.
451struct StrDeserializer<'a>(&'a str);
452
453impl<'de, 'a> Deserializer<'de> for StrDeserializer<'a> {
454    type Error = FormError;
455
456    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, FormError> {
457        visitor.visit_str(self.0)
458    }
459
460    forward_to_deserialize_any! {
461        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
462        bytes byte_buf option unit unit_struct newtype_struct seq tuple tuple_struct
463        map struct enum identifier ignored_any
464    }
465}
466
467struct FormEnumAccess<'a>(&'a str);
468
469impl<'de, 'a> EnumAccess<'de> for FormEnumAccess<'a> {
470    type Error = FormError;
471    type Variant = FormVariantAccess;
472
473    fn variant_seed<V: DeserializeSeed<'de>>(
474        self,
475        seed: V,
476    ) -> Result<(V::Value, Self::Variant), FormError> {
477        let variant = seed.deserialize(StrDeserializer(self.0))?;
478        Ok((variant, FormVariantAccess))
479    }
480}
481
482struct FormVariantAccess;
483
484impl<'de> VariantAccess<'de> for FormVariantAccess {
485    type Error = FormError;
486
487    fn unit_variant(self) -> Result<(), FormError> {
488        Ok(())
489    }
490
491    fn newtype_variant_seed<T: DeserializeSeed<'de>>(
492        self,
493        _seed: T,
494    ) -> Result<T::Value, FormError> {
495        Err(FormError::custom(
496            "form fields cannot carry a newtype enum variant; use a unit variant",
497        ))
498    }
499
500    fn tuple_variant<V: Visitor<'de>>(
501        self,
502        _len: usize,
503        _visitor: V,
504    ) -> Result<V::Value, FormError> {
505        Err(FormError::custom(
506            "form fields cannot carry a tuple enum variant; use a unit variant",
507        ))
508    }
509
510    fn struct_variant<V: Visitor<'de>>(
511        self,
512        _fields: &'static [&'static str],
513        _visitor: V,
514    ) -> Result<V::Value, FormError> {
515        Err(FormError::custom(
516            "form fields cannot carry a struct enum variant; use a unit variant",
517        ))
518    }
519}
520
521// ---------------------------------------------------------------------------
522// Deserialize for FormFile
523// ---------------------------------------------------------------------------
524
525impl<'de> serde::Deserialize<'de> for FormFile {
526    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
527        deserializer.deserialize_map(FormFileVisitor)
528    }
529}
530
531struct FormFileVisitor;
532
533impl<'de> Visitor<'de> for FormFileVisitor {
534    type Value = FormFile;
535
536    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        f.write_str("an uploaded file part")
538    }
539
540    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<FormFile, A::Error> {
541        let mut handle: Option<u64> = None;
542        while let Some(key) = map.next_key::<String>()? {
543            if key == FILE_HANDLE_KEY {
544                handle = Some(map.next_value()?);
545            } else {
546                let _: de::IgnoredAny = map.next_value()?;
547            }
548        }
549
550        let Some(position) = handle else {
551            return Err(de::Error::custom(
552                "FormFile can only be bound from a multipart/form-data request",
553            ));
554        };
555
556        ActiveForm::resolve(position as usize).ok_or_else(|| {
557            de::Error::custom("FormFile can only be bound from a multipart/form-data request")
558        })
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565    use crate::form::FormFileBuilder;
566    use serde::Deserialize;
567
568    #[derive(Debug, Deserialize, PartialEq)]
569    struct Profile {
570        username: String,
571        age: u32,
572        verified: bool,
573        nickname: Option<String>,
574    }
575
576    fn text_form() -> MultipartForm {
577        let mut form = MultipartForm::new();
578        form.push_field("username", "ada");
579        form.push_field("age", "36");
580        form.push_field("verified", "true");
581        form
582    }
583
584    fn bind<T: serde::de::DeserializeOwned>(form: MultipartForm) -> Result<T, FormError> {
585        let form = std::sync::Arc::new(form);
586        let params = HashMap::new();
587        ActiveForm::run(std::sync::Arc::clone(&form), || {
588            T::deserialize(FormDeserializer::new(&form, &params))
589        })
590    }
591
592    #[test]
593    fn binds_text_fields_to_a_struct() {
594        let profile: Profile = bind(text_form()).expect("bind failed");
595        assert_eq!(
596            profile,
597            Profile {
598                username: "ada".into(),
599                age: 36,
600                verified: true,
601                nickname: None,
602            }
603        );
604    }
605
606    #[test]
607    fn reports_a_clear_error_for_bad_numbers() {
608        let mut form = MultipartForm::new();
609        form.push_field("username", "ada");
610        form.push_field("age", "not-a-number");
611        form.push_field("verified", "true");
612        let err: FormError = bind::<Profile>(form).unwrap_err();
613        assert!(err.to_string().contains("cannot parse"), "got {err}");
614    }
615
616    #[test]
617    fn route_params_override_form_fields() {
618        let form = std::sync::Arc::new(text_form());
619        let mut params = HashMap::new();
620        params.insert("username".to_string(), "grace".to_string());
621        let profile = ActiveForm::run(std::sync::Arc::clone(&form), || {
622            Profile::deserialize(FormDeserializer::new(&form, &params))
623        })
624        .unwrap();
625        assert_eq!(profile.username, "grace");
626    }
627
628    #[derive(Debug, Deserialize)]
629    struct Upload {
630        title: String,
631        avatar: FormFile,
632    }
633
634    #[tokio::test]
635    async fn binds_memory_backed_file_parts() {
636        let mut builder =
637            FormFileBuilder::new("avatar", "../evil/photo.png", Some("image/png".into()));
638        builder.write(b"\x89PNG").await.unwrap();
639        let mut form = MultipartForm::new();
640        form.push_field("title", "hi");
641        form.push_file(builder.finish().await.unwrap());
642
643        let upload: Upload = bind(form).unwrap();
644        assert_eq!(upload.title, "hi");
645        assert_eq!(upload.avatar.file_name(), "photo.png");
646        assert_eq!(upload.avatar.size(), 4);
647        assert_eq!(upload.avatar.read_bytes().await.unwrap(), b"\x89PNG");
648    }
649
650    #[derive(Debug, Deserialize)]
651    struct Gallery {
652        photos: Vec<FormFile>,
653    }
654
655    #[tokio::test]
656    async fn binds_repeated_file_parts_into_a_vec() {
657        let mut form = MultipartForm::new();
658        for (name, body) in [("a.png", &b"aaa"[..]), ("b.png", &b"bbbb"[..])] {
659            let mut builder = FormFileBuilder::new("photos", name, None);
660            builder.write(body).await.unwrap();
661            form.push_file(builder.finish().await.unwrap());
662        }
663        let gallery: Gallery = bind(form).unwrap();
664        assert_eq!(gallery.photos.len(), 2);
665        assert_eq!(gallery.photos[0].read_bytes().await.unwrap(), b"aaa");
666        assert_eq!(gallery.photos[1].read_bytes().await.unwrap(), b"bbbb");
667    }
668
669    #[tokio::test]
670    async fn spooled_files_outlive_the_form() {
671        let dir = tempfile::tempdir().unwrap();
672        let mut builder = FormFileBuilder::new("avatar", "big.bin", None)
673            .memory_threshold(1)
674            .spool_dir(dir.path().to_path_buf());
675        builder.write(b"0123456789").await.unwrap();
676        let mut form = MultipartForm::new();
677        form.push_file(builder.finish().await.unwrap());
678
679        let upload: Upload = {
680            let mut form = form.clone();
681            form.push_field("title", "hi");
682            bind(form).unwrap()
683        };
684        // The form itself is gone; the file handle must still own the bytes.
685        assert_eq!(upload.avatar.read_bytes().await.unwrap(), b"0123456789");
686    }
687
688    #[test]
689    fn json_cannot_smuggle_a_file_handle() {
690        let json = r#"{"title":"x","avatar":{"$webx_form_file":0}}"#;
691        let err = serde_json::from_str::<Upload>(json).unwrap_err();
692        assert!(
693            err.to_string().contains("multipart/form-data"),
694            "unexpected error: {err}"
695        );
696    }
697
698    #[test]
699    fn json_with_arbitrary_keys_is_rejected() {
700        let json = r#"{"title":"x","avatar":{"path":"/etc/passwd"}}"#;
701        let err = serde_json::from_str::<Upload>(json).unwrap_err();
702        assert!(
703            err.to_string().contains("multipart/form-data"),
704            "unexpected error: {err}"
705        );
706    }
707
708    #[derive(Debug, Deserialize, PartialEq)]
709    enum Kind {
710        Disk,
711        Ssd,
712    }
713
714    #[derive(Debug, Deserialize)]
715    struct WithEnum {
716        kind: Kind,
717    }
718
719    #[test]
720    fn binds_unit_enum_variants() {
721        let mut form = MultipartForm::new();
722        form.push_field("kind", "Ssd");
723        let bound: WithEnum = bind(form).unwrap();
724        assert_eq!(bound.kind, Kind::Ssd);
725    }
726}