#![deny(clippy::use_self)] //! Another getter derive macro. //! //! # Examples: //! #### Regular struct: //! ``` //! use get::Get; //! //! #[derive(Get)] //! struct Crab { //! name: String, //! age: u64, //! // Sometimes we may not want to provide access to a field at all. //! #[get(hide)] //! secrets: String //! } //! //! fn crab() { //! let ferris = Crab::new("ferris", 10); //! assert!(matches!( //! ferris.name().as_str(), //! "ferris" //! )); //! assert_eq!(*ferris.age(), 10); //! } //! //!# impl Crab { //!# pub fn new(name: &str, age: u64) -> Self { //!# Self { name: name.to_string(), age, secrets: "crab secrets".to_string() } //!# } //!# } //!``` //! #### Tuple struct: //! ``` //! use get::Get; //! //! #[derive(Get)] //! struct Crab ( //! #[get(method = "name")] String, //! #[get(method = "age")] u64, //! ); //! //! fn crab() { //! let ferris = Crab::new("ferris", 10); //! assert!(matches!( //! ferris.name().as_str(), //! "ferris" //! )); //! assert_eq!(*ferris.age(), 10); //! } //! //!# impl Crab { //!# pub fn new(name: &str, age: u64) -> Self { //!# Self ( name.to_string(), age ) //!# } //!# } //!``` //!#### Getters on Copy types: //!``` //! use get::GetCopy; //! //! #[derive(Clone, Copy, GetCopy)] //! struct NonZeroUInt( //! #[get(method = "inner")] T //! ); //! //! fn non_zero_uint() { //! let i = NonZeroUInt::new(1).unwrap(); //! // The getter method takes "self" by value. //! assert_eq!(i.inner(), 1); //! // Since NonZeroUint:: is Copy, the value is still accessible. //! assert_eq!(i.inner() + 1, 2); //! } //! //!# impl NonZeroUInt { //!# fn new(i: u32) -> Option { //!# (i != 0).then(|| Self(i)) //!# } //!# } //!``` //! # Attributes //! Attributes are expected to contain a comma separated list of name value pairs or idents. //! //! Examples of supported attributes: //! * `#[get(method = "getter")]` //! * `#[get(hide)]` //! //! All supported name value pairs: //! * `method` (this sets the name of the getter method) //! //! All supported idents are: //! * `hide` (this will disable getters for a specific field) //! //! # Todos //! * detect and return error for fields with conflicting attributes //! * improve error output, include span information if possible //! * AsRef or Deref getters #[proc_macro_derive(Get, attributes(get))] pub fn get(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let parsed_input = syn::parse_macro_input!(input as syn::DeriveInput); get::expand(&parsed_input, false).unwrap().into() } #[proc_macro_derive(GetCopy, attributes(get))] pub fn get_copy(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let parsed_input = syn::parse_macro_input!(input as syn::DeriveInput); get::expand(&parsed_input, true).unwrap().into() } mod get { use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, format_ident, quote}; use syn::{ Attribute, Data, DeriveInput, Expr, Field, Fields, Ident, Index, Lit, Member, MetaNameValue, Token, Type, parse::Parser, punctuated::Punctuated, }; enum GetAttribute { NameValueList(GetNameValueList), IdentList(Vec), } #[derive(Default)] struct GetNameValueList { method: Option, } #[derive(Debug, Clone)] enum GetNameValue { Method(String), } #[derive(Debug, Clone)] enum GetIdent { Hide, } pub fn expand( input: &DeriveInput, is_copy: bool, ) -> Result> { let Data::Struct(target) = &input.data else { return Err("expected struct as derive input".into()); }; let getters = match &target.fields { Fields::Unnamed(fields) => expand_for_tuple_struct(fields.unnamed.iter(), is_copy)?, Fields::Named(fields) => expand_for_struct(fields.named.iter(), is_copy)?, _ => return Err("can not generate getters on a unit struct".into()), }; let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl(); let struct_name = &input.ident; Ok(quote! { #[automatically_derived] impl #impl_generics #struct_name #ty_generics #where_clause { #getters } }) } fn expand_for_struct<'a>( fields: impl Iterator, is_copy: bool, ) -> Result> { let mut tokens = TokenStream::new(); for field in fields { match field.attrs.iter().find_map(|attr| { attr.path() .is_ident("get") .then(|| GetAttribute::try_from(attr.clone())) }) { Some(Ok(GetAttribute::IdentList(list))) if list.iter().any(|i| matches!(i, GetIdent::Hide)) => { continue; } Some(Ok(GetAttribute::NameValueList(list))) => { let method_name = list .method .map(|s| format_ident!("{s}")) .unwrap_or(field.ident.as_ref().cloned().unwrap()); expand_getter( field, &Member::Named(field.ident.as_ref().cloned().unwrap()), &method_name, is_copy, ) } Some(Err(e)) => return Err(e), _ => expand_getter( field, &Member::Named(field.ident.as_ref().cloned().unwrap()), field.ident.as_ref().unwrap(), is_copy, ), } .to_tokens(&mut tokens); } Ok(tokens) } fn expand_for_tuple_struct<'a>( fields: impl Iterator, is_copy: bool, ) -> Result> { let mut tokens = TokenStream::new(); for (i, field) in fields.enumerate() { match field.attrs.iter().find_map(|attr| { attr.path() .is_ident("get") .then(|| GetAttribute::try_from(attr.clone())) }) { Some(Ok(GetAttribute::IdentList(list))) if list.iter().any(|i| matches!(i, GetIdent::Hide)) => { continue; } Some(Ok(GetAttribute::NameValueList(list))) if list.method.is_some() => { expand_getter( field, &Member::Unnamed(Index { index: i.try_into().unwrap(), span: Span::call_site(), }), &list.method.map(|s| format_ident!("{s}")).unwrap(), is_copy, ) } Some(Err(e)) => return Err(e), _ => return Err("expected attribute on tuple struct field".into()), } .to_tokens(&mut tokens) } Ok(tokens) } fn expand_getter( field: &Field, field_name: &Member, method_name: &Ident, is_copy: bool, ) -> TokenStream { let field_type = &field.ty; let field_lifetime = match &field.ty { Type::Reference(type_ref) => Some(&type_ref.lifetime), _ => None, }; let method_args = if is_copy { quote! { ( self ) } } else { quote! { ( & #field_lifetime self ) } }; let method_type = if is_copy { quote! { #field_type } } else { quote! { & #field_type } }; let method_body = if is_copy { quote! { { self . #field_name } } } else { quote! { { & self . #field_name } } }; quote! { pub fn #method_name #method_args -> #method_type #method_body } } impl TryFrom for GetAttribute { type Error = Box; fn try_from(attr: Attribute) -> Result { let meta_list = attr .meta .require_list() .map_err(|_| "failed to parse attribute")?; let name_value_list = Punctuated::::parse_terminated .parse(meta_list.tokens.clone().into()); let ident_list = Punctuated::::parse_terminated .parse(meta_list.tokens.clone().into()); Ok(match (name_value_list, ident_list) { (Ok(list), _) => Self::NameValueList(GetNameValueList::try_from(list)?), (_, Ok(list)) => { Self::IdentList(list.into_iter().map(GetIdent::try_from).collect::, _, >>( )?) } _ => return Err("failed to parse attribute".into()), }) } } #[allow(clippy::needless_update)] impl TryFrom> for GetNameValueList { type Error = Box; fn try_from(punct: Punctuated) -> Result { Ok(punct .into_iter() .map(GetNameValue::try_from) .collect::, _>>() .map(|v| { if !v.is_empty() { Ok::, Box>(v) } else { Err("expected at least 1 name value pair in attribute".into()) } })?? .into_iter() .fold(Self::default(), |acc, n| match n { GetNameValue::Method(m) => Self { method: Some(m), ..acc }, })) } } impl TryFrom for GetIdent { type Error = Box; fn try_from(i: Ident) -> Result { match i.to_string().as_str() { "hide" => Ok(Self::Hide), _ => Err(r#"expected the following ident in meta list: "hide""#.into()), } } } impl TryFrom for GetNameValue { type Error = Box; fn try_from(meta: MetaNameValue) -> Result { if let Some(name) = meta.path.get_ident().map(|ident| ident.to_string()) && let Expr::Lit(expr) = &meta.value && let Lit::Str(s) = &expr.lit && let "method" = name.as_str() { Ok(Self::Method(s.value())) } else { Err("invalid name value pair in attribute".into()) } } } }