e/proc-macros/easy-get/core/src/lib.rs
2024-05-04 10:00:44 -04:00

281 lines
5.1 KiB
Rust

use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::{
Attribute, Field, Fields, FieldsNamed, Generics, Ident, ItemStruct, LitStr,
Meta,
};
#[derive(Default)]
enum Visibility {
#[default]
Inherited,
Pub,
}
impl ToTokens for Visibility {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Visibility::Pub => tokens.append_all(quote! { pub }),
Visibility::Inherited => (),
}
}
}
#[derive(Default)]
struct GetOptions {
visibility: Visibility,
rename: Option<String>,
}
impl GetOptions {
fn from_attr(attr: &Attribute) -> syn::Result<Self> {
let mut out = Self::default();
attr.parse_nested_meta(|nm| {
if nm.path.is_ident("pub") {
out.visibility = Visibility::Pub;
}
if nm.path.is_ident("rename") {
let name = nm.value()?;
let name: LitStr = name.parse()?;
out.rename = Some(name.value());
}
Ok(())
})?;
Ok(out)
}
}
fn named_fields(
name: Ident,
generics: Generics,
n: FieldsNamed,
) -> TokenStream {
let (ipl, ty, _) = generics.split_for_impl();
n.named
.into_iter()
.flat_map(|field| {
let field_name =
field.ident.as_ref().expect("named should have ident");
let field_ty = &field.ty;
fn options_for(
attr_name: &str,
field: &Field,
) -> Option<syn::Result<GetOptions>> {
field.attrs.iter().find_map(|a| match &a.meta {
Meta::List(list) => list
.path
.get_ident()
.filter(|i| *i == attr_name)
.map(|_| GetOptions::from_attr(a)),
Meta::Path(p) => p
.get_ident()
.filter(|i| *i == attr_name)
.map(|_| Ok(GetOptions::default())),
_ => None,
})
}
let get = options_for("get", &field);
fn output_for(
options: Option<syn::Result<GetOptions>>,
field_name: &Ident,
f: impl FnOnce(Ident, Visibility) -> TokenStream,
) -> Option<TokenStream> {
match options {
Some(Ok(get)) => {
let GetOptions {
visibility, rename, ..
} = get;
let fn_name = if let Some(rename) = rename {
format_ident!("{}", rename)
} else {
field_name.clone()
};
Some(f(fn_name, visibility))
},
Some(Err(e)) => Some(e.into_compile_error()),
_ => None,
}
}
let get_out = output_for(get, field_name, |fn_name, vis| {
quote! {
impl #ipl #name #ty {
#vis fn #fn_name(&self) -> #field_ty {
self.#field_name
}
}
}
});
let get_ref = options_for("get_ref", &field);
let get_ref_out =
output_for(get_ref, field_name, |fn_name, vis| {
quote! {
impl #ipl #name #ty {
#vis fn #fn_name(&self) -> &#field_ty {
&self.#field_name
}
}
}
});
let get_mut = options_for("get_mut", &field);
let get_mut_renamed = get_mut
.as_ref()
.is_some_and(|r| r.as_ref().is_ok_and(|o| o.rename.is_some()));
let get_mut_out =
output_for(get_mut, field_name, |fn_name, vis| {
let fn_name = if get_mut_renamed {
fn_name
} else {
format_ident!("{}_mut", fn_name)
};
quote! {
impl #ipl #name #ty {
#vis fn #fn_name(&mut self) -> &mut #field_ty {
&mut self.#field_name
}
}
}
});
get_out.into_iter().chain(get_ref_out).chain(get_mut_out)
})
.collect()
}
pub fn e_easy_get_core(input: TokenStream) -> TokenStream {
let item: ItemStruct = match syn::parse2(input) {
Ok(i) => i,
Err(e) => return e.into_compile_error(),
};
match item.fields {
Fields::Named(n) => named_fields(item.ident, item.generics, n),
_ => todo!("aaaaaa"),
}
}
#[cfg(test)]
mod test {
use proc_macro2::TokenStream;
use quote::quote;
use crate::e_easy_get_core;
fn expect_output(input: TokenStream, output: TokenStream) {
assert_eq!(e_easy_get_core(input).to_string(), output.to_string())
}
#[test]
fn test_get_pub() {
expect_output(
quote! {
#[derive(EasyGet)]
pub struct Foo {
#[get(pub)]
bar: usize,
}
},
quote! {
impl Foo {
pub fn bar(&self) -> usize {
self.bar
}
}
},
);
}
#[test]
fn test_get_generic() {
expect_output(
quote! {
#[derive(EasyGet)]
pub struct Foo<T> {
#[get(pub)]
bar: T,
}
},
quote! {
impl<T> Foo<T> {
pub fn bar(&self) -> T {
self.bar
}
}
},
);
}
#[test]
fn test_get_ref_generic_default() {
expect_output(
quote! {
#[derive(EasyGet)]
pub struct Foo<T = usize> {
#[get_ref(pub)]
bar: T,
}
},
quote! {
impl<T> Foo<T> {
pub fn bar(&self) -> &T {
&self.bar
}
}
},
);
}
#[test]
fn test_rename() {
expect_output(
quote! {
#[derive(EasyGet)]
pub struct Foo<T = usize> {
#[get_ref(pub, rename = "baz")]
bar: T,
}
},
quote! {
impl<T> Foo<T> {
pub fn baz(&self) -> &T {
&self.bar
}
}
},
)
}
#[test]
fn test_get_mut() {
expect_output(
quote! {
#[derive(EasyGet)]
pub struct Foo<T = usize> {
#[get_mut(pub)]
bar: T,
}
},
quote! {
impl<T> Foo<T> {
pub fn bar_mut(&mut self) -> &mut T {
&mut self.bar
}
}
},
)
}
}