mod attrs;
mod fields;
mod types;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Fields};
use self::attrs::{extract_strict, extract_struct_default};
use self::fields::{FieldCodegen, process_fields};
pub fn process_derive(input: &DeriveInput) -> TokenStream {
let struct_name = &input.ident;
let rename_all = crate::parser::extract_rename_all(&input.attrs);
let strict = extract_strict(&input.attrs);
let struct_default = extract_struct_default(&input.attrs);
let fields = match &input.data {
syn::Data::Struct(data) => match &data.fields {
Fields::Named(named) => &named.named,
_ => {
return syn::Error::new_spanned(
&input.ident,
"Multipart only supports structs with named fields",
)
.to_compile_error();
}
},
_ => {
return syn::Error::new_spanned(
&input.ident,
"Multipart can only be derived for structs",
)
.to_compile_error();
}
};
let cg = process_fields(fields.iter(), rename_all.as_deref(), strict, struct_default);
let unknown_field_arm = if strict {
quote! {
_ => {
return std::result::Result::Err(
vespera::multipart::TypedMultipartError::UnknownField {
field_name: std::string::String::from(__field_name__)
}
);
}
}
} else {
quote! { _ => {} }
};
let missing_name_fallback = if strict {
quote! {
return std::result::Result::Err(
vespera::multipart::TypedMultipartError::NamelessField
)
}
} else {
quote! { continue }
};
let FieldCodegen {
declarations,
assignments,
post_loop,
idents,
} = &cg;
quote! {
impl<__VesperaS__: Send + Sync> vespera::multipart::TryFromMultipartWithState<__VesperaS__> for #struct_name {
async fn try_from_multipart_with_state(
__multipart__: &mut vespera::axum::extract::Multipart,
__state__: &__VesperaS__,
) -> std::result::Result<Self, vespera::multipart::TypedMultipartError> {
#(#declarations)*
while let std::option::Option::Some(__field__) = __multipart__
.next_field().await
.map_err(vespera::multipart::TypedMultipartError::from)? {
vespera::multipart::register_multipart_part()?;
let __field__ =
vespera::multipart::MeteredField::__from_field(__field__);
let __field_name__ = match __field__.name() {
| std::option::Option::Some("")
| std::option::Option::None => #missing_name_fallback,
| std::option::Option::Some(__name__) => __name__,
};
match __field_name__ {
#(#assignments)*
#unknown_field_arm
}
}
#(#post_loop)*
std::result::Result::Ok(Self { #(#idents),* })
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_derive_basic_struct() {
let input: syn::DeriveInput =
syn::parse_str("struct MyForm { pub name: String, pub age: i32 }").unwrap();
let code = process_derive(&input).to_string();
assert!(code.contains("TryFromMultipartWithState"));
assert!(code.contains("MyForm"));
assert!(code.contains("\"name\""));
assert!(code.contains("\"age\""));
}
#[test]
fn test_process_derive_with_option_field() {
let input: syn::DeriveInput =
syn::parse_str("struct MyForm { pub name: String, pub bio: Option<String> }").unwrap();
let code = process_derive(&input).to_string();
assert!(code.contains("TryFromMultipartWithState"));
assert!(code.contains("Option :: None"));
}
#[test]
fn test_process_derive_with_vec_field() {
let input: syn::DeriveInput =
syn::parse_str("struct MyForm { pub name: String, pub tags: Vec<String> }").unwrap();
let code = process_derive(&input).to_string();
assert!(code.contains("Vec :: new"));
assert!(code.contains("push"));
}
#[test]
fn test_process_derive_strict_mode() {
let input: syn::DeriveInput =
syn::parse_str("#[try_from_multipart(strict)] struct MyForm { pub name: String }")
.unwrap();
let code = process_derive(&input).to_string();
assert!(code.contains("DuplicateField"));
assert!(code.contains("UnknownField"));
assert!(code.contains("NamelessField"));
}
#[test]
fn test_process_derive_with_rename_all() {
let input: syn::DeriveInput = syn::parse_str(
r#"#[serde(rename_all = "camelCase")] struct MyForm { pub user_name: String }"#,
)
.unwrap();
assert!(process_derive(&input).to_string().contains("\"userName\""));
}
#[test]
fn test_process_derive_with_serde_default() {
let input: syn::DeriveInput =
syn::parse_str("#[serde(default)] struct MyForm { pub count: i32 }").unwrap();
assert!(
process_derive(&input)
.to_string()
.contains("unwrap_or_default")
);
}
#[test]
fn test_process_derive_with_field_default_fn() {
let input: syn::DeriveInput =
syn::parse_str(r#"struct MyForm { #[serde(default = "my_default")] pub val: String }"#)
.unwrap();
let code = process_derive(&input).to_string();
assert!(code.contains("unwrap_or_else"));
assert!(code.contains("my_default"));
}
#[test]
fn test_process_derive_with_invalid_field_default_fn_emits_compile_error() {
let input: syn::DeriveInput = syn::parse_str(
r#"struct MyForm { #[serde(default = "1 not a path")] pub val: String }"#,
)
.unwrap();
let code = process_derive(&input).to_string();
assert!(code.contains("compile_error"));
assert!(code.contains("function path"));
}
#[test]
fn test_process_derive_non_struct_errors() {
let input: syn::DeriveInput = syn::parse_str("enum Foo { A, B }").unwrap();
assert!(process_derive(&input).to_string().contains("compile_error"));
}
#[test]
fn test_process_derive_tuple_struct_errors() {
let input: syn::DeriveInput = syn::parse_str("struct Foo(String, i32);").unwrap();
assert!(process_derive(&input).to_string().contains("compile_error"));
}
#[test]
fn test_process_derive_form_data_field_name() {
let input: syn::DeriveInput = syn::parse_str(
r#"struct MyForm { #[form_data(field_name = "custom")] pub data: String }"#,
)
.unwrap();
assert!(process_derive(&input).to_string().contains("\"custom\""));
}
#[test]
fn test_process_derive_form_data_default() {
let input: syn::DeriveInput =
syn::parse_str("struct MyForm { #[form_data(default)] pub count: i32 }").unwrap();
assert!(
process_derive(&input)
.to_string()
.contains("unwrap_or_default")
);
}
#[test]
fn test_process_derive_non_strict_no_duplicate_check() {
let input: syn::DeriveInput = syn::parse_str("struct MyForm { pub name: String }").unwrap();
let code = process_derive(&input).to_string();
assert!(!code.contains("DuplicateField"));
assert!(!code.contains("UnknownField"));
}
#[test]
fn test_process_derive_file_field_without_limit_uses_runtime_default() {
let input: syn::DeriveInput =
syn::parse_str("struct Up { pub file: FieldData<NamedTempFile> }").unwrap();
let code = process_derive(&input).to_string();
assert!(
code.contains("Option :: None"),
"a file field without a limit should pass None to runtime default cap: {code}"
);
}
#[test]
fn test_process_derive_optional_file_field_without_limit_uses_runtime_default() {
let input: syn::DeriveInput =
syn::parse_str("struct Up { pub file: Option<FieldData<NamedTempFile>> }").unwrap();
assert!(
process_derive(&input)
.to_string()
.contains("Option :: None"),
"an Option-wrapped file field without a limit should use runtime default cap"
);
}
#[test]
fn test_process_derive_file_field_with_limit_ok() {
let input: syn::DeriveInput = syn::parse_str(
r#"struct Up { #[form_data(limit = "10MiB")] pub file: FieldData<NamedTempFile> }"#,
)
.unwrap();
let code = process_derive(&input).to_string();
assert!(
!code.contains("compile_error"),
"a file field with an explicit size limit must compile: {code}"
);
}
#[test]
fn test_process_derive_file_field_with_unlimited_ok() {
let input: syn::DeriveInput = syn::parse_str(
r#"struct Up { #[form_data(limit = "unlimited")] pub file: FieldData<NamedTempFile> }"#,
)
.unwrap();
assert!(
!process_derive(&input).to_string().contains("compile_error"),
"an explicit `unlimited` opt-out must compile"
);
}
#[test]
fn test_process_derive_non_file_field_without_limit_ok() {
let input: syn::DeriveInput =
syn::parse_str("struct Up { pub name: String, pub tags: Option<String> }").unwrap();
assert!(
!process_derive(&input).to_string().contains("compile_error"),
"non-file fields must not require a limit"
);
}
}