diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 17357c55..016d892b 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -39,6 +39,7 @@ use syn::{ punctuated::Punctuated, token::Comma, visit_mut::VisitMut, + AttrStyle, Data, DataEnum, DataStruct, @@ -49,6 +50,9 @@ use syn::{ Fields, Lifetime, Lit, + Meta, + MetaList, + NestedMeta, Variant, }; @@ -100,7 +104,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() } } }; @@ -127,20 +130,37 @@ fn generate_fields(fields: &FieldsList) -> Vec { StaticLifetimesReplace.visit_type_mut(&mut ty); let type_name = clean_type_string("e!(#ty).to_string()); - - if let Some(i) = ident { - quote! { - .field_of::<#ty>(stringify!(#i), #type_name) - } + let method_call = if is_compact(f) { + quote!(.compact_of::<#ty>) } else { - quote! { - .field_of::<#ty>(#type_name) - } + quote!(.field_of::<#ty>) + }; + if let Some(ident) = ident { + quote!(#method_call(stringify!(#ident), #type_name)) + } else { + quote!(#method_call(#type_name)) } }) .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/derive/src/trait_bounds.rs b/derive/src/trait_bounds.rs index 7ee5b23e..34228ed3 100644 --- a/derive/src/trait_bounds.rs +++ b/derive/src/trait_bounds.rs @@ -51,10 +51,21 @@ pub fn make_where_clause<'a>( let types = collect_types_to_bind(input_ident, data, &ty_params_ids)?; - types.into_iter().for_each(|ty| { - where_clause - .predicates - .push(parse_quote!(#ty : ::scale_info::TypeInfo + 'static)) + types.into_iter().for_each(|(ty, is_compact)| { + // Compact types need extra bounds, T: HasCompact and ::Type: TypeInfo + 'static + if is_compact { + where_clause + .predicates + .push(parse_quote!(#ty : ::scale::HasCompact)); + where_clause + .predicates + .push(parse_quote!(<#ty as ::scale::HasCompact>::Type : ::scale_info::TypeInfo + 'static)); + } else { + where_clause + .predicates + .push(parse_quote!(#ty : ::scale_info::TypeInfo + 'static)); + } }); generics.type_params().into_iter().for_each(|type_param| { @@ -94,14 +105,14 @@ fn type_contains_idents(ty: &Type, idents: &[Ident]) -> bool { visitor.result } -/// Returns all types that must be added to the where clause with the respective -/// trait bound. +/// Returns all types that must be added to the where clause with a boolean +/// indicating if the field is [`scale::Compact`] or not. fn collect_types_to_bind( input_ident: &Ident, data: &syn::Data, ty_params: &[Ident], -) -> Result> { - let types_from_fields = |fields: &Punctuated| -> Vec { +) -> Result> { + let types_from_fields = |fields: &Punctuated| -> Vec<(Type, bool)> { fields .iter() .filter(|field| { @@ -112,7 +123,7 @@ fn collect_types_to_bind( // to not have them in the where clause. !type_contains_idents(&field.ty, &[input_ident.clone()]) }) - .map(|f| f.ty.clone()) + .map(|f| (f.ty.clone(), super::is_compact(f))) .collect() }; diff --git a/src/build.rs b/src/build.rs index 3abdc4a0..3bad9718 100644 --- a/src/build.rs +++ b/src/build.rs @@ -259,6 +259,17 @@ impl FieldsBuilder { self.fields.push(Field::named_of::(name, type_name)); self } + + /// Add a named, [`Compact`] field of type `T`. + pub fn compact_of(mut self, name: &'static str, type_name: &'static str) -> Self + where + T: scale::HasCompact, + ::Type: TypeInfo + 'static, + { + self.fields + .push(Field::compact_of::(Some(name), type_name)); + self + } } impl FieldsBuilder { @@ -270,6 +281,16 @@ impl FieldsBuilder { self.fields.push(Field::unnamed_of::(type_name)); self } + + /// Add an unnamed, [`Compact`] field of type `T`. + pub fn compact_of(mut self, type_name: &'static str) -> Self + where + T: scale::HasCompact, + ::Type: TypeInfo + 'static, + { + self.fields.push(Field::compact_of::(None, type_name)); + self + } } /// Build a type with no variants. diff --git a/src/impls.rs b/src/impls.rs index 42e3bf17..ae6b3b47 100644 --- a/src/impls.rs +++ b/src/impls.rs @@ -26,6 +26,7 @@ use crate::{ Path, Type, TypeDefArray, + TypeDefCompact, TypeDefPhantom, TypeDefPrimitive, TypeDefSequence, @@ -251,3 +252,13 @@ where TypeDefPhantom::new(MetaType::new::()).into() } } + +impl TypeInfo for scale::Compact +where + T: TypeInfo + 'static, +{ + type Identity = Self; + fn type_info() -> Type { + TypeDefCompact::new(MetaType::new::()).into() + } +} diff --git a/src/registry.rs b/src/registry.rs index 4c83c8e9..79d69c61 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 107da62a..fbbf4bea 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -17,6 +17,7 @@ use crate::{ *, }; use core::marker::PhantomData; +use scale::Compact; #[cfg(not(feature = "std"))] use alloc::{ @@ -77,6 +78,11 @@ fn prelude_items() { assert_type!(PhantomData, TypeDefPhantom::new(meta_type::())); } +#[test] +fn scale_compact_types() { + assert_type!(Compact, TypeDefCompact::new(meta_type::())) +} + #[test] fn tuple_primitives() { // unit @@ -128,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 57f122a6..2b709289 100644 --- a/src/ty/fields.rs +++ b/src/ty/fields.rs @@ -26,6 +26,7 @@ use crate::{ use scale::{ Decode, Encode, + HasCompact, }; #[cfg(feature = "serde")] use serde::{ @@ -129,6 +130,15 @@ impl Field { { Self::new(None, MetaType::new::(), type_name) } + + /// Creates a new [`Compact`] field. + pub fn compact_of(name: Option<&'static str>, type_name: &'static str) -> Field + where + T: HasCompact, + ::Type: TypeInfo + 'static, + { + Self::new(name, MetaType::new::<::Type>(), type_name) + } } impl Field diff --git a/src/ty/mod.rs b/src/ty/mod.rs index c15d1346..91429073 100644 --- a/src/ty/mod.rs +++ b/src/ty/mod.rs @@ -94,35 +94,24 @@ impl IntoPortable for Type { } } -impl From for Type { - fn from(primitive: TypeDefPrimitive) -> Self { - Self::new(Path::voldemort(), Vec::new(), primitive) - } -} - -impl From for Type { - fn from(array: TypeDefArray) -> Self { - Self::new(Path::voldemort(), Vec::new(), array) - } -} - -impl From for Type { - fn from(sequence: TypeDefSequence) -> Self { - Self::new(Path::voldemort(), Vec::new(), sequence) - } -} - -impl From for Type { - fn from(tuple: TypeDefTuple) -> Self { - Self::new(Path::voldemort(), Vec::new(), tuple) - } +macro_rules! impl_from_type_def_for_type { + ( $( $t:ty ), + $(,)?) => { $( + impl From<$t> for Type { + fn from(item: $t) -> Self { + Self::new(Path::voldemort(), Vec::new(), item) + } + } + )* } } -impl From for Type { - fn from(phantom: TypeDefPhantom) -> Self { - Self::new(Path::voldemort(), Vec::new(), phantom) - } -} +impl_from_type_def_for_type!( + TypeDefPrimitive, + TypeDefArray, + TypeDefSequence, + TypeDefTuple, + TypeDefCompact, + TypeDefPhantom, +); impl Type { /// Create a [`TypeBuilder`](`crate::build::TypeBuilder`) the public API for constructing a [`Type`] @@ -187,6 +176,8 @@ pub enum TypeDef { Tuple(TypeDefTuple), /// A Rust primitive type. Primitive(TypeDefPrimitive), + /// A type using the [`Compact`] encoding + Compact(TypeDefCompact), /// A PhantomData type. Phantom(TypeDefPhantom), } @@ -202,6 +193,7 @@ impl IntoPortable for TypeDef { TypeDef::Array(array) => array.into_portable(registry).into(), TypeDef::Tuple(tuple) => tuple.into_portable(registry).into(), TypeDef::Primitive(primitive) => primitive.into(), + TypeDef::Compact(compact) => compact.into_portable(registry).into(), TypeDef::Phantom(phantom) => phantom.into_portable(registry).into(), } } @@ -391,6 +383,41 @@ where } } +/// A type wrapped in [`Compact`]. +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TypeDefCompact { + /// The type wrapped in [`Compact`], i.e. the `T` in `Compact`. + #[cfg_attr(feature = "serde", serde(rename = "type"))] + type_param: T::Type, +} + +impl IntoPortable for TypeDefCompact { + type Output = TypeDefCompact; + + fn into_portable(self, registry: &mut Registry) -> Self::Output { + TypeDefCompact { + type_param: registry.register_type(&self.type_param), + } + } +} + +impl TypeDefCompact { + /// Creates a new type wrapped in [`Compact`]. + pub fn new(type_param: MetaType) -> Self { + Self { type_param } + } +} +impl TypeDefCompact +where + T: Form, +{ + /// Returns the [`Compact`] wrapped type, i.e. the `T` in `Compact`. + pub fn type_param(&self) -> &T::Type { + &self.type_param + } +} + /// A type describing a `PhantomData` type. /// /// In the context of SCALE encoded types, including `PhantomData` types in diff --git a/test_suite/tests/derive.rs b/test_suite/tests/derive.rs index fed7895f..4e5016ed 100644 --- a/test_suite/tests/derive.rs +++ b/test_suite/tests/derive.rs @@ -11,16 +11,20 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -#![cfg_attr(not(feature = "std"), no_std)] -use scale_info::prelude::{ - boxed::Box, - marker::PhantomData, -}; +#![cfg_attr(not(feature = "std"), no_std)] use pretty_assertions::assert_eq; +use scale::{ + Compact, + Encode, +}; use scale_info::{ build::*, + prelude::{ + boxed::Box, + marker::PhantomData, + }, tuple_meta_type, Path, Type, @@ -233,6 +237,52 @@ 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 ty_alt = Type::builder() + .path(Path::new("Dense", "derive")) + .composite( + Fields::named() + .field_of::("a", "u8") + .compact_of::("b", "u16"), + ); + assert_type!(Dense, ty_alt); +} + +#[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().compact_of::("AccountIndex")) + .variant( + "Address32", + Fields::unnamed().field_of::<[u8; 32]>("[u8; 32]"), + ), + ); + + assert_type!(MutilatedMultiAddress, ty); +} + #[test] fn whitespace_scrubbing_works() { #[allow(unused)] diff --git a/test_suite/tests/json.rs b/test_suite/tests/json.rs index afc1bed6..393a670e 100644 --- a/test_suite/tests/json.rs +++ b/test_suite/tests/json.rs @@ -28,6 +28,7 @@ use pretty_assertions::{ assert_eq, assert_ne, }; +use scale::Encode; use scale_info::{ form::PortableForm, meta_type, @@ -195,6 +196,53 @@ 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, + a_not_compact: 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() + .compact_of::("a", "u128") + .field_of::("a_not_compact", "u128") + .field_of::<[u8; 32]>("b", "[u8; 32]") + .compact_of::("c", "u64"), + ) + } + } + assert_json_for_type::(json![{ + "path": ["json", "Dense"], + "def": { + "composite": { + "fields": [ + { "name": "a", "type": 1, "typeName": "u128" }, + { "name": "a_not_compact", "type": 2, "typeName": "u128" }, + { "name": "b", "type": 3, "typeName": "[u8; 32]" }, + { "name": "c", "type": 5, "typeName": "u64" }, + ], + }, + } + }]); +} + fn test_struct_with_phantom() { use scale_info::prelude::marker::PhantomData; #[derive(TypeInfo)] @@ -322,6 +370,61 @@ fn test_recursive_type_with_box() { assert_eq!(serde_json::to_value(registry).unwrap(), expected_json,); } +#[test] +fn registry_knows_about_compact_types() { + #[allow(unused)] + #[derive(TypeInfo, Encode)] + struct Dense { + #[codec(compact)] + a: u128, + a_not_compact: u128, + b: [u8; 32], + #[codec(compact)] + c: u64, + } + let mut registry = Registry::new(); + let type_id = registry.register_type(&meta_type::()); + + let expected_json = json!({ + "types": [ + { // type 1 + "path": ["json", "Dense"], + "def": { + "composite": { + "fields": [ + { "name": "a", "type": 2, "typeName": "u128" }, + { "name": "a_not_compact", "type": 3, "typeName": "u128" }, + { "name": "b", "type": 4, "typeName": "[u8; 32]" }, + { "name": "c", "type": 6, "typeName": "u64" } + ] + } + } + }, + { // type 2, the `Compact` of field `a`. + "def": { "compact": { "type": 3 } }, + }, + { // type 3, the `u128` used by type 2 and field `a_not_compact`. + "def": { "primitive": "u128" } + }, + { // type 4, the `[u8; 32]` of field `b`. + "def": { "array": { "len": 32, "type": 5 }} + }, + { // type 5, the `u8` in `[u8; 32]` + "def": { "primitive": "u8" } + }, + { // type 6, the `Compact` of field `c` + "def": { "compact": { "type": 7 } }, + }, + { // type 7, the `u64` in `Compact` of field `c` + "def": { "primitive": "u64" } + }, + ] + }); + + let registry: PortableRegistry = registry.into(); + assert_eq!(serde_json::to_value(registry).unwrap(), expected_json,); +} + #[test] fn test_registry() { let mut registry = Registry::new();