actix-web
                                
                                 actix-web copied to clipboard
                                
                                    actix-web copied to clipboard
                            
                            
                            
                        actix-multipart: Feature: Add typed multipart form extractor
PR Type
Feature
PR Checklist
- [x] Tests for the changes have been added / updated.
- [x] Documentation comments have been added / updated.
- [ ] A changelog entry has been made for the appropriate packages.
- [x] Format code with the latest stable rustfmt.
- [ ] (Team) Label with affected crates and semver status.
Overview
This introduces a new multipart form/data extractor for recieving a multipart upload into a struct.
I believe this should be enough to cover all the various features of the existing similar crates as per the discussion here: https://github.com/actix/actix-web/issues/2849
An example form looks like:
#[derive(MultipartForm)]
struct Upload {
    description: Option<Text<String>>,
    timestamp: Text<i64>,
    #[multipart(rename="image_set[]")
    image_set: Vec<Tempfile>,
}
async fn route(form: MultipartForm<Upload>) -> impl Responder {
    ...
}
The key feature is that each field just needs to implement the FieldReader trait. This trait allows the user to provide their own abitarary async handler for processing a field, for example they may want to stream the data to S3.
I look forward to your feedback! @robjtede @asonix @JSH32 @e-rhodes
List of features:
- [x] Optional fields
- [x] Lists/Vec fields (see RFC)
- [x] Field renaming
- [x] User customisable field handlers
- [x] Global and field level data limits
- [x] Configurable action on duplicate fields
- [x] Allow denying unknown fields
- [x] User customisable error messages
- [x] Stream fields to temporary files
- [x] Deserialize integers, floats, enums from plain text fields
- [x] Deserialize complex structs from JSON fields
Looks good! What is the purpose of using TextField instead of String directly? Can you show an example of how someone might implement their own field or make their own field compatible with TextField? (just for making it easy to reference later on)
@JSH32 yep that is a good question.
We want to allow reading into not just String itself, but also things like integers, floats, enums, i.e. Text<T: DeserializeOwned>. This leads to two problems:
Trait Conflicts
But we can't simultaneously implement FieldReader for any T: DeserializeOwned, whilst also allowing the user to use native Vec and Option, giving us two choices:
Choice 1 (what I have implemented)
#[derive(MultipartForm)]
struct Upload {
    numbers: Vec<Text<i64>>,
}
Choice 2 (we would have to use Vec and Option wrapper types)
#[derive(MultipartForm)]
struct Upload {
    numbers: VecWrapper<i64>,
}
This is because of conflicting trait implementations (although one day this might be solved by specialization)
impl<'t, T: DeserializeOwned> FieldReader<'t> for T 
error[E0119]: conflicting implementations of trait `form::FieldGroupReader<'_>` for type `std::option::Option<_>`
   --> actix-multipart\src\form\mod.rs:233:1
    |
158 | / impl<'t, T> FieldGroupReader<'t> for Option<T>
159 | | where
160 | |     T: FieldReader<'t>,
161 | | {
...   |
194 | |     }
195 | | }
    | |_- first implementation here
...
233 | / impl<'t, T> FieldGroupReader<'t> for T
234 | | where
235 | |     T: FieldReader<'t>,
236 | | {
...   |
272 | |     }
273 | | }
    | |_^ conflicting implementation for `std::option::Option<_>`
For more information about this error, try `rustc --explain E0119`.
(Technically we could implement directly for String itself (rather than generic T), but it seems pointless since we would still need Text<T: DeserializeOwned> to work also)
Deserialization Ambiguity
The multipart standard doesn't define any serialization format for the data within the fields themselves. So even though we may want to use serde to automatically deserialize the contents of a field, there is no correct answer to which serde backend to use.
Instead by using the Text type it allows the user to specically opt-in to using serde_plain. Alternatively they can use the Json type, and the text would be deserialized using serde_json instead.
For example:
#[derive(MultipartForm)]
struct DeserializationMethods {
    json: Json<HashMap<String, String>>,
    plain: Text<String>,
}
async fn send() {
        let mut form = multipart::Form::default();
        // We can send the exact same input, but it is up to the server to choose how to deserialize it
        form.add_text("json", "{\"key1\": \"value1\", \"key2\": \"value2\"}");
        form.add_text("plain", "{\"key1\": \"value1\", \"key2\": \"value2\"}");
        ...
}
Compatibility
Can you show an example of how someone might implement their own field or make their own field compatible with TextField
I'm not 100% sure what you mean by this - but the idea is that Text works with any types that implement Deserialize  (provided that they are compatible with serde_plain)
If you wanted to use a more complex type e.g. arbitrary structs & complex enums you could use Json instead.
If you wanted to implement your own field type, then you just need to impl<'t> FieldReader<'t> for YourField - have a look at Bytes, Json, Text, or Tempfile as examples
@robjtede when you have time, please can you review this PR - and let me know what you think?
Sorry for the mega delay; finally getting around to reviewing this. Excited by what I've gone through so far :)
Thanks for looking at this @robjtede - I'm glad you figured out some improvements to the trait name 😆 !
What is the status of this PR? This is excellent work and I'd really love to have an "official" way to handle forms that have text fields as well as binary files. I am currently evaluating actix-easy-multipart v3.0.0 but I'd rather use actix-multipart with the same features. Thanks @jacob-pro for the hard work!
Any status update?
Thanks for the nudge @DuckyBlender.
Added some trybuild tests. Once CI passes we'll get this merged and released 🎉