Skip to content

Commit d4b833c

Browse files
jacob-prorobjtede
andauthored
actix-multipart: Feature: Add typed multipart form extractor (#2883)
Co-authored-by: Rob Ede <[email protected]>
1 parent 358c1cf commit d4b833c

30 files changed

+2017
-32
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ members = [
55
"actix-http-test",
66
"actix-http",
77
"actix-multipart",
8+
"actix-multipart-derive",
89
"actix-router",
910
"actix-test",
1011
"actix-web-actors",
@@ -27,6 +28,7 @@ actix-files = { path = "actix-files" }
2728
actix-http = { path = "actix-http" }
2829
actix-http-test = { path = "actix-http-test" }
2930
actix-multipart = { path = "actix-multipart" }
31+
actix-multipart-derive = { path = "actix-multipart-derive" }
3032
actix-router = { path = "actix-router" }
3133
actix-test = { path = "actix-test" }
3234
actix-web = { path = "actix-web" }

actix-multipart-derive/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "actix-multipart-derive"
3+
version = "0.5.0"
4+
authors = ["Jacob Halsey <[email protected]>"]
5+
description = "Multipart form derive macro for Actix Web"
6+
keywords = ["http", "web", "framework", "async", "futures"]
7+
homepage = "https://actix.rs"
8+
repository = "https://github.com/actix/actix-web.git"
9+
license = "MIT OR Apache-2.0"
10+
edition = "2018"
11+
12+
[lib]
13+
proc-macro = true
14+
15+
[dependencies]
16+
darling = "0.14"
17+
parse-size = "1"
18+
proc-macro2 = "1"
19+
quote = "1"
20+
syn = "1"
21+
22+
[dev-dependencies]
23+
actix-multipart = "0.5"
24+
actix-web = "4"
25+
rustversion = "1"
26+
trybuild = "1"

actix-multipart-derive/LICENSE-APACHE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../LICENSE-APACHE

actix-multipart-derive/LICENSE-MIT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../LICENSE-MIT

actix-multipart-derive/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# actix-multipart-derive
2+
3+
> The derive macro implementation for actix-multipart.

actix-multipart-derive/src/lib.rs

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
//! Multipart form derive macro for Actix Web.
2+
//!
3+
//! See [`macro@MultipartForm`] for usage examples.
4+
5+
#![deny(rust_2018_idioms, nonstandard_style)]
6+
#![warn(future_incompatible)]
7+
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
8+
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
9+
#![cfg_attr(docsrs, feature(doc_cfg))]
10+
11+
use std::{collections::HashSet, convert::TryFrom as _};
12+
13+
use darling::{FromDeriveInput, FromField, FromMeta};
14+
use parse_size::parse_size;
15+
use proc_macro::TokenStream;
16+
use proc_macro2::Ident;
17+
use quote::quote;
18+
use syn::{parse_macro_input, Type};
19+
20+
#[derive(FromMeta)]
21+
enum DuplicateField {
22+
Ignore,
23+
Deny,
24+
Replace,
25+
}
26+
27+
impl Default for DuplicateField {
28+
fn default() -> Self {
29+
Self::Ignore
30+
}
31+
}
32+
33+
#[derive(FromDeriveInput, Default)]
34+
#[darling(attributes(multipart), default)]
35+
struct MultipartFormAttrs {
36+
deny_unknown_fields: bool,
37+
duplicate_field: DuplicateField,
38+
}
39+
40+
#[derive(FromField, Default)]
41+
#[darling(attributes(multipart), default)]
42+
struct FieldAttrs {
43+
rename: Option<String>,
44+
limit: Option<String>,
45+
}
46+
47+
struct ParsedField<'t> {
48+
serialization_name: String,
49+
rust_name: &'t Ident,
50+
limit: Option<usize>,
51+
ty: &'t Type,
52+
}
53+
54+
/// Implements `MultipartCollect` for a struct so that it can be used with the `MultipartForm`
55+
/// extractor.
56+
///
57+
/// # Basic Use
58+
///
59+
/// Each field type should implement the `FieldReader` trait:
60+
///
61+
/// ```
62+
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
63+
///
64+
/// #[derive(MultipartForm)]
65+
/// struct ImageUpload {
66+
/// description: Text<String>,
67+
/// timestamp: Text<i64>,
68+
/// image: TempFile,
69+
/// }
70+
/// ```
71+
///
72+
/// # Optional and List Fields
73+
///
74+
/// You can also use `Vec<T>` and `Option<T>` provided that `T: FieldReader`.
75+
///
76+
/// A [`Vec`] field corresponds to an upload with multiple parts under the [same field
77+
/// name](https://www.rfc-editor.org/rfc/rfc7578#section-4.3).
78+
///
79+
/// ```
80+
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
81+
///
82+
/// #[derive(MultipartForm)]
83+
/// struct Form {
84+
/// category: Option<Text<String>>,
85+
/// files: Vec<TempFile>,
86+
/// }
87+
/// ```
88+
///
89+
/// # Field Renaming
90+
///
91+
/// You can use the `#[multipart(rename = "foo")]` attribute to receive a field by a different name.
92+
///
93+
/// ```
94+
/// use actix_multipart::form::{tempfile::TempFile, MultipartForm};
95+
///
96+
/// #[derive(MultipartForm)]
97+
/// struct Form {
98+
/// #[multipart(rename = "files[]")]
99+
/// files: Vec<TempFile>,
100+
/// }
101+
/// ```
102+
///
103+
/// # Field Limits
104+
///
105+
/// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit
106+
/// string is parsed using [parse_size].
107+
///
108+
/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
109+
///
110+
/// ```
111+
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
112+
///
113+
/// #[derive(MultipartForm)]
114+
/// struct Form {
115+
/// #[multipart(limit = "2 KiB")]
116+
/// description: Text<String>,
117+
///
118+
/// #[multipart(limit = "512 MiB")]
119+
/// files: Vec<TempFile>,
120+
/// }
121+
/// ```
122+
///
123+
/// # Unknown Fields
124+
///
125+
/// By default fields with an unknown name are ignored. They can be rejected using the
126+
/// `#[multipart(deny_unknown_fields)]` attribute:
127+
///
128+
/// ```
129+
/// # use actix_multipart::form::MultipartForm;
130+
/// #[derive(MultipartForm)]
131+
/// #[multipart(deny_unknown_fields)]
132+
/// struct Form { }
133+
/// ```
134+
///
135+
/// # Duplicate Fields
136+
///
137+
/// The behaviour for when multiple fields with the same name are received can be changed using the
138+
/// `#[multipart(duplicate_field = "<behavior>")]` attribute:
139+
///
140+
/// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted.
141+
/// - "deny": A `MultipartError::UnsupportedField` error response is returned.
142+
/// - "replace": Each field is processed, but only the last one is persisted.
143+
///
144+
/// Note that `Vec` fields will ignore this option.
145+
///
146+
/// ```
147+
/// # use actix_multipart::form::MultipartForm;
148+
/// #[derive(MultipartForm)]
149+
/// #[multipart(duplicate_field = "deny")]
150+
/// struct Form { }
151+
/// ```
152+
///
153+
/// [parse_size]: https://docs.rs/parse-size/1/parse_size
154+
#[proc_macro_derive(MultipartForm, attributes(multipart))]
155+
pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
156+
let input: syn::DeriveInput = parse_macro_input!(input);
157+
158+
let name = &input.ident;
159+
160+
let data_struct = match &input.data {
161+
syn::Data::Struct(data_struct) => data_struct,
162+
_ => {
163+
return compile_err(syn::Error::new(
164+
input.ident.span(),
165+
"`MultipartForm` can only be derived for structs",
166+
))
167+
}
168+
};
169+
170+
let fields = match &data_struct.fields {
171+
syn::Fields::Named(fields_named) => fields_named,
172+
_ => {
173+
return compile_err(syn::Error::new(
174+
input.ident.span(),
175+
"`MultipartForm` can only be derived for a struct with named fields",
176+
))
177+
}
178+
};
179+
180+
let attrs = match MultipartFormAttrs::from_derive_input(&input) {
181+
Ok(attrs) => attrs,
182+
Err(err) => return err.write_errors().into(),
183+
};
184+
185+
// Parse the field attributes
186+
let parsed = match fields
187+
.named
188+
.iter()
189+
.map(|field| {
190+
let rust_name = field.ident.as_ref().unwrap();
191+
let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?;
192+
let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string());
193+
194+
let limit = match attrs.limit.map(|limit| match parse_size(&limit) {
195+
Ok(size) => Ok(usize::try_from(size).unwrap()),
196+
Err(err) => Err(syn::Error::new(
197+
field.ident.as_ref().unwrap().span(),
198+
format!("Could not parse size limit `{}`: {}", limit, err),
199+
)),
200+
}) {
201+
Some(Err(err)) => return Err(compile_err(err)),
202+
limit => limit.map(Result::unwrap),
203+
};
204+
205+
Ok(ParsedField {
206+
serialization_name,
207+
rust_name,
208+
limit,
209+
ty: &field.ty,
210+
})
211+
})
212+
.collect::<Result<Vec<_>, TokenStream>>()
213+
{
214+
Ok(attrs) => attrs,
215+
Err(err) => return err,
216+
};
217+
218+
// Check that field names are unique
219+
let mut set = HashSet::new();
220+
for field in &parsed {
221+
if !set.insert(field.serialization_name.clone()) {
222+
return compile_err(syn::Error::new(
223+
field.rust_name.span(),
224+
format!("Multiple fields named: `{}`", field.serialization_name),
225+
));
226+
}
227+
}
228+
229+
// Return value when a field name is not supported by the form
230+
let unknown_field_result = if attrs.deny_unknown_fields {
231+
quote!(::std::result::Result::Err(
232+
::actix_multipart::MultipartError::UnsupportedField(field.name().to_string())
233+
))
234+
} else {
235+
quote!(::std::result::Result::Ok(()))
236+
};
237+
238+
// Value for duplicate action
239+
let duplicate_field = match attrs.duplicate_field {
240+
DuplicateField::Ignore => quote!(::actix_multipart::form::DuplicateField::Ignore),
241+
DuplicateField::Deny => quote!(::actix_multipart::form::DuplicateField::Deny),
242+
DuplicateField::Replace => quote!(::actix_multipart::form::DuplicateField::Replace),
243+
};
244+
245+
// limit() implementation
246+
let mut limit_impl = quote!();
247+
for field in &parsed {
248+
let name = &field.serialization_name;
249+
if let Some(value) = field.limit {
250+
limit_impl.extend(quote!(
251+
#name => ::std::option::Option::Some(#value),
252+
));
253+
}
254+
}
255+
256+
// handle_field() implementation
257+
let mut handle_field_impl = quote!();
258+
for field in &parsed {
259+
let name = &field.serialization_name;
260+
let ty = &field.ty;
261+
262+
handle_field_impl.extend(quote!(
263+
#name => ::std::boxed::Box::pin(
264+
<#ty as ::actix_multipart::form::FieldGroupReader>::handle_field(req, field, limits, state, #duplicate_field)
265+
),
266+
));
267+
}
268+
269+
// from_state() implementation
270+
let mut from_state_impl = quote!();
271+
for field in &parsed {
272+
let name = &field.serialization_name;
273+
let rust_name = &field.rust_name;
274+
let ty = &field.ty;
275+
from_state_impl.extend(quote!(
276+
#rust_name: <#ty as ::actix_multipart::form::FieldGroupReader>::from_state(#name, &mut state)?,
277+
));
278+
}
279+
280+
let gen = quote! {
281+
impl ::actix_multipart::form::MultipartCollect for #name {
282+
fn limit(field_name: &str) -> ::std::option::Option<usize> {
283+
match field_name {
284+
#limit_impl
285+
_ => None,
286+
}
287+
}
288+
289+
fn handle_field<'t>(
290+
req: &'t ::actix_web::HttpRequest,
291+
field: ::actix_multipart::Field,
292+
limits: &'t mut ::actix_multipart::form::Limits,
293+
state: &'t mut ::actix_multipart::form::State,
294+
) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::actix_multipart::MultipartError>> + 't>> {
295+
match field.name() {
296+
#handle_field_impl
297+
_ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)),
298+
}
299+
}
300+
301+
fn from_state(mut state: ::actix_multipart::form::State) -> ::std::result::Result<Self, ::actix_multipart::MultipartError> {
302+
Ok(Self {
303+
#from_state_impl
304+
})
305+
}
306+
307+
}
308+
};
309+
gen.into()
310+
}
311+
312+
/// Transform a syn error into a token stream for returning.
313+
fn compile_err(err: syn::Error) -> TokenStream {
314+
TokenStream::from(err.to_compile_error())
315+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#[rustversion::stable(1.59)] // MSRV
2+
#[test]
3+
fn compile_macros() {
4+
let t = trybuild::TestCases::new();
5+
6+
t.pass("tests/trybuild/all-required.rs");
7+
t.pass("tests/trybuild/optional-and-list.rs");
8+
t.pass("tests/trybuild/rename.rs");
9+
t.pass("tests/trybuild/deny-unknown.rs");
10+
11+
t.pass("tests/trybuild/deny-duplicates.rs");
12+
t.compile_fail("tests/trybuild/deny-parse-fail.rs");
13+
14+
t.pass("tests/trybuild/size-limits.rs");
15+
t.compile_fail("tests/trybuild/size-limit-parse-fail.rs");
16+
}

0 commit comments

Comments
 (0)