use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Fields, Type};
use crate::parser::{extract_default, extract_field_rename, extract_rename_all, rename_field};
struct FieldCodegen<'a> {
declarations: Vec<TokenStream>,
assignments: Vec<TokenStream>,
post_loop: Vec<TokenStream>,
idents: Vec<&'a syn::Ident>,
}
enum DefaultKind {
None,
Trait,
Function(String),
}
fn process_fields<'a>(
fields: impl Iterator<Item = &'a syn::Field>,
rename_all: Option<&str>,
strict: bool,
struct_default: bool,
) -> FieldCodegen<'a> {
let mut cg = FieldCodegen {
declarations: Vec::new(),
assignments: Vec::new(),
post_loop: Vec::new(),
idents: Vec::new(),
};
for field in fields {
let ident = field.ident.as_ref().unwrap();
let ty = &field.ty;
let is_vec = is_vec_type(ty);
let is_option = is_option_type(ty);
let field_name = resolve_field_name(ident, &field.attrs, rename_all);
let limit_tokens = extract_limit_tokens(&field.attrs);
let default_kind = resolve_default_kind(&field.attrs, struct_default);
let parse_ty = if is_option || is_vec {
extract_inner_generic(ty).unwrap_or_else(|| ty.clone())
} else {
ty.clone()
};
if is_vec {
cg.declarations
.push(quote! { let mut #ident: #ty = std::vec::Vec::new(); });
} else if is_option {
cg.declarations
.push(quote! { let mut #ident: #ty = std::option::Option::None; });
} else {
cg.declarations.push(
quote! { let mut #ident: std::option::Option<#ty> = std::option::Option::None; },
);
}
let try_from_call = quote! { <#parse_ty as vespera::multipart::TryFromFieldWithState<__VesperaS__>>::try_from_field_with_state };
let parse_value = quote! { #try_from_call(__field__, #limit_tokens, __state__).await? };
let assignment = if is_vec {
quote! { #ident.push(#parse_value); }
} else if strict {
let set_value = quote! { #ident = std::option::Option::Some(#parse_value) };
let dup_err = quote! { return std::result::Result::Err(vespera::multipart::TypedMultipartError::DuplicateField { field_name: std::string::String::from(#field_name) }) };
quote! { if #ident.is_none() { #set_value ; } else { #dup_err ; } }
} else {
quote! { #ident = std::option::Option::Some(#parse_value); }
};
let field_match = quote! { if __field_name__ == #field_name { #assignment } };
cg.assignments.push(field_match);
if !is_option && !is_vec {
match &default_kind {
DefaultKind::Trait => {
cg.post_loop.push(quote! {
let #ident: #ty = #ident.unwrap_or_default();
});
}
DefaultKind::Function(fn_path) => {
let path: syn::ExprPath =
syn::parse_str(fn_path).expect("invalid default function path");
cg.post_loop.push(quote! {
let #ident: #ty = #ident.unwrap_or_else(#path);
});
}
DefaultKind::None => {
cg.post_loop.push(quote! {
let #ident = #ident.ok_or(
vespera::multipart::TypedMultipartError::MissingField {
field_name: std::string::String::from(#field_name)
}
)?;
});
}
}
}
cg.idents.push(ident);
}
cg
}
pub fn process_derive(input: &DeriveInput) -> TokenStream {
let struct_name = &input.ident;
let rename_all = 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 mut cg = process_fields(fields.iter(), rename_all.as_deref(), strict, struct_default);
if strict {
cg.assignments.push(quote! {
{
return std::result::Result::Err(
vespera::multipart::TypedMultipartError::UnknownField {
field_name: __field_name__
}
);
}
});
}
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)? {
let __field_name__ = match __field__.name() {
| std::option::Option::Some("")
| std::option::Option::None => #missing_name_fallback,
| std::option::Option::Some(__name__) => __name__.to_string(),
};
#(#assignments) else *
}
#(#post_loop)*
std::result::Result::Ok(Self { #(#idents),* })
}
}
}
}
fn resolve_field_name(
ident: &syn::Ident,
attrs: &[syn::Attribute],
rename_all: Option<&str>,
) -> String {
if let Some(name) = extract_form_data_field_name(attrs) {
return name;
}
if let Some(name) = extract_field_rename(attrs) {
return name;
}
let rust_name = strip_raw_prefix(&ident.to_string());
rename_field(&rust_name, rename_all)
}
fn extract_form_data_field_name(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("form_data") {
let mut found = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("field_name")
&& let Ok(value) = meta.value()
&& let Ok(lit) = value.parse::<syn::LitStr>()
{
found = Some(lit.value());
}
Ok(())
});
if found.is_some() {
return found;
}
}
}
None
}
fn extract_strict(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if attr.path().is_ident("try_from_multipart") {
let mut strict = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("strict") {
strict = true;
}
Ok(())
});
if strict {
return true;
}
}
}
false
}
fn extract_limit_tokens(attrs: &[syn::Attribute]) -> TokenStream {
for attr in attrs {
if attr.path().is_ident("form_data") {
let mut limit_str = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("limit")
&& let Ok(value) = meta.value()
&& let Ok(lit) = value.parse::<syn::LitStr>()
{
limit_str = Some(lit.value());
}
Ok(())
});
if let Some(s) = limit_str {
if s == "unlimited" {
return quote! { std::option::Option::None };
}
if let Some(bytes) = parse_byte_unit(&s) {
return quote! { std::option::Option::Some(#bytes) };
}
}
}
}
quote! { std::option::Option::None }
}
fn resolve_default_kind(attrs: &[syn::Attribute], struct_default: bool) -> DefaultKind {
if extract_form_data_default(attrs) {
return DefaultKind::Trait;
}
if let Some(serde_default) = extract_default(attrs) {
return serde_default.map_or(DefaultKind::Trait, DefaultKind::Function);
}
if struct_default {
return DefaultKind::Trait;
}
DefaultKind::None
}
fn extract_form_data_default(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if attr.path().is_ident("form_data") {
let mut has_default = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
has_default = true;
}
Ok(())
});
if has_default {
return true;
}
}
}
false
}
fn extract_struct_default(attrs: &[syn::Attribute]) -> bool {
extract_default(attrs).is_some()
}
fn extract_inner_generic(ty: &Type) -> Option<Type> {
let Type::Path(type_path) = ty else {
return None;
};
let segment = type_path.path.segments.last()?;
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return Some(inner.clone());
}
None
}
fn is_option_type(ty: &Type) -> bool {
matches_type_name(
ty,
&["Option", "std::option::Option", "core::option::Option"],
)
}
fn is_vec_type(ty: &Type) -> bool {
matches_type_name(ty, &["Vec", "std::vec::Vec"])
}
fn matches_type_name(ty: &Type, names: &[&str]) -> bool {
let path = match ty {
Type::Path(type_path) if type_path.qself.is_none() => &type_path.path,
_ => return false,
};
let sig = path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::");
names.contains(&sig.as_str())
}
fn strip_raw_prefix(s: &str) -> String {
s.strip_prefix("r#").unwrap_or(s).to_string()
}
fn parse_byte_unit(s: &str) -> Option<usize> {
let s = s.trim();
let suffixes: &[(&str, usize)] = &[
("GiB", 1024 * 1024 * 1024),
("MiB", 1024 * 1024),
("KiB", 1024),
("GB", 1_000_000_000),
("MB", 1_000_000),
("KB", 1_000),
("B", 1),
];
for (suffix, multiplier) in suffixes {
if let Some(num_str) = s.strip_suffix(suffix) {
return num_str.trim().parse::<usize>().ok().map(|n| n * multiplier);
}
}
s.parse::<usize>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_byte_unit() {
assert_eq!(parse_byte_unit("10MiB"), Some(10 * 1024 * 1024));
assert_eq!(parse_byte_unit("50MiB"), Some(50 * 1024 * 1024));
assert_eq!(parse_byte_unit("1GB"), Some(1_000_000_000));
assert_eq!(parse_byte_unit("500KB"), Some(500_000));
assert_eq!(parse_byte_unit("1024"), Some(1024));
assert_eq!(parse_byte_unit("0"), Some(0));
assert_eq!(parse_byte_unit("invalid"), None);
}
#[test]
fn test_parse_byte_unit_all_suffixes() {
assert_eq!(parse_byte_unit("1GiB"), Some(1024 * 1024 * 1024));
assert_eq!(parse_byte_unit("2KiB"), Some(2 * 1024));
assert_eq!(parse_byte_unit("3MB"), Some(3_000_000));
assert_eq!(parse_byte_unit("4B"), Some(4));
assert_eq!(parse_byte_unit(" 5MiB "), Some(5 * 1024 * 1024));
}
#[test]
fn test_strip_raw_prefix() {
assert_eq!(strip_raw_prefix("r#type"), "type");
assert_eq!(strip_raw_prefix("normal"), "normal");
}
#[test]
fn test_extract_inner_generic_option() {
let ty: syn::Type = syn::parse_str("Option<String>").unwrap();
let inner = extract_inner_generic(&ty).unwrap();
assert_eq!(quote!(#inner).to_string(), "String");
}
#[test]
fn test_extract_inner_generic_vec() {
let ty: syn::Type = syn::parse_str("Vec<i32>").unwrap();
let inner = extract_inner_generic(&ty).unwrap();
assert_eq!(quote!(#inner).to_string(), "i32");
}
#[test]
fn test_extract_inner_generic_no_generics() {
let ty: syn::Type = syn::parse_str("String").unwrap();
assert!(extract_inner_generic(&ty).is_none());
}
#[test]
fn test_extract_inner_generic_non_path() {
let ty: syn::Type = syn::parse_str("(i32, String)").unwrap();
assert!(extract_inner_generic(&ty).is_none());
}
#[test]
fn test_is_option_type() {
let ty: syn::Type = syn::parse_str("Option<String>").unwrap();
assert!(is_option_type(&ty));
let ty: syn::Type = syn::parse_str("std::option::Option<i32>").unwrap();
assert!(is_option_type(&ty));
let ty: syn::Type = syn::parse_str("Vec<String>").unwrap();
assert!(!is_option_type(&ty));
let ty: syn::Type = syn::parse_str("String").unwrap();
assert!(!is_option_type(&ty));
}
#[test]
fn test_is_vec_type() {
let ty: syn::Type = syn::parse_str("Vec<String>").unwrap();
assert!(is_vec_type(&ty));
let ty: syn::Type = syn::parse_str("std::vec::Vec<i32>").unwrap();
assert!(is_vec_type(&ty));
let ty: syn::Type = syn::parse_str("Option<String>").unwrap();
assert!(!is_vec_type(&ty));
let ty: syn::Type = syn::parse_str("String").unwrap();
assert!(!is_vec_type(&ty));
}
#[test]
fn test_matches_type_name_simple() {
let ty: syn::Type = syn::parse_str("Option<i32>").unwrap();
assert!(matches_type_name(&ty, &["Option"]));
assert!(!matches_type_name(&ty, &["Vec"]));
}
#[test]
fn test_matches_type_name_qualified() {
let ty: syn::Type = syn::parse_str("std::option::Option<i32>").unwrap();
assert!(matches_type_name(&ty, &["std::option::Option"]));
assert!(!matches_type_name(&ty, &["Option"])); }
#[test]
fn test_matches_type_name_non_path() {
let ty: syn::Type = syn::parse_str("(i32, String)").unwrap();
assert!(!matches_type_name(&ty, &["Option", "Vec"]));
}
fn parse_field(code: &str) -> syn::Field {
let input: syn::DeriveInput = syn::parse_str(&format!("struct T {{ {code} }}")).unwrap();
match &input.data {
syn::Data::Struct(s) => match &s.fields {
Fields::Named(n) => n.named.first().unwrap().clone(),
_ => unreachable!(),
},
_ => unreachable!(),
}
}
fn parse_attrs(code: &str) -> Vec<syn::Attribute> {
parse_field(code).attrs
}
#[test]
fn test_extract_form_data_field_name_present() {
let attrs = parse_attrs(r#"#[form_data(field_name = "custom")] pub x: String"#);
assert_eq!(
extract_form_data_field_name(&attrs),
Some("custom".to_string())
);
}
#[test]
fn test_extract_form_data_field_name_absent() {
let attrs = parse_attrs("pub x: String");
assert_eq!(extract_form_data_field_name(&attrs), None);
}
#[test]
fn test_extract_form_data_field_name_other_form_data_attr() {
let attrs = parse_attrs(r#"#[form_data(limit = "100")] pub x: String"#);
assert_eq!(extract_form_data_field_name(&attrs), None);
}
fn parse_struct_attrs(code: &str) -> Vec<syn::Attribute> {
let input: syn::DeriveInput = syn::parse_str(code).unwrap();
input.attrs
}
#[test]
fn test_extract_strict_present() {
let attrs = parse_struct_attrs("#[try_from_multipart(strict)] struct T { }");
assert!(extract_strict(&attrs));
}
#[test]
fn test_extract_strict_absent() {
let attrs = parse_struct_attrs("struct T { }");
assert!(!extract_strict(&attrs));
}
#[test]
fn test_extract_strict_other_attr() {
let attrs =
parse_struct_attrs("#[try_from_multipart(rename_all = \"camelCase\")] struct T { }");
assert!(!extract_strict(&attrs));
}
#[test]
fn test_extract_form_data_default_present() {
let attrs = parse_attrs("#[form_data(default)] pub x: i32");
assert!(extract_form_data_default(&attrs));
}
#[test]
fn test_extract_form_data_default_absent() {
let attrs = parse_attrs("pub x: i32");
assert!(!extract_form_data_default(&attrs));
}
#[test]
fn test_extract_form_data_default_other_form_data() {
let attrs = parse_attrs(r#"#[form_data(limit = "100")] pub x: i32"#);
assert!(!extract_form_data_default(&attrs));
}
#[test]
fn test_extract_struct_default_present() {
let attrs = parse_struct_attrs("#[serde(default)] struct T { }");
assert!(extract_struct_default(&attrs));
}
#[test]
fn test_extract_struct_default_absent() {
let attrs = parse_struct_attrs("struct T { }");
assert!(!extract_struct_default(&attrs));
}
#[test]
fn test_resolve_default_kind_none() {
let attrs = parse_attrs("pub x: i32");
assert!(matches!(
resolve_default_kind(&attrs, false),
DefaultKind::None
));
}
#[test]
fn test_resolve_default_kind_serde_default() {
let attrs = parse_attrs("#[serde(default)] pub x: i32");
assert!(matches!(
resolve_default_kind(&attrs, false),
DefaultKind::Trait
));
}
#[test]
fn test_resolve_default_kind_serde_default_fn() {
let attrs = parse_attrs(r#"#[serde(default = "my_fn")] pub x: i32"#);
assert!(
matches!(resolve_default_kind(&attrs, false), DefaultKind::Function(ref f) if f == "my_fn")
);
}
#[test]
fn test_resolve_default_kind_form_data_default() {
let attrs = parse_attrs("#[form_data(default)] pub x: i32");
assert!(matches!(
resolve_default_kind(&attrs, false),
DefaultKind::Trait
));
}
#[test]
fn test_resolve_default_kind_struct_level() {
let attrs = parse_attrs("pub x: i32");
assert!(matches!(
resolve_default_kind(&attrs, true),
DefaultKind::Trait
));
}
#[test]
fn test_resolve_default_kind_form_data_overrides_struct_default() {
let attrs = parse_attrs("#[form_data(default)] pub x: i32");
assert!(matches!(
resolve_default_kind(&attrs, true),
DefaultKind::Trait
));
}
#[test]
fn test_resolve_field_name_plain() {
let field = parse_field("pub my_field: String");
let name = resolve_field_name(field.ident.as_ref().unwrap(), &field.attrs, None);
assert_eq!(name, "my_field");
}
#[test]
fn test_resolve_field_name_rename_all() {
let field = parse_field("pub my_field: String");
let name = resolve_field_name(
field.ident.as_ref().unwrap(),
&field.attrs,
Some("camelCase"),
);
assert_eq!(name, "myField");
}
#[test]
fn test_resolve_field_name_serde_rename() {
let field = parse_field(r#"#[serde(rename = "custom")] pub my_field: String"#);
let name = resolve_field_name(
field.ident.as_ref().unwrap(),
&field.attrs,
Some("camelCase"),
);
assert_eq!(name, "custom"); }
#[test]
fn test_resolve_field_name_form_data_field_name() {
let field = parse_field(
r#"#[form_data(field_name = "override")] #[serde(rename = "serde_name")] pub my_field: String"#,
);
let name = resolve_field_name(
field.ident.as_ref().unwrap(),
&field.attrs,
Some("camelCase"),
);
assert_eq!(name, "override"); }
#[test]
fn test_extract_limit_tokens_none() {
let attrs = parse_attrs("pub x: String");
let tokens = extract_limit_tokens(&attrs);
assert_eq!(tokens.to_string(), "std :: option :: Option :: None");
}
#[test]
fn test_extract_limit_tokens_with_value() {
let attrs = parse_attrs(r#"#[form_data(limit = "100")] pub x: String"#);
let tokens = extract_limit_tokens(&attrs);
assert_eq!(
tokens.to_string(),
"std :: option :: Option :: Some (100usize)"
);
}
#[test]
fn test_extract_limit_tokens_unlimited() {
let attrs = parse_attrs(r#"#[form_data(limit = "unlimited")] pub x: String"#);
let tokens = extract_limit_tokens(&attrs);
assert_eq!(tokens.to_string(), "std :: option :: Option :: None");
}
#[test]
fn test_extract_limit_tokens_mib() {
let attrs = parse_attrs(r#"#[form_data(limit = "10MiB")] pub x: String"#);
let tokens = extract_limit_tokens(&attrs);
let expected = 10 * 1024 * 1024;
assert_eq!(
tokens.to_string(),
format!("std :: option :: Option :: Some ({expected}usize)")
);
}
#[test]
fn test_process_derive_basic_struct() {
let input: syn::DeriveInput =
syn::parse_str("struct MyForm { pub name: String, pub age: i32 }").unwrap();
let tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("TryFromMultipartWithState"),
"should generate trait impl"
);
assert!(code.contains("MyForm"), "should reference the struct name");
assert!(code.contains("\"name\""), "should reference field name");
assert!(code.contains("\"age\""), "should reference field name");
}
#[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 tokens = process_derive(&input);
let code = tokens.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 tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("Vec :: new"),
"Vec fields should be initialized with Vec::new()"
);
assert!(code.contains("push"), "Vec fields should use 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 tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("DuplicateField"),
"strict mode should check for duplicates"
);
assert!(
code.contains("UnknownField"),
"strict mode should reject unknown fields"
);
assert!(
code.contains("NamelessField"),
"strict mode should reject nameless fields"
);
}
#[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();
let tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("\"userName\""),
"rename_all should convert to camelCase"
);
}
#[test]
fn test_process_derive_with_serde_default() {
let input: syn::DeriveInput =
syn::parse_str("#[serde(default)] struct MyForm { pub count: i32 }").unwrap();
let tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("unwrap_or_default"),
"struct-level default should use 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 tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("unwrap_or_else"),
"field default fn should use unwrap_or_else"
);
assert!(
code.contains("my_default"),
"should reference the default function"
);
}
#[test]
fn test_process_derive_non_struct_errors() {
let input: syn::DeriveInput = syn::parse_str("enum Foo { A, B }").unwrap();
let tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("compile_error"),
"enums should produce compile error"
);
}
#[test]
fn test_process_derive_tuple_struct_errors() {
let input: syn::DeriveInput = syn::parse_str("struct Foo(String, i32);").unwrap();
let tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("compile_error"),
"tuple structs should produce 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();
let tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("\"custom\""),
"form_data field_name should be used"
);
}
#[test]
fn test_process_derive_form_data_default() {
let input: syn::DeriveInput =
syn::parse_str("struct MyForm { #[form_data(default)] pub count: i32 }").unwrap();
let tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
code.contains("unwrap_or_default"),
"form_data(default) should use 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 tokens = process_derive(&input);
let code = tokens.to_string();
assert!(
!code.contains("DuplicateField"),
"non-strict should not check for duplicates"
);
assert!(
!code.contains("UnknownField"),
"non-strict should not check for unknown fields"
);
}
fn parse_fields_from(code: &str) -> syn::DeriveInput {
syn::parse_str(code).unwrap()
}
fn get_named_fields(
input: &syn::DeriveInput,
) -> &syn::punctuated::Punctuated<syn::Field, syn::token::Comma> {
match &input.data {
syn::Data::Struct(s) => match &s.fields {
Fields::Named(n) => &n.named,
_ => panic!("expected named fields"),
},
_ => panic!("expected struct"),
}
}
#[test]
fn test_process_fields_required_field_generates_parse_value() {
let input = parse_fields_from("struct T { pub name: String }");
let fields = get_named_fields(&input);
let cg = process_fields(fields.iter(), None, false, false);
let assignment_code = cg
.assignments
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
assert!(
assignment_code.contains("TryFromFieldWithState"),
"parse_value should contain turbofish call"
);
assert!(
assignment_code.contains("try_from_field_with_state"),
"should call try_from_field_with_state"
);
assert!(
assignment_code.contains("\"name\""),
"should match on field name"
);
let post_code = cg
.post_loop
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
assert!(
post_code.contains("MissingField"),
"required field should have MissingField check"
);
}
#[test]
fn test_process_fields_strict_required_field_generates_duplicate_check() {
let input = parse_fields_from("struct T { pub name: String, pub age: i32 }");
let fields = get_named_fields(&input);
let cg = process_fields(fields.iter(), None, true, false);
let assignment_code = cg
.assignments
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
assert!(
assignment_code.contains("is_none"),
"strict assignment should check is_none"
);
assert!(
assignment_code.contains("DuplicateField"),
"strict assignment should have DuplicateField"
);
assert!(
assignment_code.contains("\"name\""),
"should match name field"
);
assert!(
assignment_code.contains("\"age\""),
"should match age field"
);
assert!(
assignment_code.contains("TryFromFieldWithState"),
"should contain turbofish"
);
}
#[test]
fn test_process_fields_vec_field_generates_push() {
let input = parse_fields_from("struct T { pub tags: Vec<String> }");
let fields = get_named_fields(&input);
let cg = process_fields(fields.iter(), None, false, false);
let decl_code = cg
.declarations
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
assert!(
decl_code.contains("Vec :: new"),
"Vec field should initialize with Vec::new()"
);
let assignment_code = cg
.assignments
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
assert!(
assignment_code.contains("push"),
"Vec field assignment should use push"
);
assert!(
cg.post_loop.is_empty(),
"Vec fields should not have post-loop checks"
);
}
#[test]
fn test_process_fields_option_field_no_missing_check() {
let input = parse_fields_from("struct T { pub bio: Option<String> }");
let fields = get_named_fields(&input);
let cg = process_fields(fields.iter(), None, false, false);
let decl_code = cg
.declarations
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
assert!(
decl_code.contains("Option :: None"),
"Option field should initialize to None"
);
assert!(
cg.post_loop.is_empty(),
"Option fields should not have post-loop checks"
);
}
#[test]
fn test_process_fields_strict_vec_field_uses_push_not_duplicate() {
let input = parse_fields_from("struct T { pub tags: Vec<String> }");
let fields = get_named_fields(&input);
let cg = process_fields(fields.iter(), None, true, false);
let assignment_code = cg
.assignments
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
assert!(
assignment_code.contains("push"),
"Vec in strict mode should still use push"
);
assert!(
!assignment_code.contains("DuplicateField"),
"Vec should not have duplicate check"
);
}
#[test]
fn test_process_fields_mixed_types() {
let input = parse_fields_from(
"struct T { pub name: String, pub tags: Vec<String>, pub bio: Option<String> }",
);
let fields = get_named_fields(&input);
let cg = process_fields(fields.iter(), None, false, false);
assert_eq!(cg.idents.len(), 3, "should have 3 fields");
assert_eq!(cg.declarations.len(), 3, "should have 3 declarations");
assert_eq!(cg.assignments.len(), 3, "should have 3 assignments");
assert_eq!(
cg.post_loop.len(),
1,
"only required field should have post-loop"
);
}
}