vespera_macro 0.4.0

Procedural macros for Vespera - automatic route discovery and OpenAPI generation
Documentation
//! Vespera's `Multipart` derive macro implementation.
//!
//! This is a re-implementation of `axum_typed_multipart`'s derive macro that
//! natively supports `#[serde(rename_all)]` and `#[serde(rename)]` attributes
//! for field name resolution in multipart form data.
//!
//! ## Why?
//!
//! `axum_typed_multipart`'s derive macro only reads `#[try_from_multipart(rename_all)]`
//! and ignores `#[serde(rename_all)]`. This causes a mismatch: the OpenAPI spec
//! (generated by `Schema` derive) shows camelCase field names, but the runtime
//! multipart parser expects snake_case Rust field names.
//!
//! ## Field Name Resolution Priority
//!
//! 1. `#[form_data(field_name = "...")]` — explicit override (highest priority)
//! 2. `#[serde(rename = "...")]` — serde field rename
//! 3. `#[serde(rename_all = "...")]` or `#[try_from_multipart(rename_all = "...")]` applied to Rust name
//! 4. Rust field name as-is (lowest priority)

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};

/// Process the `#[derive(TryFromMultipart)]` macro input.
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);

    // Wildcard arm of the field-dispatch `match`: strict mode rejects an
    // unknown field name; non-strict ignores it.  Replaces the trailing
    // `else { ... }` of the previous `if __field_name__ == "..." else if`
    // chain.  Cold path: the owned name is allocated only on rejection.
    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)? {
                    // Count EVERY wire part against the request-wide `max_fields`
                    // cap up front — before name resolution / dispatch — so
                    // unknown parts (the `_ => {}` non-strict arm) and nameless
                    // parts cannot slip past the limit the per-field parsers
                    // formerly enforced only for known fields.
                    vespera::multipart::register_multipart_part()?;
                    // Wrap the raw axum field so EVERY byte a parser reads is
                    // metered against the request-wide `max_total_bytes` cap —
                    // even a hand-written custom `TryFromFieldWithState` cannot
                    // bypass the aggregate limit (it reads through MeteredField).
                    let __field__ =
                        vespera::multipart::MeteredField::__from_field(__field__);
                    // Borrowed `&str` — NLL ends the borrow on each match
                    // arm before `__field__` is consumed by the parser, so
                    // no per-field `String` allocation is needed.
                    let __field_name__ = match __field__.name() {
                        | std::option::Option::Some("")
                        | std::option::Option::None => #missing_name_fallback,
                        | std::option::Option::Some(__name__) => __name__,
                    };

                    // Dispatch by resolved field name — a `match` over the
                    // field-name string literals instead of an
                    // `if __field_name__ == "..." else if ...` chain.
                    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() {
        // A malformed `#[serde(default = "...")]` function path must surface as
        // a clean span-attached compile_error, NOT panic the macro. The test
        // running to completion proves the former `.expect(...)` panic is gone.
        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"));
    }

    // ── File-field upload-limit defaults (runtime) ───────────

    #[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() {
        // Non-file fields keep their prior behaviour — no limit required.
        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"
        );
    }
}