use std::borrow::Cow;
#[derive(Debug, Clone)]
pub struct SetterConfig {
pub setter_name: Cow<'static, str>,
pub _field_name: Cow<'static, str>,
pub _clean_name: Cow<'static, str>,
pub skip_setter: bool,
pub doc_comment: String,
}
#[derive(Debug, Clone)]
pub struct DefaultConfig {
pub _has_custom_default: bool,
pub default_expression: Option<syn::Expr>,
pub _requires_default_trait: bool,
}
pub fn resolve_effective_impl_into(field_impl_into: Option<bool>, struct_impl_into: bool) -> bool {
field_impl_into.unwrap_or(struct_impl_into)
}
#[derive(Debug, Clone)]
pub struct SetterParameterConfig {
pub param_type: proc_macro2::TokenStream,
pub field_assignment_expr: proc_macro2::TokenStream,
}
pub fn resolve_setter_parameter_config(
field_type: &syn::Type,
converter: Option<&syn::Expr>,
use_impl_into: bool,
) -> SetterParameterConfig {
if let Some(converter_expr) = converter {
let param_type = extract_closure_parameter_type(converter_expr).unwrap_or_else(
|| quote::quote! { },
);
SetterParameterConfig {
param_type,
field_assignment_expr: quote::quote! { (#converter_expr)(value) },
}
} else if use_impl_into {
SetterParameterConfig {
param_type: quote::quote! { impl Into<#field_type> },
field_assignment_expr: quote::quote! { value.into() },
}
} else {
SetterParameterConfig {
param_type: quote::quote! { #field_type },
field_assignment_expr: quote::quote! { value },
}
}
}
fn extract_closure_parameter_type(expr: &syn::Expr) -> Option<proc_macro2::TokenStream> {
match expr {
syn::Expr::Closure(closure) => {
if let Some(first_param) = closure.inputs.first() {
match first_param {
syn::Pat::Type(pat_type) => {
let param_type = &pat_type.ty;
Some(quote::quote! { #param_type })
}
_ => {
None
}
}
} else {
None
}
}
_ => {
None
}
}
}
#[derive(Debug)]
pub struct ClosureInfo {
pub param_name: proc_macro2::TokenStream,
pub param_type: proc_macro2::TokenStream,
pub body: proc_macro2::TokenStream,
}
pub fn extract_closure_info(expr: &syn::Expr) -> Option<ClosureInfo> {
match expr {
syn::Expr::Closure(closure) => {
if let Some(first_param) = closure.inputs.first() {
match first_param {
syn::Pat::Type(pat_type) => {
let param_name = &pat_type.pat;
let param_type = &pat_type.ty;
let body = &closure.body;
Some(ClosureInfo {
param_name: quote::quote! { #param_name },
param_type: quote::quote! { #param_type },
body: quote::quote! { #body },
})
}
_ => {
None
}
}
} else {
None
}
}
_ => {
None
}
}
}
pub fn generate_const_converter_fn_name(field_name: &str) -> syn::Ident {
syn::Ident::new(
&format!("__const_{}_setter_converter", field_name),
proc_macro2::Span::call_site(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_effective_impl_into_field_inherits_struct() {
assert!(resolve_effective_impl_into(None, true));
assert!(!resolve_effective_impl_into(None, false));
}
#[test]
fn test_resolve_effective_impl_into_field_overrides_struct() {
assert!(resolve_effective_impl_into(Some(true), false));
assert!(!resolve_effective_impl_into(Some(false), true));
assert!(resolve_effective_impl_into(Some(true), true));
assert!(!resolve_effective_impl_into(Some(false), false));
}
#[test]
fn test_resolve_effective_impl_into_precedence() {
let test_cases = [
(None, true, true), (None, false, false), (Some(true), true, true), (Some(true), false, true), (Some(false), true, false), (Some(false), false, false), ];
for (field_setting, struct_setting, expected) in test_cases {
let result = resolve_effective_impl_into(field_setting, struct_setting);
assert_eq!(
result, expected,
"Failed for field_impl_into={field_setting:?}, struct_impl_into={struct_setting}, expected={expected}"
);
}
}
#[test]
fn test_resolve_setter_parameter_config_converter_closure() {
let field_type: syn::Type = syn::parse_quote!(Vec<String>);
let converter: syn::Expr = syn::parse_quote!(|values: Vec<&str>| values
.into_iter()
.map(|s| s.to_string())
.collect());
let config = resolve_setter_parameter_config(
&field_type,
Some(&converter),
false, );
assert_eq!(config.param_type.to_string(), "Vec < & str >");
assert_eq!(config.field_assignment_expr.to_string(), "(| values : Vec < & str > | values . into_iter () . map (| s | s . to_string ()) . collect ()) (value)");
}
#[test]
fn test_resolve_setter_parameter_config_converter_ignores_impl_into() {
let field_type: syn::Type = syn::parse_quote!(String);
let converter: syn::Expr = syn::parse_quote!(|input: &str| input.to_uppercase());
let config = resolve_setter_parameter_config(&field_type, Some(&converter), true);
assert_eq!(config.param_type.to_string(), "& str");
assert_eq!(
config.field_assignment_expr.to_string(),
"(| input : & str | input . to_uppercase ()) (value)"
);
}
#[test]
fn test_resolve_setter_parameter_config_impl_into() {
let field_type: syn::Type = syn::parse_quote!(String);
let config = resolve_setter_parameter_config(&field_type, None, true);
assert_eq!(config.param_type.to_string(), "impl Into < String >");
assert_eq!(config.field_assignment_expr.to_string(), "value . into ()");
}
#[test]
fn test_resolve_setter_parameter_config_regular_setter() {
let field_type: syn::Type = syn::parse_quote!(i32);
let config = resolve_setter_parameter_config(&field_type, None, false);
assert_eq!(config.param_type.to_string(), "i32");
assert_eq!(config.field_assignment_expr.to_string(), "value");
}
#[test]
fn test_resolve_setter_parameter_config_complex_types() {
let field_type: syn::Type = syn::parse_quote!(HashMap<String, Vec<i32>>);
let config = resolve_setter_parameter_config(&field_type, None, true);
assert_eq!(
config.param_type.to_string(),
"impl Into < HashMap < String , Vec < i32 > > >"
);
let config = resolve_setter_parameter_config(&field_type, None, false);
assert_eq!(
config.param_type.to_string(),
"HashMap < String , Vec < i32 > >"
);
let converter: syn::Expr =
syn::parse_quote!(|data: Vec<(String, Vec<i32>)>| data.into_iter().collect());
let config = resolve_setter_parameter_config(&field_type, Some(&converter), false);
assert_eq!(
config.param_type.to_string(),
"Vec < (String , Vec < i32 >) >"
);
assert_eq!(
config.field_assignment_expr.to_string(),
"(| data : Vec < (String , Vec < i32 >) > | data . into_iter () . collect ()) (value)"
);
}
#[test]
fn test_resolve_setter_parameter_config_complex_converters() {
let field_type: syn::Type = syn::parse_quote!(PathBuf);
let converter: syn::Expr =
syn::parse_quote!(|path_str: &str| PathBuf::from(path_str.trim()));
let config = resolve_setter_parameter_config(&field_type, Some(&converter), false);
assert_eq!(config.param_type.to_string(), "& str");
assert_eq!(
config.field_assignment_expr.to_string(),
"(| path_str : & str | PathBuf :: from (path_str . trim ())) (value)"
);
let converter: syn::Expr = syn::parse_quote!(|items: Vec<String>| items
.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("/")
.into());
let config = resolve_setter_parameter_config(&field_type, Some(&converter), false);
assert_eq!(config.param_type.to_string(), "Vec < String >");
assert_eq!(config.field_assignment_expr.to_string(), "(| items : Vec < String > | items . into_iter () . filter (| s | ! s . is_empty ()) . collect :: < Vec < _ > > () . join (\"/\") . into ()) (value)");
}
#[test]
fn test_resolve_setter_parameter_config_precedence() {
let field_type: syn::Type = syn::parse_quote!(String);
let converter: syn::Expr = syn::parse_quote!(|input: &str| input.to_string());
let configs = [
resolve_setter_parameter_config(&field_type, Some(&converter), true),
resolve_setter_parameter_config(&field_type, Some(&converter), false),
];
for config in configs {
assert_eq!(config.param_type.to_string(), "& str");
assert_eq!(
config.field_assignment_expr.to_string(),
"(| input : & str | input . to_string ()) (value)"
);
}
}
}