Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ proc-macro = true

[dependencies]
quote = "1"
syn = "2"
syn = { version = "2", features = ["full"] }
proc-macro2 = { version = "1", default-features = false }
proc-macro-error2 = "2"
107 changes: 83 additions & 24 deletions src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ use syn::{
use self::GenMode::{Get, GetClone, GetCopy, GetMut, Set, SetWith};
use super::parse_attr;

// Helper function to check if a meta contains a specific mode
fn has_mode_in_meta(meta: &Meta, mode: GenMode) -> bool {
match meta {
Meta::Path(path) => path.is_ident(mode.name()),
Meta::List(meta_list) => {
// Parse the meta list and check if it contains the mode
if let Ok(nested_metas) = meta_list.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
) {
nested_metas
.iter()
.any(|nested| nested.path().is_ident(mode.name()))
} else {
false
}
}
Meta::NameValue(name_value) => name_value.path.is_ident(mode.name()),
}
}

pub struct GenParams {
pub mode: GenMode,
pub global_attr: Option<Meta>,
Expand Down Expand Up @@ -97,42 +117,76 @@ pub fn parse_visibility(attr: Option<&Meta>, meta_name: &str) -> Option<Visibili

/// Some users want legacy/compatibility.
/// (Getters are often prefixed with `get_`)
fn has_prefix_attr(f: &Field, params: &GenParams) -> bool {
fn get_prefix_attr(f: &Field, params: &GenParams) -> Option<String> {
// helper function to check if meta has `with_prefix` attribute
let meta_has_prefix = |meta: &Meta| -> bool {
let get_prefix_from_meta = |meta: &Meta| -> Option<String> {
if let Meta::NameValue(name_value) = meta {
if let Some(s) = expr_to_string(&name_value.value) {
return s.split(" ").any(|v| v == "with_prefix");
if meta.path().is_ident("prefix")
&& let Some(s) = expr_to_string(&name_value.value)
{
return Some(s);
} else if let Some(s) = expr_to_string(&name_value.value)
&& s.split(" ").any(|v| v == "with_prefix")
{
return Some("get_".to_string());
}
}
false
None
};

let field_attr_has_prefix = f
let mut prefix = f
.attrs
.iter()
.filter_map(|attr| parse_attr(attr, params.mode))
.find(|meta| {
meta.path().is_ident("get")
|| meta.path().is_ident("get_clone")
|| meta.path().is_ident("get_copy")
|| meta.path().is_ident("get_mut")
.filter(|attr| attr.path().is_ident("getset"))
.find_map(|attr| {
if let Ok(meta_list) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
) {
meta_list.into_iter().find_map(|meta| {
if meta.path().is_ident("prefix") {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this if statement is redundant.

get_prefix_from_meta(&meta)
} else {
None
}
})
} else {
None
}
})
.as_ref()
.is_some_and(meta_has_prefix);
.or_else(|| {
if let Some(meta) = f
.attrs
.iter()
.filter_map(|attr| parse_attr(attr, params.mode))
.find(|meta| {
meta.path().is_ident("get")
|| meta.path().is_ident("get_clone")
|| meta.path().is_ident("get_copy")
|| meta.path().is_ident("get_mut")
})
{
return get_prefix_from_meta(&meta);
}
None
});

let global_attr_has_prefix = params.global_attr.as_ref().is_some_and(meta_has_prefix);
// Prefix can be set globally
if prefix.is_none()
&& let Some(meta) = params.global_attr.as_ref()
{
prefix = get_prefix_from_meta(meta);
}

field_attr_has_prefix || global_attr_has_prefix
prefix
}

pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 {
let field_name = field
.ident
.clone()
.unwrap_or_else(|| abort!(field.span(), "Expected the field to have a name"));

let fn_name = if !has_prefix_attr(field, params)
let prefix = get_prefix_attr(field, params);
let fn_name = if prefix.is_none()
&& (params.mode.is_get())
&& params.mode.suffix().is_empty()
&& field_name.to_string().starts_with("r#")
Expand All @@ -142,11 +196,7 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 {
Ident::new(
&format!(
"{}{}{}{}",
if has_prefix_attr(field, params) && (params.mode.is_get()) {
"get_"
} else {
""
},
prefix.unwrap_or("".to_string()),
params.mode.prefix(),
field_name.unraw(),
params.mode.suffix()
Expand All @@ -163,7 +213,16 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 {
.iter()
.filter_map(|v| parse_attr(v, params.mode))
.next_back()
.or_else(|| params.global_attr.clone());
.or_else(|| {
// Only inherit global attribute if it contains the current mode
params.global_attr.as_ref().and_then(|global_attr| {
if has_mode_in_meta(global_attr, params.mode) {
Some(global_attr.clone())
} else {
None
}
})
});

let visibility = parse_visibility(attr.as_ref(), params.mode.name());
match attr {
Expand Down
8 changes: 6 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ mod submodule {
let mut foo = submodule::Foo::default();
foo.public();
```

[DEPRECATED]
For some purposes, it's useful to have the `get_` prefix on the getters for
either legacy of compatibility reasons. It is done with `with_prefix`.

Expand Down Expand Up @@ -367,6 +367,7 @@ fn parse_attr(attr: &syn::Attribute, mode: GenMode) -> Option<syn::Meta> {
|| meta.path().is_ident("get_mut")
|| meta.path().is_ident("set")
|| meta.path().is_ident("set_with")
|| meta.path().is_ident("prefix")
|| meta.path().is_ident("skip"))
{
abort!(meta.path().span(), "unknown setter or getter")
Expand Down Expand Up @@ -398,7 +399,10 @@ fn parse_attr(attr: &syn::Attribute, mode: GenMode) -> Option<syn::Meta> {
);
}
} else {
last
// If no mode-specific attribute was found, only return prefix attributes for global processing
last.or_else(|| {
collected.into_iter().find(|meta| meta.path().is_ident("prefix"))
})
}
} else if attr.path().is_ident(mode.name()) {
// If skip is not used, return the last occurrence of matching
Expand Down
71 changes: 71 additions & 0 deletions tests/prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#[macro_use]
extern crate getset;

// For testing the prefix attribute
#[derive(Getters, Default)]
pub struct PrefixTest {
#[getset(get = "pub", prefix = "custom_")]
field1: usize,

#[getset(get = "pub", prefix = "mock_")]
field2: String,

#[getset(get = "pub")]
field3: bool,
}

impl PrefixTest {
fn new() -> Self {
PrefixTest {
field1: 42,
field2: "hello".to_string(),
field3: true,
}
}
}

#[test]
fn test_custom_prefix() {
let val = PrefixTest::new();
assert_eq!(42, *val.custom_field1());
assert_eq!("hello", val.mock_field2());
assert_eq!(true, *val.field3());
}

// For testing the global prefix attribute
#[derive(Getters, Default)]
#[getset(prefix = "global_")]
pub struct GlobalPrefixTest {
#[getset(get = "pub")]
field1: usize,

#[getset(skip)]
field2: String,

#[getset(get = "pub", prefix = "override_")]
field3: bool,
}

impl GlobalPrefixTest {
fn new() -> Self {
GlobalPrefixTest {
field1: 100,
field2: "world".to_string(),
field3: false,
}
}

// Compile time error if field2 was not properly skipped
fn field2(&self) -> &String {
&self.field2
}
}

#[test]
fn test_global_prefix() {
let val = GlobalPrefixTest::new();
assert_eq!(100, *val.global_field1());
// field2 skipped - we can define our own method
assert_eq!("world", val.field2());
assert_eq!(false, *val.override_field3());
}
Loading