diff --git a/scale-encode-derive/src/lib.rs b/scale-encode-derive/src/lib.rs index 8ca9c3f..2f4d9a2 100644 --- a/scale-encode-derive/src/lib.rs +++ b/scale-encode-derive/src/lib.rs @@ -13,14 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use darling::FromAttributes; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{parse_macro_input, punctuated::Punctuated, DeriveInput}; +// The default attribute name for attrs const ATTR_NAME: &str = "encode_as_type"; // Macro docs in main crate; don't add any docs here. -#[proc_macro_derive(EncodeAsType, attributes(encode_as_type))] +#[proc_macro_derive(EncodeAsType, attributes(encode_as_type, codec))] pub fn derive_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -76,6 +78,7 @@ fn generate_enum_impl( quote!( impl #impl_generics #path_to_scale_encode::EncodeAsType for #path_to_type #ty_generics #where_clause { + #[allow(unused_variables)] fn encode_as_type_to( &self, // long variable names to prevent conflict with struct field names: @@ -108,6 +111,7 @@ fn generate_struct_impl( quote!( impl #impl_generics #path_to_scale_encode::EncodeAsType for #path_to_type #ty_generics #where_clause { + #[allow(unused_variables)] fn encode_as_type_to( &self, // long variable names to prevent conflict with struct field names: @@ -124,6 +128,7 @@ fn generate_struct_impl( } } impl #impl_generics #path_to_scale_encode::EncodeAsFields for #path_to_type #ty_generics #where_clause { + #[allow(unused_variables)] fn encode_as_fields_to( &self, // long variable names to prevent conflict with struct field names: @@ -181,11 +186,14 @@ fn fields_to_matcher_and_composite( let field_name = &f.ident; quote!(#field_name) }); - let tuple_body = fields.named.iter().map(|f| { - let field_name_str = f.ident.as_ref().unwrap().to_string(); - let field_name = &f.ident; - quote!((Some(#field_name_str), #field_name as &dyn #path_to_scale_encode::EncodeAsType)) - }); + let tuple_body = fields.named + .iter() + .filter(|f| !should_skip(&f.attrs)) + .map(|f| { + let field_name_str = f.ident.as_ref().unwrap().to_string(); + let field_name = &f.ident; + quote!((Some(#field_name_str), #field_name as &dyn #path_to_scale_encode::EncodeAsType)) + }); ( quote!({#( #match_body ),*}), @@ -197,10 +205,12 @@ fn fields_to_matcher_and_composite( .unnamed .iter() .enumerate() - .map(|(idx, _)| format_ident!("_{idx}")); - let match_body = field_idents.clone().map(|i| quote!(#i)); + .map(|(idx, f)| (format_ident!("_{idx}"), f)); + + let match_body = field_idents.clone().map(|(i, _)| quote!(#i)); let tuple_body = field_idents - .map(|i| quote!((None as Option<&'static str>, #i as &dyn #path_to_scale_encode::EncodeAsType))); + .filter(|(_, f)| !should_skip(&f.attrs)) + .map(|(i, _)| quote!((None as Option<&'static str>, #i as &dyn #path_to_scale_encode::EncodeAsType))); ( quote!((#( #match_body ),*)), @@ -255,3 +265,18 @@ impl TopLevelAttrs { Ok(res) } } + +// Checks if the attributes contain `skip`. +// +// NOTE: Since we only care about `skip` at the moment, we just expose this helper, +// but if we add more attrs we can expose `FieldAttrs` properly: +fn should_skip(attrs: &[syn::Attribute]) -> bool { + #[derive(FromAttributes, Default)] + #[darling(attributes(encode_as_type, codec))] + struct FieldAttrs { + #[darling(default)] + skip: bool, + } + + FieldAttrs::from_attributes(attrs).unwrap_or_default().skip +} diff --git a/scale-encode/src/impls/mod.rs b/scale-encode/src/impls/mod.rs index 8c66d50..a1d555e 100644 --- a/scale-encode/src/impls/mod.rs +++ b/scale-encode/src/impls/mod.rs @@ -1079,4 +1079,78 @@ mod test { ("hello".to_string(), (123u8,), true, (1u64,)), ); } + + #[test] + fn encode_to_number_skipping_attrs_via_macro_works() { + struct NotEncodeAsType; + + #[derive(EncodeAsType)] + #[encode_as_type(crate_path = "crate")] + struct FooNotSkipping { + value: u64, + other: bool, + third: String, + } + + #[derive(EncodeAsType)] + #[encode_as_type(crate_path = "crate")] + struct FooSkipping { + value: u64, + #[encode_as_type(skip)] + other: bool, + // Even though this type doesn't impl EncodeAsType, + // it's ignored so should be fine: + #[codec(skip)] + third: NotEncodeAsType, + } + + assert_value_roundtrips_to( + FooSkipping { + value: 123, + other: true, + third: NotEncodeAsType, + }, + 123u64, + ); + } + + #[test] + fn encode_unnamed_to_number_skipping_attrs_via_macro_works() { + struct NotEncodeAsType; + + #[derive(EncodeAsType)] + #[encode_as_type(crate_path = "crate")] + struct FooSkipping( + u64, + #[encode_as_type(skip)] bool, + // Even though this type doesn't impl EncodeAsType, + // it's ignored so should be fine: + #[codec(skip)] NotEncodeAsType, + ); + + assert_value_roundtrips_to(FooSkipping(123, true, NotEncodeAsType), 123u64); + } + + // If you don't skip values, you can't turn a multi-value + // struct into a number. + #[test] + #[should_panic] + fn encode_to_number_not_skipping_via_macro_fails() { + #[derive(EncodeAsType)] + #[encode_as_type(crate_path = "crate")] + struct FooNotSkipping { + value: u64, + other: bool, + third: String, + } + + assert_value_roundtrips_to( + FooNotSkipping { + value: 123, + other: true, + third: "hello".to_string(), + }, + 123u64, + ); + } }