Skip to main content

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