diff --git a/README.md b/README.md index b652c16..5145643 100644 --- a/README.md +++ b/README.md @@ -52,127 +52,127 @@ fn main() { You can use `cargo-expand` to generate the output. Here are the functions that the above generates (Replicate with `cargo expand --example simple`): ```rust -use std::sync::Arc; -use getset::{CloneGetters, CopyGetters, Getters, MutGetters, Setters, WithSetters}; -pub struct Foo -where - T: Copy + Clone + Default, -{ - /// Doc comments are supported! - /// Multiline, even. - #[getset(get, set, get_mut, set_with)] - private: T, - /// Doc comments are supported! - /// Multiline, even. - #[getset(get_copy = "pub", set = "pub", get_mut = "pub", set_with = "pub")] - public: T, - /// Arc supported through CloneGetters - #[getset(get_clone = "pub", set = "pub", get_mut = "pub", set_with = "pub")] - arc: Arc, -} -impl Foo -where - T: Copy + Clone + Default, -{ - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] - fn private(&self) -> &T { - &self.private - } -} -impl Foo -where - T: Copy + Clone + Default, -{ - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] - fn set_private(&mut self, val: T) -> &mut Self { +use std::sync::Arc; +use getset::{CloneGetters, CopyGetters, Getters, MutGetters, Setters, WithSetters}; +pub struct Foo +where + T: Copy + Clone + Default, +{ + /// Doc comments are supported! + /// Multiline, even. + #[getset(get, set, get_mut, set_with)] + private: T, + /// Doc comments are supported! + /// Multiline, even. + #[getset(get_copy = "pub", set = "pub", get_mut = "pub", set_with = "pub")] + public: T, + /// Arc supported through CloneGetters + #[getset(get_clone = "pub", set = "pub", get_mut = "pub", set_with = "pub")] + arc: Arc, +} +impl Foo +where + T: Copy + Clone + Default, +{ + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] + fn private(&self) -> &T { + &self.private + } +} +impl Foo +where + T: Copy + Clone + Default, +{ + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] + fn set_private(&mut self, val: T) -> &mut Self { self.private = val; - self - } - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] - pub fn set_public(&mut self, val: T) -> &mut Self { - self.public = val; - self - } - /// Arc supported through CloneGetters - #[inline(always)] + self + } + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] + pub fn set_public(&mut self, val: T) -> &mut Self { + self.public = val; + self + } + /// Arc supported through CloneGetters + #[inline(always)] pub fn set_arc(&mut self, val: Arc) -> &mut Self { - self.arc = val; - self - } -} -impl Foo -where - T: Copy + Clone + Default, -{ - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] - fn with_private(mut self, val: T) -> Self { - self.private = val; - self - } - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] - pub fn with_public(mut self, val: T) -> Self { - self.public = val; - self - } - /// Arc supported through CloneGetters - #[inline(always)] - pub fn with_arc(mut self, val: Arc) -> Self { - self.arc = val; - self - } -} -impl Foo -where - T: Copy + Clone + Default, -{ - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] + self.arc = val; + self + } +} +impl Foo +where + T: Copy + Clone + Default, +{ + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] + fn with_private(mut self, val: T) -> Self { + self.private = val; + self + } + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] + pub fn with_public(mut self, val: T) -> Self { + self.public = val; + self + } + /// Arc supported through CloneGetters + #[inline(always)] + pub fn with_arc(mut self, val: Arc) -> Self { + self.arc = val; + self + } +} +impl Foo +where + T: Copy + Clone + Default, +{ + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] fn private_mut(&mut self) -> &mut T { - &mut self.private - } - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] - pub fn public_mut(&mut self) -> &mut T { - &mut self.public - } - /// Arc supported through CloneGetters - #[inline(always)] - pub fn arc_mut(&mut self) -> &mut Arc { - &mut self.arc - } -} -impl Foo -where - T: Copy + Clone + Default, -{ - /// Doc comments are supported! - /// Multiline, even. - #[inline(always)] - pub fn public(&self) -> T { - self.public - } -} -impl Foo -where - T: Copy + Clone + Default, -{ - /// Arc supported through CloneGetters - #[inline(always)] - pub fn arc(&self) -> Arc { - self.arc.clone() - } + &mut self.private + } + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] + pub fn public_mut(&mut self) -> &mut T { + &mut self.public + } + /// Arc supported through CloneGetters + #[inline(always)] + pub fn arc_mut(&mut self) -> &mut Arc { + &mut self.arc + } +} +impl Foo +where + T: Copy + Clone + Default, +{ + /// Doc comments are supported! + /// Multiline, even. + #[inline(always)] + pub fn public(&self) -> T { + self.public + } +} +impl Foo +where + T: Copy + Clone + Default, +{ + /// Arc supported through CloneGetters + #[inline(always)] + pub fn arc(&self) -> Arc { + self.arc.clone() + } } ``` @@ -257,3 +257,36 @@ impl Foo { } } ``` + +By default, a field of type `Option` or `Result` generates a getter +returning `&Option` or `&Result`. To get an `Option<&T>` or +`Result<&T, &E>` instead, opt in with the `as_ref` / `as_mut` flags: + +```rust +use getset::{Getters, MutGetters}; + +#[derive(Getters, MutGetters)] +struct MyStruct { + /// Returns `Option<&T>` instead of `&Option` + #[getset(get = "as_ref", get_mut = "as_mut")] + maybe: Option, + + /// Returns `Result<&T, &E>` instead of `&Result` + #[getset(get = "as_ref", get_mut = "as_mut")] + parsed: Result, +} + +let mut s = MyStruct { + maybe: Some(42), + parsed: Err("oops"), +}; + +assert_eq!(s.maybe(), Some(&42)); +assert_eq!(s.maybe_mut(), Some(&mut 42)); + +assert_eq!(s.parsed(), Err(&"oops")); +assert_eq!(s.parsed_mut(), Err(&mut "oops")); +``` + +- `get = "as_ref"` makes the generated getter call `.as_ref()`. +- `get_mut = "as_mut"` makes the generated mutable getter call `.as_mut()`. diff --git a/src/generate.rs b/src/generate.rs index 30436ae..80d4c20 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,7 +1,8 @@ use proc_macro_error2::abort; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use syn::{ - self, Expr, Field, Lit, Meta, MetaNameValue, Visibility, ext::IdentExt, spanned::Spanned, + self, Expr, Field, GenericArgument, Lit, Meta, MetaNameValue, PathArguments, PathSegment, Type, + TypePath, Visibility, ext::IdentExt, spanned::Spanned, }; use self::GenMode::{Get, GetClone, GetCopy, GetMut, Set, SetWith}; @@ -70,6 +71,21 @@ fn expr_to_string(expr: &Expr) -> Option { } } +// Helper function to collect named value (named is `meta_name`) from attribute +fn parse_named_value(attr: Option<&Meta>, meta_name: &str) -> Option { + let meta = attr?; + let Meta::NameValue(MetaNameValue { path, value, .. }) = meta else { + return None; + }; + + if !path.is_ident(meta_name) { + return None; + } + + let value_str = expr_to_string(value)?; + Some(value_str) +} + // Helper function to parse visibility fn parse_vis_str(s: &str, span: proc_macro2::Span) -> Visibility { match syn::parse_str(s) { @@ -80,19 +96,111 @@ fn parse_vis_str(s: &str, span: proc_macro2::Span) -> Visibility { // Helper function to parse visibility attribute pub fn parse_visibility(attr: Option<&Meta>, meta_name: &str) -> Option { - let meta = attr?; - let Meta::NameValue(MetaNameValue { value, path, .. }) = meta else { - return None; + let value_str = parse_named_value(attr, meta_name)?; + let vis_str = value_str.split(' ').find(|v| v.starts_with("pub"))?; + + Some(parse_vis_str(vis_str, attr.span())) +} + +fn get_option_inner(seg: &PathSegment, span: proc_macro2::Span) -> &Type { + if let PathArguments::AngleBracketed(args) = &seg.arguments { + if let Some(GenericArgument::Type(inner_type)) = args.args.first() { + return inner_type; + } + } + abort!( + span, + "as_ref attribute is only supported on `Option` or `Result`" + ) +} + +fn get_result_inner(seg: &PathSegment, span: proc_macro2::Span) -> (&Type, &Type) { + if let PathArguments::AngleBracketed(args) = &seg.arguments { + let mut args_iter = args.args.iter(); + if let Some(GenericArgument::Type(ok_ty)) = args_iter.next() { + if let Some(GenericArgument::Type(err_ty)) = args_iter.next() { + return (ok_ty, err_ty); + } + } + } + abort!( + span, + "as_ref attribute is only supported on `Option` or `Result`" + ) +} + +// Helper function to parse as_ref attribute +pub fn parse_as_ref(attr: Option<&Meta>, meta_name: &str) -> bool { + let Some(value_str) = parse_named_value(attr, meta_name) else { + return false; }; + value_str.split(' ').any(|v| v == "as_ref") +} - if !path.is_ident(meta_name) { - return None; +fn get_as_ref_return(ty: &Type, span: proc_macro2::Span) -> TokenStream2 { + match ty { + Type::Path(TypePath { path, .. }) => { + let Some(last) = path.segments.last() else { + abort!( + span, + "as_ref attribute is only supported on `Option` or `Result`" + ); + }; + + if last.ident == "Option" { + let inner_ty = get_option_inner(last, span); + return quote! {Option<&#inner_ty>}; + } + + if last.ident == "Result" { + let (ok_ty, err_ty) = get_result_inner(last, span); + return quote! {Result<&#ok_ty, &#err_ty>}; + } + + abort!( + span, + "as_ref attribute is only supported on `Option` or `Result`" + ) + } + _ => abort!( + span, + "as_ref attribute is only supported on `Option` or `Result`" + ), } +} - let value_str = expr_to_string(value)?; - let vis_str = value_str.split(' ').find(|v| *v != "with_prefix")?; +pub fn parse_as_mut(attr: Option<&Meta>, meta_name: &str) -> bool { + let Some(value_str) = parse_named_value(attr, meta_name) else { + return false; + }; + value_str.split(' ').any(|v| v == "as_mut") +} + +fn get_as_mut_return(ty: &Type, span: proc_macro2::Span) -> TokenStream2 { + match ty { + Type::Path(TypePath { path, .. }) => { + if let Some(last) = path.segments.last() { + if last.ident == "Option" { + let inner_ty = get_option_inner(last, span); + return quote! {Option<&mut #inner_ty>}; + } + + if last.ident == "Result" { + let (ok_ty, err_ty) = get_result_inner(last, span); + return quote! {Result<&mut #ok_ty, &mut #err_ty>}; + } + } - Some(parse_vis_str(vis_str, value.span())) + abort!( + span, + "as_mut attribute is only supported on `Option` or `Result`" + ) + } + _ => abort!( + span, + "as_mut attribute is only supported on `Option` or `Results`" + ), + } } /// Some users want legacy/compatibility. @@ -171,11 +279,19 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { Some(meta) if meta.path().is_ident("skip") => quote! {}, Some(_) => match params.mode { Get => { + let (return_code, return_ty) = if parse_as_ref(attr.as_ref(), params.mode.name()) { + ( + quote! {self.#field_name.as_ref()}, + get_as_ref_return(&ty, field.span()), + ) + } else { + (quote! {&self.#field_name}, quote! {&#ty}) + }; quote! { #(#doc)* #[inline(always)] - #visibility fn #fn_name(&self) -> &#ty { - &self.#field_name + #visibility fn #fn_name(&self) -> #return_ty { + #return_code } } } @@ -208,11 +324,19 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { } } GetMut => { + let (return_code, return_ty) = if parse_as_mut(attr.as_ref(), params.mode.name()) { + ( + quote! {self.#field_name.as_mut()}, + get_as_mut_return(&ty, field.span()), + ) + } else { + (quote! {&mut self.#field_name}, quote! {&mut #ty}) + }; quote! { #(#doc)* #[inline(always)] - #visibility fn #fn_name(&mut self) -> &mut #ty { - &mut self.#field_name + #visibility fn #fn_name(&mut self) -> #return_ty { + #return_code } } } @@ -248,11 +372,19 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 Some(_) => match params.mode { Get => { let fn_name = Ident::new("get", Span::call_site()); + let (return_code, return_ty) = if parse_as_ref(attr.as_ref(), params.mode.name()) { + ( + quote! {self.0.as_ref()}, + get_as_ref_return(&ty, field.span()), + ) + } else { + (quote! {&self.0}, quote! {&#ty}) + }; quote! { #(#doc)* #[inline(always)] - #visibility fn #fn_name(&self) -> &#ty { - &self.0 + #visibility fn #fn_name(&self) -> #return_ty { + #return_code } } } @@ -289,11 +421,19 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 } GetMut => { let fn_name = Ident::new("get_mut", Span::call_site()); + let (return_code, return_ty) = if parse_as_mut(attr.as_ref(), params.mode.name()) { + ( + quote! {self.0.as_mut()}, + get_as_mut_return(&ty, field.span()), + ) + } else { + (quote! {&mut self.0}, quote! {&mut #ty}) + }; quote! { #(#doc)* #[inline(always)] - #visibility fn #fn_name(&mut self) -> &mut #ty { - &mut self.0 + #visibility fn #fn_name(&mut self) -> #return_ty { + #return_code } } } diff --git a/src/lib.rs b/src/lib.rs index e5ac3da..b35bb97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -258,6 +258,39 @@ struct CopyUnaryTuple(#[getset(get_copy)] i32); let tup = CopyUnaryTuple(42); ``` + +By default, a field of type `Option` or `Result` generates a getter +returning `&Option` or `&Result`. To get an `Option<&T>` or +`Result<&T, &E>` instead, opt in with the `as_ref` / `as_mut` flags: + +```rust +use getset::{Getters, MutGetters}; + +#[derive(Getters, MutGetters)] +struct MyStruct { + /// Returns `Option<&T>` instead of `&Option` + #[getset(get = "as_ref", get_mut = "as_mut")] + maybe: Option, + + /// Returns `Result<&T, &E>` instead of `&Result` + #[getset(get = "as_ref", get_mut = "as_mut")] + parsed: Result, +} + +let mut s = MyStruct { + maybe: Some(42), + parsed: Err("oops"), +}; + +assert_eq!(s.maybe(), Some(&42)); +assert_eq!(s.maybe_mut(), Some(&mut 42)); + +assert_eq!(s.parsed(), Err(&"oops")); +assert_eq!(s.parsed_mut(), Err(&mut "oops")); +``` + +* `get = "as_ref"` makes the generated getter call `.as_ref()`. +* `get_mut = "as_mut"` makes the generated mutable getter call `.as_mut()`. */ #[macro_use] diff --git a/tests/as_mut.rs b/tests/as_mut.rs new file mode 100644 index 0000000..316bd3d --- /dev/null +++ b/tests/as_mut.rs @@ -0,0 +1,78 @@ +#[macro_use] +extern crate getset; + +#[derive(MutGetters)] +struct ForAsMut { + #[getset(get_mut = "as_mut")] + inner: Option, + + #[getset(get_mut)] + inner_without_as_mut: Option, + + #[getset(get_mut = "as_mut")] + inner_result: Result, + + #[getset(get_mut)] + inner_result_without_as_mut: Result, +} + +impl Default for ForAsMut { + fn default() -> Self { + Self { + inner: None, + inner_without_as_mut: None, + inner_result: Ok(0), + inner_result_without_as_mut: Ok(0), + } + } +} + +#[derive(MutGetters, Default)] +#[getset(get_mut = "as_mut")] +struct Unnamed(Option); + +#[derive(MutGetters)] +#[getset(get_mut = "as_mut")] +struct UnnamedResult(Result); + +impl Default for UnnamedResult { + fn default() -> Self { + Self(Ok(0)) + } +} + +#[test] +fn test_as_mut() { + let mut val = ForAsMut::default(); + assert_eq!(val.inner_mut(), None); + assert_eq!(val.inner_without_as_mut_mut(), &None); + + let mut val = ForAsMut { + inner: Some(1), + inner_without_as_mut: Some(2), + inner_result: Err("error".to_owned()), + inner_result_without_as_mut: Err("error".to_owned()), + }; + assert_eq!(val.inner_mut(), Some(&mut 1)); + assert_eq!(val.inner_without_as_mut_mut(), &mut Some(2)); + assert_eq!(val.inner_result_mut(), Err(&mut "error".to_owned())); + assert_eq!( + val.inner_result_without_as_mut_mut(), + &mut Err("error".to_owned()) + ); +} + +#[test] +fn test_as_mut_unnamed() { + let mut val = Unnamed::default(); + assert_eq!(val.get_mut(), None); + + let mut val = Unnamed(Some(3)); + assert_eq!(val.get_mut(), Some(&mut 3)); + + let mut val = UnnamedResult::default(); + assert_eq!(val.get_mut(), Ok(&mut 0)); + + let mut val = UnnamedResult(Err("error".to_owned())); + assert_eq!(val.get_mut(), Err(&mut "error".to_owned())); +} diff --git a/tests/as_ref.rs b/tests/as_ref.rs new file mode 100644 index 0000000..c2224f8 --- /dev/null +++ b/tests/as_ref.rs @@ -0,0 +1,77 @@ +#[macro_use] +extern crate getset; + +#[derive(Getters)] +struct ForAsRef { + #[getset(get = "as_ref")] + inner: Option, + + #[getset(get)] + inner_without_as_ref: Option, + + #[getset(get = "as_ref")] + inner_result: Result, + + #[getset(get)] + inner_result_without_as_ref: Result, +} + +impl Default for ForAsRef { + fn default() -> Self { + Self { + inner: None, + inner_without_as_ref: None, + inner_result: Ok(0), + inner_result_without_as_ref: Ok(0), + } + } +} + +#[derive(Getters, Default)] +#[getset(get = "as_ref")] +struct Unnamed(Option); + +#[derive(Getters)] +#[getset(get = "as_ref")] +struct UnnamedResult(Result); + +impl Default for UnnamedResult { + fn default() -> Self { + Self(Ok(0)) + } +} + +#[test] +fn test_as_ref() { + let val = ForAsRef::default(); + assert_eq!(val.inner(), None); + assert_eq!(val.inner_without_as_ref(), &None); + assert_eq!(val.inner_result(), Ok(&0)); + assert_eq!(val.inner_result_without_as_ref(), &Ok(0)); + + let val = ForAsRef { + inner: Some(1), + inner_without_as_ref: Some(2), + inner_result: Err("error".to_owned()), + inner_result_without_as_ref: Err("error".to_owned()), + }; + assert_eq!(val.inner(), Some(&1)); + assert_eq!(val.inner_without_as_ref(), &Some(2)); + assert_eq!(val.inner_result(), Err(&"error".to_owned())); + assert_eq!(val.inner_result_without_as_ref(), &Err("error".to_owned())); +} + +#[test] +fn test_as_ref_unnamed() { + let val = Unnamed::default(); + assert_eq!(val.get(), None); + + let val = Unnamed(Some(3)); + assert_eq!(val.get(), Some(&3)); + + let val = UnnamedResult::default(); + assert_eq!(val.get(), Ok(&0)); + + let val = UnnamedResult(Err("error".to_owned())); + assert_eq!(val.get(), Err(&"error".to_owned())); +}