diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 03dfb848..8f2a02ff 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -37,6 +37,7 @@ use syn::{ }, punctuated::Punctuated, token::Comma, + AttrStyle, Data, DataEnum, DataStruct, @@ -46,6 +47,9 @@ use syn::{ Field, Fields, Lit, + Meta, + MetaList, + NestedMeta, Variant, }; @@ -92,7 +96,6 @@ fn generate_type(input: TokenStream2) -> Result { .path(::scale_info::Path::new(stringify!(#ident), module_path!())) .type_params(::scale_info::prelude::vec![ #( #generic_type_ids ),* ]) .#build_type - .into() } } }; @@ -108,20 +111,43 @@ fn generate_fields(fields: &FieldsList) -> Vec { .map(|f| { let (ty, ident) = (&f.ty, &f.ident); let type_name = clean_type_string("e!(#ty).to_string()); - + let compact = if is_compact(f) { + quote! { + .compact() + } + } else { + quote! {} + }; if let Some(i) = ident { quote! { - .field_of::<#ty>(stringify!(#i), #type_name) + .field_of::<#ty>(stringify!(#i), #type_name) #compact } } else { quote! { - .field_of::<#ty>(#type_name) + .field_of::<#ty>(#type_name) #compact } } }) .collect() } +/// Look for a `#[codec(compact)]` outer attribute. +fn is_compact(f: &Field) -> bool { + f.attrs.iter().any(|attr| { + let mut is_compact = false; + if attr.style == AttrStyle::Outer && attr.path.is_ident("codec") { + if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() { + if let Some(NestedMeta::Meta(Meta::Path(path))) = nested.iter().next() { + if path.is_ident("compact") { + is_compact = true; + } + } + } + } + is_compact + }) +} + fn clean_type_string(input: &str) -> String { input .replace(" ::", "::") diff --git a/src/build.rs b/src/build.rs index 3abdc4a0..388a8916 100644 --- a/src/build.rs +++ b/src/build.rs @@ -248,6 +248,15 @@ impl FieldsBuilder { pub fn finalize(self) -> Vec> { self.fields } + + /// Mark last field as compact, meaning that encoding/decoding should be in the [`scale_codec::Compact`] format. + pub fn compact(mut self) -> Self { + self.fields.iter_mut().last().map(|f| { + f.compact(); + f + }); + self + } } impl FieldsBuilder { diff --git a/src/registry.rs b/src/registry.rs index a3ce846c..7dacd033 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -270,7 +270,6 @@ mod tests { "&mut RecursiveRefs", ), ) - .into() } } diff --git a/src/tests.rs b/src/tests.rs index 49de287b..965a0182 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -134,7 +134,6 @@ fn struct_with_generics() { .path(Path::new("MyStruct", module_path!())) .type_params(tuple_meta_type!(T)) .composite(Fields::named().field_of::("data", "T")) - .into() } } diff --git a/src/ty/fields.rs b/src/ty/fields.rs index 018a39b0..47abb036 100644 --- a/src/ty/fields.rs +++ b/src/ty/fields.rs @@ -86,6 +86,17 @@ pub struct Field { ty: T::Type, /// The name of the type of the field as it appears in the source code. type_name: T::String, + /// This field should be encode/decoded as a + /// [`Compact`](parity_scale_codec::Compact) field + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false", default))] + compact: bool, +} + +// Need to obey the required serde signature here +#[allow(clippy::trivially_copy_pass_by_ref)] +#[allow(dead_code)] +const fn is_false(v: &bool) -> bool { + !(*v) } impl IntoPortable for Field { @@ -96,6 +107,7 @@ impl IntoPortable for Field { name: self.name.map(|name| name.into_portable(registry)), ty: registry.register_type(&self.ty), type_name: self.type_name.into_portable(registry), + compact: self.compact, } } } @@ -108,11 +120,13 @@ impl Field { name: Option<&'static str>, ty: MetaType, type_name: &'static str, + compact: bool, ) -> Self { Self { name, ty, type_name, + compact, } } @@ -124,7 +138,7 @@ impl Field { where T: TypeInfo + ?Sized + 'static, { - Self::new(Some(name), MetaType::new::(), type_name) + Self::new(Some(name), MetaType::new::(), type_name, false) } /// Creates a new unnamed field. @@ -135,7 +149,7 @@ impl Field { where T: TypeInfo + ?Sized + 'static, { - Self::new(None, MetaType::new::(), type_name) + Self::new(None, MetaType::new::(), type_name, false) } } @@ -161,4 +175,10 @@ where pub fn type_name(&self) -> &T::String { &self.type_name } + + /// Set the `compact` property to true, signalling that this type is to be + /// encoded/decoded as a [`parity_scale_codec::Compact`]. + pub fn compact(&mut self) { + self.compact = true; + } } diff --git a/test_suite/tests/derive.rs b/test_suite/tests/derive.rs index 784e3b41..c2b3f824 100644 --- a/test_suite/tests/derive.rs +++ b/test_suite/tests/derive.rs @@ -13,11 +13,11 @@ // limitations under the License. #![cfg_attr(not(feature = "std"), no_std)] -use scale_info::prelude::boxed::Box; - use pretty_assertions::assert_eq; +use scale::Encode; use scale_info::{ build::*, + prelude::boxed::Box, tuple_meta_type, Path, Type, @@ -204,6 +204,57 @@ fn associated_types_derive_without_bounds() { assert_type!(Assoc, struct_type); } +#[test] +fn scale_compact_types_work_in_structs() { + #[allow(unused)] + #[derive(Encode, TypeInfo)] + struct Dense { + a: u8, + #[codec(compact)] + b: u16, + } + + let dense = Type::builder() + .path(Path::new("Dense", "derive")) + .composite( + Fields::named() + .field_of::("a", "u8") + .field_of::("b", "u16") + .compact(), + ); + + assert_type!(Dense, dense); +} + +#[test] +fn scale_compact_types_work_in_enums() { + #[allow(unused)] + #[derive(Encode, TypeInfo)] + enum MutilatedMultiAddress { + Id(AccountId), + Index(#[codec(compact)] AccountIndex), + Address32([u8; 32]), + } + + let ty = Type::builder() + .path(Path::new("MutilatedMultiAddress", "derive")) + .type_params(tuple_meta_type!(u8, u16)) + .variant( + Variants::with_fields() + .variant("Id", Fields::unnamed().field_of::("AccountId")) + .variant( + "Index", + Fields::unnamed().field_of::("AccountIndex").compact(), + ) + .variant( + "Address32", + Fields::unnamed().field_of::<[u8; 32]>("[u8; 32]"), + ), + ); + + assert_type!(MutilatedMultiAddress, ty); +} + #[rustversion::nightly] #[test] fn ui_tests() { diff --git a/test_suite/tests/json.rs b/test_suite/tests/json.rs index 6b8e6ddd..2a29ebe3 100644 --- a/test_suite/tests/json.rs +++ b/test_suite/tests/json.rs @@ -197,6 +197,54 @@ fn test_struct() { })); } +#[test] +fn test_struct_with_some_fields_marked_as_compact() { + use scale::Encode; + + // #[derive(TypeInfo, Encode)] + #[derive(Encode)] + struct Dense { + #[codec(compact)] + a: u128, + b: [u8; 32], + #[codec(compact)] + c: u64, + } + use scale_info::{ + build::Fields, + Path, + Type, + }; + impl TypeInfo for Dense { + type Identity = Self; + fn type_info() -> Type { + Type::builder() + .path(Path::new("Dense", module_path!())) + .composite( + Fields::named() + .field_of::("a", "i32") + .compact() + .field_of::<[u8; 32]>("b", "[u8; 32]") + .field_of::("c", "u64") + .compact(), + ) + } + } + + assert_json_for_type::(json![{ + "path": ["json", "Dense"], + "def": { + "composite": { + "fields": [ + { "name": "a", "type": 1, "typeName": "i32", "compact": true }, + { "name": "b", "type": 2, "typeName": "[u8; 32]" }, + { "name": "c", "type": 3, "typeName": "u64", "compact": true }, + ], + }, + } + }]); +} + #[test] fn test_clike_enum() { #[derive(TypeInfo)]