#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructAttributes {
pub build_method_name: Option<String>,
pub setter_prefix: Option<String>,
pub impl_into: bool,
pub const_builder: bool,
}
impl Default for StructAttributes {
fn default() -> Self {
Self {
build_method_name: None,
setter_prefix: None,
impl_into: false,
const_builder: false,
}
}
}
impl StructAttributes {
pub fn get_build_method_name(&self) -> &str {
self.build_method_name.as_deref().unwrap_or("build")
}
pub fn get_setter_prefix(&self) -> Option<&str> {
self.setter_prefix.as_deref()
}
pub fn get_impl_into(&self) -> bool {
self.impl_into
}
pub fn get_const_builder(&self) -> bool {
self.const_builder
}
pub fn validate(&self) -> syn::Result<()> {
if let Some(build_method_name) = &self.build_method_name {
if build_method_name.is_empty() {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"Build method name cannot be empty",
));
}
if syn::parse_str::<syn::Ident>(build_method_name).is_err() {
if !build_method_name.starts_with("r#") {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"Invalid build method name '{build_method_name}'. Build method names must be valid Rust identifiers. \
Use raw identifier syntax (r#name) for keywords."
),
));
}
}
}
if let Some(setter_prefix) = &self.setter_prefix {
if setter_prefix.is_empty() {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"Setter prefix cannot be empty",
));
}
if setter_prefix.chars().next().is_some_and(|c| c.is_numeric()) {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"Invalid setter prefix '{setter_prefix}'. Setter prefixes cannot start with a number. \
Use a valid identifier prefix like 'with_' or 'set_'."
),
));
}
if !setter_prefix
.chars()
.all(|c| c.is_alphanumeric() || c == '_')
{
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"Invalid setter prefix '{setter_prefix}'. Setter prefixes must contain only alphanumeric characters and underscores."
),
));
}
}
if self.const_builder && self.impl_into {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"`const` and `impl_into` cannot be used together. \
`impl Into<T>` requires trait bounds which are not supported in const fn.",
));
}
Ok(())
}
}
pub fn parse_struct_attributes(attrs: &[syn::Attribute]) -> syn::Result<StructAttributes> {
let mut struct_attributes = StructAttributes::default();
for attr in attrs {
if attr.path().is_ident("builder") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("build_method") {
let value = meta.value()?;
let lit_str: syn::LitStr = value.parse()?;
let build_method_name = lit_str.value();
if build_method_name.is_empty() {
return Err(meta.error("Build method name cannot be empty"));
}
struct_attributes.build_method_name = Some(build_method_name);
Ok(())
} else if meta.path.is_ident("setter_prefix") {
let value = meta.value()?;
let lit_str: syn::LitStr = value.parse()?;
let setter_prefix = lit_str.value();
if setter_prefix.is_empty() {
return Err(meta.error("Setter prefix cannot be empty"));
}
struct_attributes.setter_prefix = Some(setter_prefix);
Ok(())
} else if meta.path.is_ident("impl_into") {
struct_attributes.impl_into = true;
Ok(())
} else if meta.path.is_ident("const") {
struct_attributes.const_builder = true;
Ok(())
} else {
Err(meta.error(
"Unknown struct-level builder attribute. Supported attributes: build_method, setter_prefix, impl_into, const"
))
}
})?;
}
}
struct_attributes.validate()?;
Ok(struct_attributes)
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_default_struct_attributes() {
let attrs = StructAttributes::default();
assert!(attrs.build_method_name.is_none());
assert!(attrs.setter_prefix.is_none());
assert!(!attrs.impl_into);
assert!(!attrs.const_builder);
assert_eq!(attrs.get_build_method_name(), "build");
assert_eq!(attrs.get_setter_prefix(), None);
assert!(!attrs.get_impl_into());
assert!(!attrs.get_const_builder());
}
#[test]
fn test_get_build_method_name() {
let default_attrs = StructAttributes::default();
assert_eq!(default_attrs.get_build_method_name(), "build");
let custom_attrs = StructAttributes {
build_method_name: Some("create".to_string()),
setter_prefix: None,
impl_into: false,
const_builder: false,
};
assert_eq!(custom_attrs.get_build_method_name(), "create");
}
#[test]
fn test_with_build_method_name() {
let attrs = StructAttributes {
build_method_name: Some("create".to_string()),
setter_prefix: None,
impl_into: false,
const_builder: false,
};
assert_eq!(attrs.get_build_method_name(), "create");
let attrs2 = StructAttributes {
build_method_name: Some("construct".to_string()),
setter_prefix: None,
impl_into: false,
const_builder: false,
};
assert_eq!(attrs2.get_build_method_name(), "construct");
}
#[test]
fn test_validate_valid_attributes() {
let valid_attrs = StructAttributes {
build_method_name: Some("create".to_string()),
setter_prefix: Some("with_".to_string()),
impl_into: false,
const_builder: false,
};
assert!(valid_attrs.validate().is_ok());
let default_attrs = StructAttributes::default();
assert!(default_attrs.validate().is_ok());
}
#[test]
fn test_validate_empty_build_method_name() {
let invalid_attrs = StructAttributes {
build_method_name: Some("".to_string()),
setter_prefix: None,
impl_into: false,
const_builder: false,
};
let result = invalid_attrs.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Build method name cannot be empty"));
}
#[test]
fn test_parse_build_method_attribute() {
let attrs = vec![parse_quote!(#[builder(build_method = "create")])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert_eq!(struct_attrs.build_method_name, Some("create".to_string()));
assert_eq!(struct_attrs.get_build_method_name(), "create");
}
#[test]
fn test_parse_raw_identifier_build_method() {
let attrs = vec![parse_quote!(#[builder(build_method = "r#type")])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert_eq!(struct_attrs.build_method_name, Some("r#type".to_string()));
assert_eq!(struct_attrs.get_build_method_name(), "r#type");
}
#[test]
fn test_parse_no_builder_attributes() {
let attrs = vec![parse_quote!(#[derive(Debug)])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert!(struct_attrs.build_method_name.is_none());
assert_eq!(struct_attrs.get_build_method_name(), "build");
}
#[test]
fn test_parse_multiple_builder_attributes() {
let attrs = vec![
parse_quote!(#[builder(build_method = "create")]),
];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert_eq!(struct_attrs.get_build_method_name(), "create");
}
#[test]
fn test_parse_empty_build_method_error() {
let attrs = vec![parse_quote!(#[builder(build_method = "")])];
let result = parse_struct_attributes(&attrs);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be empty"));
}
#[test]
fn test_parse_unknown_attribute_error() {
let attrs = vec![parse_quote!(#[builder(unknown_attr = "value")])];
let result = parse_struct_attributes(&attrs);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unknown struct-level builder attribute"));
}
#[test]
fn test_parse_invalid_build_method_name() {
let attrs = vec![parse_quote!(#[builder(build_method = "123invalid")])];
let result = parse_struct_attributes(&attrs);
if let Err(e) = result {
assert!(
e.to_string().contains("Invalid build method name")
|| e.to_string().contains("identifier")
);
}
}
#[test]
fn test_get_setter_prefix() {
let default_attrs = StructAttributes::default();
assert_eq!(default_attrs.get_setter_prefix(), None);
let custom_attrs = StructAttributes {
build_method_name: None,
setter_prefix: Some("with_".to_string()),
impl_into: false,
const_builder: false,
};
assert_eq!(custom_attrs.get_setter_prefix(), Some("with_"));
}
#[test]
fn test_parse_setter_prefix_attribute() {
let attrs = vec![parse_quote!(#[builder(setter_prefix = "with_")])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert_eq!(struct_attrs.setter_prefix, Some("with_".to_string()));
assert_eq!(struct_attrs.get_setter_prefix(), Some("with_"));
assert_eq!(struct_attrs.get_build_method_name(), "build"); }
#[test]
fn test_parse_combined_attributes() {
let attrs =
vec![parse_quote!(#[builder(build_method = "create", setter_prefix = "with_")])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert_eq!(struct_attrs.build_method_name, Some("create".to_string()));
assert_eq!(struct_attrs.setter_prefix, Some("with_".to_string()));
assert_eq!(struct_attrs.get_build_method_name(), "create");
assert_eq!(struct_attrs.get_setter_prefix(), Some("with_"));
}
#[test]
fn test_parse_empty_setter_prefix_error() {
let attrs = vec![parse_quote!(#[builder(setter_prefix = "")])];
let result = parse_struct_attributes(&attrs);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Setter prefix cannot be empty"));
}
#[test]
fn test_validate_empty_setter_prefix() {
let invalid_attrs = StructAttributes {
build_method_name: None,
setter_prefix: Some("".to_string()),
impl_into: false,
const_builder: false,
};
let result = invalid_attrs.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Setter prefix cannot be empty"));
}
#[test]
fn test_validate_invalid_setter_prefix_starting_with_number() {
let invalid_attrs = StructAttributes {
build_method_name: None,
setter_prefix: Some("1invalid_".to_string()),
impl_into: false,
const_builder: false,
};
let result = invalid_attrs.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("cannot start with a number"));
}
#[test]
fn test_validate_invalid_setter_prefix_special_chars() {
let invalid_attrs = StructAttributes {
build_method_name: None,
setter_prefix: Some("with-".to_string()),
impl_into: false,
const_builder: false,
};
let result = invalid_attrs.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("alphanumeric characters and underscores"));
}
#[test]
fn test_validate_valid_setter_prefixes() {
let valid_prefixes = [
"with_", "set_", "use_", "add_", "remove_", "update_", "get_",
];
for prefix in valid_prefixes {
let attrs = StructAttributes {
build_method_name: None,
setter_prefix: Some(prefix.to_string()),
impl_into: false,
const_builder: false,
};
assert!(
attrs.validate().is_ok(),
"Prefix '{prefix}' should be valid"
);
}
}
#[test]
fn test_parse_multiple_setter_prefix_attributes() {
let attrs = vec![
parse_quote!(#[builder(setter_prefix = "with_")]),
parse_quote!(#[builder(build_method = "create")]),
];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert_eq!(struct_attrs.get_setter_prefix(), Some("with_"));
assert_eq!(struct_attrs.get_build_method_name(), "create");
}
#[test]
fn test_get_impl_into() {
let default_attrs = StructAttributes::default();
assert!(!default_attrs.get_impl_into());
let impl_into_attrs = StructAttributes {
build_method_name: None,
setter_prefix: None,
impl_into: true,
const_builder: false,
};
assert!(impl_into_attrs.get_impl_into());
let no_impl_into_attrs = StructAttributes {
build_method_name: None,
setter_prefix: None,
impl_into: false,
const_builder: false,
};
assert!(!no_impl_into_attrs.get_impl_into());
}
#[test]
fn test_parse_impl_into_attribute() {
let attrs = vec![parse_quote!(#[builder(impl_into)])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert!(struct_attrs.impl_into);
assert!(struct_attrs.get_impl_into());
assert_eq!(struct_attrs.get_build_method_name(), "build"); assert_eq!(struct_attrs.get_setter_prefix(), None); }
#[test]
fn test_parse_combined_attributes_with_impl_into() {
let attrs = vec![parse_quote!(#[builder(
impl_into,
build_method = "create",
setter_prefix = "with_"
)])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert!(struct_attrs.impl_into);
assert_eq!(struct_attrs.build_method_name, Some("create".to_string()));
assert_eq!(struct_attrs.setter_prefix, Some("with_".to_string()));
assert!(struct_attrs.get_impl_into());
assert_eq!(struct_attrs.get_build_method_name(), "create");
assert_eq!(struct_attrs.get_setter_prefix(), Some("with_"));
}
#[test]
fn test_parse_multiple_impl_into_attributes() {
let attrs = vec![
parse_quote!(#[builder(impl_into)]),
parse_quote!(#[builder(build_method = "create")]),
];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert!(struct_attrs.get_impl_into());
assert_eq!(struct_attrs.get_build_method_name(), "create");
}
#[test]
fn test_validate_impl_into_attributes() {
let valid_attrs = StructAttributes {
build_method_name: Some("create".to_string()),
setter_prefix: Some("with_".to_string()),
impl_into: true,
const_builder: false,
};
assert!(valid_attrs.validate().is_ok());
let impl_into_only = StructAttributes {
build_method_name: None,
setter_prefix: None,
impl_into: true,
const_builder: false,
};
assert!(impl_into_only.validate().is_ok());
let no_impl_into = StructAttributes {
build_method_name: None,
setter_prefix: None,
impl_into: false,
const_builder: false,
};
assert!(no_impl_into.validate().is_ok());
}
#[test]
fn test_get_const_builder() {
let default_attrs = StructAttributes::default();
assert!(!default_attrs.get_const_builder());
let const_attrs = StructAttributes {
build_method_name: None,
setter_prefix: None,
impl_into: false,
const_builder: true,
};
assert!(const_attrs.get_const_builder());
}
#[test]
fn test_parse_const_attribute() {
let attrs = vec![parse_quote!(#[builder(const)])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert!(struct_attrs.const_builder);
assert!(struct_attrs.get_const_builder());
assert_eq!(struct_attrs.get_build_method_name(), "build"); assert!(!struct_attrs.get_impl_into()); }
#[test]
fn test_parse_const_with_other_attributes() {
let attrs = vec![parse_quote!(#[builder(const, build_method = "create")])];
let struct_attrs = parse_struct_attributes(&attrs).unwrap();
assert!(struct_attrs.const_builder);
assert_eq!(struct_attrs.build_method_name, Some("create".to_string()));
assert!(struct_attrs.get_const_builder());
assert_eq!(struct_attrs.get_build_method_name(), "create");
}
#[test]
fn test_validate_const_with_impl_into_error() {
let invalid_attrs = StructAttributes {
build_method_name: None,
setter_prefix: None,
impl_into: true,
const_builder: true,
};
let result = invalid_attrs.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("`const` and `impl_into` cannot be used together"));
}
#[test]
fn test_parse_const_with_impl_into_error() {
let attrs = vec![parse_quote!(#[builder(const, impl_into)])];
let result = parse_struct_attributes(&attrs);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("`const` and `impl_into` cannot be used together"));
}
#[test]
fn test_validate_const_alone_is_valid() {
let const_only = StructAttributes {
build_method_name: None,
setter_prefix: None,
impl_into: false,
const_builder: true,
};
assert!(const_only.validate().is_ok());
}
#[test]
fn test_validate_const_with_setter_prefix_is_valid() {
let attrs = StructAttributes {
build_method_name: Some("create".to_string()),
setter_prefix: Some("with_".to_string()),
impl_into: false,
const_builder: true,
};
assert!(attrs.validate().is_ok());
}
}