improve macro compile errors

This commit is contained in:
Rob Ede 2022-11-26 00:33:17 +00:00
parent d3199d4f77
commit 6bd94ac3c5
No known key found for this signature in database
GPG Key ID: 97C636207D3EF933
5 changed files with 105 additions and 75 deletions

View File

@ -10,17 +10,11 @@ use std::{collections::HashSet, convert::TryFrom as _};
use darling::{FromDeriveInput, FromField, FromMeta};
use parse_size::parse_size;
use proc_macro2::Ident;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{parse_macro_input, Type};
#[derive(FromDeriveInput, Default)]
#[darling(attributes(multipart), default)]
struct MultipartFormAttrs {
deny_unknown_fields: bool,
duplicate_field: DuplicateField,
}
#[derive(FromMeta)]
enum DuplicateField {
Ignore,
@ -34,6 +28,13 @@ impl Default for DuplicateField {
}
}
#[derive(FromDeriveInput, Default)]
#[darling(attributes(multipart), default)]
struct MultipartFormAttrs {
deny_unknown_fields: bool,
duplicate_field: DuplicateField,
}
#[derive(FromField, Default)]
#[darling(attributes(multipart), default)]
struct FieldAttrs {
@ -51,16 +52,13 @@ struct ParsedField<'t> {
/// Implements the `MultipartFormTrait` for a struct so that it can be used with the `MultipartForm`
/// extractor.
///
/// # Examples
///
/// ## Basic Use
/// # Basic Use
///
/// Each field type should implement the `FieldReader` trait:
///
/// ```
/// # use actix_multipart::form::tempfile::Tempfile;
/// # use actix_multipart::form::text::Text;
/// # use actix_multipart::form::MultipartForm;
/// use actix_multipart::form::{tempfile::Tempfile, text::Text, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct ImageUpload {
/// description: Text<String>,
@ -69,17 +67,16 @@ struct ParsedField<'t> {
/// }
/// ```
///
/// ## Optional and List Fields
/// # Optional and List Fields
///
/// You can also use `Vec<T>` and `Option<T>` provided that `T: FieldReader`.
///
/// A [`Vec`] field corresponds to an upload with multiple parts under the
/// [same field name](https://www.rfc-editor.org/rfc/rfc7578#section-4.3).
/// A [`Vec`] field corresponds to an upload with multiple parts under the [same field
/// name](https://www.rfc-editor.org/rfc/rfc7578#section-4.3).
///
/// ```
/// # use actix_multipart::form::tempfile::Tempfile;
/// # use actix_multipart::form::text::Text;
/// # use actix_multipart::form::MultipartForm;
/// use actix_multipart::form::{tempfile::Tempfile, text::Text, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct Form {
/// category: Option<Text<String>>,
@ -87,13 +84,13 @@ struct ParsedField<'t> {
/// }
/// ```
///
/// ## Field Renaming
/// # Field Renaming
///
/// You can use the `#[multipart(rename = "foo")]` attribute to receive a field by a different name.
///
/// ```
/// # use actix_multipart::form::tempfile::Tempfile;
/// # use actix_multipart::form::MultipartForm;
/// use actix_multipart::form::{tempfile::Tempfile, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct Form {
/// #[multipart(rename = "files[]")]
@ -101,7 +98,7 @@ struct ParsedField<'t> {
/// }
/// ```
///
/// ## Field Limits
/// # Field Limits
///
/// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit
/// string is parsed using [parse_size].
@ -109,40 +106,41 @@ struct ParsedField<'t> {
/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
///
/// ```
/// # use actix_multipart::form::tempfile::Tempfile;
/// # use actix_multipart::form::text::Text;
/// # use actix_multipart::form::MultipartForm;
/// use actix_multipart::form::{tempfile::Tempfile, text::Text, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct Form {
/// #[multipart(limit = "2KiB")]
/// #[multipart(limit = "2 KiB")]
/// description: Text<String>,
/// #[multipart(limit = "512MiB")]
///
/// #[multipart(limit = "512 MiB")]
/// files: Vec<Tempfile>,
/// }
/// ```
///
/// ## Unknown Fields
/// # Unknown Fields
///
/// By default fields with an unknown name are ignored. You can change this using the
/// By default fields with an unknown name are ignored. They can be rejected using the
/// `#[multipart(deny_unknown_fields)]` attribute:
///
/// ```
/// # use actix_multipart::form::MultipartForm;
/// use actix_multipart::form::MultipartForm;
///
/// #[derive(MultipartForm)]
/// #[multipart(deny_unknown_fields)]
/// struct Form { }
/// ```
///
/// ## Duplicate Fields
/// # Duplicate Fields
///
/// You can change the behaviour for when multiple fields are received with the same name using the
/// `#[multipart(duplicate_field = "")]` attribute:
/// The behaviour for when multiple fields with the same name are received can be changed using the
/// `#[multipart(duplicate_field = "<behavior>")]` attribute:
///
/// - "ignore": Extra fields are ignored (default).
/// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted.
/// - "deny": A `MultipartError::UnsupportedField` error response is returned.
/// - "replace": Each field is processed, but only the last one is persisted.
/// - "deny": A `MultipartError::UnsupportedField` error is returned.
///
/// (Note this option does not apply to `Vec` fields)
/// Note that `Vec` fields will ignore this option.
///
/// ```
/// # use actix_multipart::form::MultipartForm;
@ -157,16 +155,34 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
let input: syn::DeriveInput = parse_macro_input!(input);
let name = &input.ident;
let str = match &input.data {
syn::Data::Struct(s) => s,
_ => panic!("This trait can only be derived for a struct"),
};
let fields = match &str.fields {
syn::Fields::Named(n) => n,
_ => panic!("This trait can only be derived for a struct"),
let data_struct = match &input.data {
syn::Data::Struct(data_struct) => data_struct,
_ => {
return TokenStream::from(
syn::Error::new(
Span::call_site(),
"This trait can only be derived for a struct",
)
.to_compile_error(),
)
}
};
let attrs: MultipartFormAttrs = match MultipartFormAttrs::from_derive_input(&input) {
let fields = match &data_struct.fields {
syn::Fields::Named(fields_named) => fields_named,
_ => {
return TokenStream::from(
syn::Error::new(
Span::call_site(),
"This trait can only be derived for a struct with named fields",
)
.to_compile_error(),
)
}
};
let attrs = match MultipartFormAttrs::from_derive_input(&input) {
Ok(attrs) => attrs,
Err(err) => return err.write_errors().into(),
};
@ -177,16 +193,19 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
.iter()
.map(|field| {
let rust_name = field.ident.as_ref().unwrap();
let attrs: FieldAttrs = FieldAttrs::from_field(field)?;
let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?;
let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string());
let limit = attrs.limit.map(|limit| {
usize::try_from(
parse_size(&limit)
.unwrap_or_else(|_| panic!("Unable to parse limit `{}`", limit)),
)
.unwrap()
});
let limit = match attrs.limit.map(|limit| match parse_size(&limit) {
Ok(size) => Ok(usize::try_from(size).unwrap()),
Err(err) => Err(syn::Error::new(
field.ident.as_ref().unwrap().span(),
format!("Unable to parse limit `{}`: {}", limit, err),
)),
}) {
Some(Err(err)) => return Err(TokenStream::from(err.to_compile_error())),
limit => limit.map(Result::unwrap),
};
Ok(ParsedField {
serialization_name,
@ -195,17 +214,23 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
ty: &field.ty,
})
})
.collect::<Result<Vec<_>, darling::Error>>()
.collect::<Result<Vec<_>, TokenStream>>()
{
Ok(attrs) => attrs,
Err(err) => return err.write_errors().into(),
Err(err) => return err,
};
// Check that field names are unique
let mut set = HashSet::new();
for f in &parsed {
if !set.insert(f.serialization_name.clone()) {
panic!("Multiple fields named: `{}`", f.serialization_name);
for field in &parsed {
if !set.insert(field.serialization_name.clone()) {
return TokenStream::from(
syn::Error::new(
field.rust_name.span(),
format!("Multiple fields named: `{}`", field.serialization_name),
)
.to_compile_error(),
);
}
}
@ -230,6 +255,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
for field in &parsed {
let name = &field.serialization_name;
let ty = &field.ty;
read_field_impl.extend(quote!(
#name => ::std::boxed::Box::pin(
<#ty as ::actix_multipart::form::FieldGroupReader>::handle_field(req, field, limits, state, #duplicate_field)

View File

@ -1,13 +1,14 @@
# Changes
## Unreleased - 2022-xx-xx
- Added `MultipartForm` typed data extractor. [#2883]
- `Field::content_type()` now returns `Option<&mime::Mime>`. [#2880]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2880].
- Added `MultipartForm` typed data extractor [#2883].
[#2880]: https://github.com/actix/actix-web/pull/2880
[#2883]: https://github.com/actix/actix-web/pull/2883
## 0.4.0 - 2022-02-25
- No significant changes since `0.4.0-beta.13`.

View File

@ -24,7 +24,6 @@ path = "src/lib.rs"
[dependencies]
actix-multipart-derive = { version = "=0.4.0", optional = true }
actix-http = "3"
actix-utils = "3"
actix-web = { version = "4", default-features = false }
@ -37,14 +36,15 @@ local-waker = "0.1"
log = "0.4"
memchr = "2.5"
mime = "0.3"
serde = "1.0"
serde_json = "1.0"
serde_plain = "1.0"
serde = "1"
serde_json = "1"
serde_plain = "1"
# TODO(MSRV 1.60): replace with dep: prefix
tempfile-dep = { package = "tempfile", version = "3.3.0", optional = true }
tokio = { version = "1.13.1", features = ["sync"] }
[dev-dependencies]
actix-http = "3"
actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2"
actix-test = "0.1.0"

View File

@ -26,7 +26,10 @@ impl<T: DeserializeOwned> Text<T> {
}
}
impl<'t, T: DeserializeOwned + 'static> FieldReader<'t> for Text<T> {
impl<'t, T> FieldReader<'t> for Text<T>
where
T: DeserializeOwned + 'static,
{
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
@ -43,7 +46,7 @@ impl<'t, T: DeserializeOwned + 'static> FieldReader<'t> for Text<T> {
true
};
if !valid && config.validate_content_type {
if !valid {
return Err(MultipartError::Field {
field_name,
source: config.map_error(req, TextError::ContentType),

View File

@ -864,7 +864,7 @@ impl PayloadBuffer {
mod tests {
use std::time::Duration;
use actix_http::h1::Payload;
use actix_http::h1;
use actix_web::{
http::header::{DispositionParam, DispositionType},
rt,
@ -1124,7 +1124,7 @@ mod tests {
#[actix_rt::test]
async fn test_basic() {
let (_, payload) = Payload::create(false);
let (_, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(payload.buf.len(), 0);
@ -1134,7 +1134,7 @@ mod tests {
#[actix_rt::test]
async fn test_eof() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_max(4).unwrap());
@ -1150,7 +1150,7 @@ mod tests {
#[actix_rt::test]
async fn test_err() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_max(1).unwrap());
sender.set_error(PayloadError::Incomplete(None));
@ -1159,7 +1159,7 @@ mod tests {
#[actix_rt::test]
async fn test_readmax() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
sender.feed_data(Bytes::from("line1"));
@ -1176,7 +1176,7 @@ mod tests {
#[actix_rt::test]
async fn test_readexactly() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_exact(2));
@ -1194,7 +1194,7 @@ mod tests {
#[actix_rt::test]
async fn test_readuntil() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_until(b"ne").unwrap());
@ -1235,7 +1235,7 @@ mod tests {
#[actix_rt::test]
async fn test_multipart_payload_consumption() {
// with sample payload and HttpRequest with no headers
let (_, inner_payload) = Payload::create(false);
let (_, inner_payload) = h1::Payload::create(false);
let mut payload = actix_web::dev::Payload::from(inner_payload);
let req = TestRequest::default().to_http_request();