codegen: Generate type aliases for better API ergonomics (#1249)

* codegen: Generate type alias for storage return types

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Generate type alias for call function arguments

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Update polkadot.rs code from commit 2e2a75ff81

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Type aliases for runtime API parameters

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Type alias for runtime apis output

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* storage: Change path of the aliased module

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Adjust module indentation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Do not alias for api::runtime_types with unresolved generics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Fix and document runtime API alias generation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update artifacts

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update cargo.lock file

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Generate composite structs with alias unnamed fields

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Update polkadot.rs file

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Alias storage unnamed parameters

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update polkadot.rs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Change polkadot to rococo runtime

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Fix compiling tests in the codegen crate

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Extend storage test with alias module

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* cli/tests: Adjust exepcted commands to the latest metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Remove missleading comment and docs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Ensure unique names for generated runtime API types

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen/tests: Test expected runtime type generation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen/tests: Check duplicate params in runtime APIs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen/tests: Test colliding names of type aliases and parameters

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Fix clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Separate alias module from struct definition

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update polkadot.rs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Remove outdated docs from composite def

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
This commit is contained in:
Alexandru Vasile
2023-12-08 15:07:50 +02:00
committed by GitHub
parent f06a95d687
commit c976d0dbce
15 changed files with 23437 additions and 20542 deletions
+280 -14
View File
@@ -2,6 +2,8 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use std::collections::HashSet;
use crate::{types::TypeGenerator, CodegenError};
use heck::ToSnakeCase as _;
use heck::ToUpperCamelCase as _;
@@ -36,38 +38,80 @@ fn generate_runtime_api(
.then_some(quote! { #( #[doc = #docs ] )* })
.unwrap_or_default();
let mut unique_names = HashSet::new();
let mut unique_aliases = HashSet::new();
let inputs: Vec<_> = method.inputs().enumerate().map(|(idx, input)| {
// These are method names, which can just be '_', but struct field names can't
// just be an underscore, so fix any such names we find to work in structs.
let name = if input.name == "_" {
format_ident!("_{}", idx)
} else {
format_ident!("{}", &input.name)
};
let ty = type_gen.resolve_type_path(input.ty);
let param = quote!(#name: #ty);
(param, name)
let mut name = input.name.trim_start_matches('_').to_string();
if name.is_empty() {
name = format!("_{}", idx);
}
while !unique_names.insert(name.clone()) {
// Name is already used, append the index until it is unique.
name = format!("{}_param{}", name, idx);
}
let mut alias = name.to_upper_camel_case();
// Note: name is not empty.
if alias.as_bytes()[0].is_ascii_digit() {
alias = format!("Param{}", alias);
}
while !unique_aliases.insert(alias.clone()) {
alias = format!("{}Param{}", alias, idx);
}
let (alias_name, name) = (format_ident!("{alias}"), format_ident!("{name}"));
// Generate alias for runtime type.
let ty = type_gen.resolve_type_path(input.ty);
let aliased_param = quote!( pub type #alias_name = #ty; );
// Structures are placed on the same level as the alias module.
let struct_ty_path = quote!( #method_name::#alias_name );
let struct_param = quote!(#name: #struct_ty_path);
// Function parameters must be indented by `types`.
let fn_param = quote!(#name: types::#struct_ty_path);
(fn_param, struct_param, name, aliased_param)
}).collect();
let params = inputs.iter().map(|(param, _)| param);
let param_names = inputs.iter().map(|(_, name)| name);
let fn_params = inputs.iter().map(|(fn_param, _, _, _)| fn_param);
let struct_params = inputs.iter().map(|(_, struct_param, _, _)| struct_param);
let param_names = inputs.iter().map(|(_, _, name, _,)| name);
let type_aliases = inputs.iter().map(|(_, _, _, aliased_param)| aliased_param);
let output = type_gen.resolve_type_path(method.output_ty());
let aliased_module = quote!(
pub mod #method_name {
use super::#types_mod_ident;
#( #type_aliases )*
// Guard the `Output` name against collisions by placing it in a dedicated module.
pub mod output {
use super::#types_mod_ident;
pub type Output = #output;
}
}
);
// From the method metadata generate a structure that holds
// all parameter types. This structure is used with metadata
// to encode parameters to the call via `encode_as_fields_to`.
let derives = type_gen.default_derives();
let struct_name = format_ident!("{}", method.name().to_upper_camel_case());
let struct_params = params.clone();
let struct_input = quote!(
#aliased_module
#derives
pub struct #struct_name {
#( pub #struct_params, )*
}
);
let output = type_gen.resolve_type_path(method.output_ty());
let Some(call_hash) = api.method_hash(method.name()) else {
return Err(CodegenError::MissingRuntimeApiMetadata(
trait_name_str.to_owned(),
@@ -77,7 +121,7 @@ fn generate_runtime_api(
let method = quote!(
#docs
pub fn #method_name(&self, #( #params, )* ) -> #crate_path::runtime_api::Payload<types::#struct_name, #output> {
pub fn #method_name(&self, #( #fn_params, )* ) -> #crate_path::runtime_api::Payload<types::#struct_name, types::#method_name::output::Output> {
#crate_path::runtime_api::Payload::new_static(
#trait_name_str,
#method_name_str,
@@ -160,3 +204,225 @@ pub fn generate_runtime_apis(
}
})
}
#[cfg(test)]
mod tests {
use crate::RuntimeGenerator;
use frame_metadata::v15::{
self, RuntimeApiMetadata, RuntimeApiMethodMetadata, RuntimeApiMethodParamMetadata,
};
use quote::quote;
use scale_info::meta_type;
use subxt_metadata::Metadata;
fn metadata_with_runtime_apis(runtime_apis: Vec<RuntimeApiMetadata>) -> Metadata {
let extrinsic_metadata = v15::ExtrinsicMetadata {
version: 0,
signed_extensions: vec![],
address_ty: meta_type::<()>(),
call_ty: meta_type::<()>(),
signature_ty: meta_type::<()>(),
extra_ty: meta_type::<()>(),
};
let metadata: Metadata = v15::RuntimeMetadataV15::new(
vec![],
extrinsic_metadata,
meta_type::<()>(),
runtime_apis,
v15::OuterEnums {
call_enum_ty: meta_type::<()>(),
event_enum_ty: meta_type::<()>(),
error_enum_ty: meta_type::<()>(),
},
v15::CustomMetadata {
map: Default::default(),
},
)
.try_into()
.expect("can build valid metadata");
metadata
}
fn generate_code(runtime_apis: Vec<RuntimeApiMetadata>) -> String {
let metadata = metadata_with_runtime_apis(runtime_apis);
let item_mod = syn::parse_quote!(
pub mod api {}
);
let generator = RuntimeGenerator::new(metadata);
let generated = generator
.generate_runtime(
item_mod,
Default::default(),
Default::default(),
syn::parse_str("::subxt_path").unwrap(),
false,
)
.expect("should be able to generate runtime");
generated.to_string()
}
#[test]
fn unique_param_names() {
let runtime_apis = vec![RuntimeApiMetadata {
name: "Test",
methods: vec![RuntimeApiMethodMetadata {
name: "test",
inputs: vec![
RuntimeApiMethodParamMetadata {
name: "foo",
ty: meta_type::<bool>(),
},
RuntimeApiMethodParamMetadata {
name: "bar",
ty: meta_type::<bool>(),
},
],
output: meta_type::<bool>(),
docs: vec![],
}],
docs: vec![],
}];
let code = generate_code(runtime_apis);
let structure = quote! {
pub struct Test {
pub foo: test::Foo,
pub bar: test::Bar,
}
};
let expected_alias = quote!(
pub mod test {
use super::runtime_types;
pub type Foo = ::core::primitive::bool;
pub type Bar = ::core::primitive::bool;
pub mod output {
use super::runtime_types;
pub type Output = ::core::primitive::bool;
}
}
);
assert!(code.contains(&structure.to_string()));
assert!(code.contains(&expected_alias.to_string()));
}
#[test]
fn duplicate_param_names() {
let runtime_apis = vec![RuntimeApiMetadata {
name: "Test",
methods: vec![RuntimeApiMethodMetadata {
name: "test",
inputs: vec![
RuntimeApiMethodParamMetadata {
name: "_a",
ty: meta_type::<bool>(),
},
RuntimeApiMethodParamMetadata {
name: "a",
ty: meta_type::<bool>(),
},
RuntimeApiMethodParamMetadata {
name: "__a",
ty: meta_type::<bool>(),
},
],
output: meta_type::<bool>(),
docs: vec![],
}],
docs: vec![],
}];
let code = generate_code(runtime_apis);
let structure = quote! {
pub struct Test {
pub a: test::A,
pub a_param1: test::AParam1,
pub a_param2: test::AParam2,
}
};
let expected_alias = quote!(
pub mod test {
use super::runtime_types;
pub type A = ::core::primitive::bool;
pub type AParam1 = ::core::primitive::bool;
pub type AParam2 = ::core::primitive::bool;
pub mod output {
use super::runtime_types;
pub type Output = ::core::primitive::bool;
}
}
);
assert!(code.contains(&structure.to_string()));
assert!(code.contains(&expected_alias.to_string()));
}
#[test]
fn duplicate_param_and_alias_names() {
let runtime_apis = vec![RuntimeApiMetadata {
name: "Test",
methods: vec![RuntimeApiMethodMetadata {
name: "test",
inputs: vec![
RuntimeApiMethodParamMetadata {
name: "_",
ty: meta_type::<bool>(),
},
RuntimeApiMethodParamMetadata {
name: "_a",
ty: meta_type::<bool>(),
},
RuntimeApiMethodParamMetadata {
name: "_param_0",
ty: meta_type::<bool>(),
},
RuntimeApiMethodParamMetadata {
name: "__",
ty: meta_type::<bool>(),
},
RuntimeApiMethodParamMetadata {
name: "___param_0_param_2",
ty: meta_type::<bool>(),
},
],
output: meta_type::<bool>(),
docs: vec![],
}],
docs: vec![],
}];
let code = generate_code(runtime_apis);
let structure = quote! {
pub struct Test {
pub _0: test::Param0,
pub a: test::A,
pub param_0: test::Param0Param2,
pub _3: test::Param3,
pub param_0_param_2: test::Param0Param2Param4,
}
};
let expected_alias = quote!(
pub mod test {
use super::runtime_types;
pub type Param0 = ::core::primitive::bool;
pub type A = ::core::primitive::bool;
pub type Param0Param2 = ::core::primitive::bool;
pub type Param3 = ::core::primitive::bool;
pub type Param0Param2Param4 = ::core::primitive::bool;
pub mod output {
use super::runtime_types;
pub type Output = ::core::primitive::bool;
}
}
);
assert!(code.contains(&structure.to_string()));
assert!(code.contains(&expected_alias.to_string()));
}
}