use proc_macro2::TokenStream;
use quote::quote;
use super::fields::DefaultKind;
use super::types::{parse_byte_unit, strip_raw_prefix};
use crate::parser::{extract_default, extract_field_rename, rename_field};
pub(super) 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
}
pub(super) 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
}
pub(super) 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::Some(usize::MAX) };
}
if let Some(bytes) = parse_byte_unit(&s) {
return quote! { std::option::Option::Some(#bytes) };
}
}
}
}
quote! { std::option::Option::None }
}
#[cfg(test)]
pub(super) fn has_explicit_limit(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if attr.path().is_ident("form_data") {
let mut valid = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("limit")
&& let Ok(value) = meta.value()
&& let Ok(lit) = value.parse::<syn::LitStr>()
{
let s = lit.value();
valid = s == "unlimited" || parse_byte_unit(&s).is_some();
}
Ok(())
});
if valid {
return true;
}
}
}
false
}
pub(super) 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
}
pub(super) fn extract_struct_default(attrs: &[syn::Attribute]) -> bool {
extract_default(attrs).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use syn::Fields;
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
}
fn parse_struct_attrs(code: &str) -> Vec<syn::Attribute> {
let input: syn::DeriveInput = syn::parse_str(code).unwrap();
input.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() {
assert_eq!(
extract_form_data_field_name(&parse_attrs("pub x: String")),
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);
}
#[test]
fn malformed_form_data_field_name_falls_back_to_serde_rename() {
let field = parse_field(
r#"#[form_data(field_name = 42)] #[serde(rename = "wire_name")] pub rust_name: String"#,
);
assert_eq!(
resolve_field_name(field.ident.as_ref().unwrap(), &field.attrs, None),
"wire_name"
);
}
#[test]
fn valueless_form_data_field_name_falls_back_to_rust_name() {
let field = parse_field(r"#[form_data(field_name)] pub rust_name: String");
assert_eq!(
resolve_field_name(field.ident.as_ref().unwrap(), &field.attrs, None),
"rust_name"
);
}
#[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() {
assert!(extract_form_data_default(&parse_attrs(
"#[form_data(default)] pub x: i32"
)));
}
#[test]
fn test_extract_form_data_default_absent() {
assert!(!extract_form_data_default(&parse_attrs("pub x: i32")));
}
#[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() {
assert_eq!(
extract_limit_tokens(&parse_attrs("pub x: String")).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"#);
assert_eq!(
extract_limit_tokens(&attrs).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"#);
assert_eq!(
extract_limit_tokens(&attrs).to_string(),
"std :: option :: Option :: Some (usize :: MAX)"
);
}
#[test]
fn test_extract_limit_tokens_mib() {
let attrs = parse_attrs(r#"#[form_data(limit = "10MiB")] pub x: String"#);
let expected = 10 * 1024 * 1024;
assert_eq!(
extract_limit_tokens(&attrs).to_string(),
format!("std :: option :: Option :: Some ({expected}usize)")
);
}
#[rstest]
#[case::valueless(r"#[form_data(limit)] pub file: String")]
#[case::non_string(r"#[form_data(limit = 10)] pub file: String")]
#[case::invalid_size(r#"#[form_data(limit = "not-a-size")] pub file: String"#)]
fn malformed_limit_is_neither_emitted_nor_treated_as_explicit(#[case] code: &str) {
let attrs = parse_attrs(code);
assert_eq!(
extract_limit_tokens(&attrs).to_string(),
"std :: option :: Option :: None"
);
assert!(!has_explicit_limit(&attrs));
}
#[test]
fn test_has_explicit_limit_size() {
assert!(has_explicit_limit(&parse_attrs(
r#"#[form_data(limit = "10MiB")] pub x: String"#
)));
assert!(has_explicit_limit(&parse_attrs(
r#"#[form_data(limit = "100")] pub x: String"#
)));
}
#[test]
fn test_has_explicit_limit_unlimited() {
assert!(has_explicit_limit(&parse_attrs(
r#"#[form_data(limit = "unlimited")] pub x: String"#
)));
}
#[test]
fn test_has_explicit_limit_absent() {
assert!(!has_explicit_limit(&parse_attrs("pub x: String")));
}
#[test]
fn test_has_explicit_limit_invalid_value() {
assert!(!has_explicit_limit(&parse_attrs(
r#"#[form_data(limit = "garbage")] pub x: String"#
)));
}
#[test]
fn test_has_explicit_limit_other_form_data_key() {
assert!(!has_explicit_limit(&parse_attrs(
r#"#[form_data(field_name = "x")] pub x: String"#
)));
}
}