use std::collections::HashSet;
use heck::{ToSnakeCase as _, ToUpperCamelCase as _};
use pezkuwi_subxt_metadata::{Metadata, RuntimeApiMetadata};
use scale_typegen::{typegen::ir::ToTokensWithSettings, TypeGenerator};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use crate::CodegenError;
pub fn generate_runtime_apis(
metadata: &Metadata,
type_gen: &TypeGenerator,
types_mod_ident: &syn::Ident,
crate_path: &syn::Path,
) -> Result<TokenStream2, CodegenError> {
let runtime_fns: Vec<_> = metadata
.runtime_api_traits()
.map(|api| generate_runtime_api(api, type_gen, crate_path))
.collect::<Result<_, _>>()?;
let trait_defs = runtime_fns.iter().map(|(apis, _)| apis);
let trait_getters = runtime_fns.iter().map(|(_, getters)| getters);
Ok(quote! {
pub mod runtime_apis {
use super::root_mod;
use super::#types_mod_ident;
use #crate_path::ext::codec::Encode;
pub struct RuntimeApi;
impl RuntimeApi {
#( #trait_getters )*
}
#( #trait_defs )*
}
})
}
fn generate_runtime_api(
api: RuntimeApiMetadata,
type_gen: &TypeGenerator,
crate_path: &syn::Path,
) -> Result<(TokenStream2, TokenStream2), CodegenError> {
let types_mod_ident = type_gen.types_mod_ident();
let trait_name_str = api.name();
let trait_name_snake = format_ident!("{}", api.name().to_snake_case());
let docs = api.docs();
let docs: TokenStream2 = type_gen
.settings()
.should_gen_docs
.then_some(quote! { #( #[doc = #docs ] )* })
.unwrap_or_default();
let types_and_methods = api
.methods()
.map(|method| {
let method_name = format_ident!("{}", method.name());
let method_name_str = method.name();
let validation_hash = method.hash();
let docs = method.docs();
let docs: TokenStream2 = type_gen
.settings()
.should_gen_docs
.then_some(quote! { #( #[doc = #docs ] )* })
.unwrap_or_default();
struct Input {
name: syn::Ident,
type_alias: syn::Ident,
type_path: TokenStream2,
}
let runtime_api_inputs: Vec<Input> = {
let mut unique_names = HashSet::new();
let mut unique_aliases = HashSet::new();
method
.inputs()
.enumerate()
.map(|(idx, input)| {
let mut name = input.name.trim_start_matches('_').to_string();
if name.is_empty() {
name = format!("_{idx}");
}
while !unique_names.insert(name.clone()) {
name = format!("{name}_param{idx}");
}
let mut alias = name.trim_start_matches('_').to_upper_camel_case();
if alias.as_bytes()[0].is_ascii_digit() {
alias = format!("Param{alias}");
}
while !unique_aliases.insert(alias.clone()) {
alias = format!("{alias}Param{idx}");
}
let type_path = type_gen
.resolve_type_path(input.id)
.expect("runtime api input type is in metadata; qed")
.to_token_stream(type_gen.settings());
Input {
name: format_ident!("{name}"),
type_alias: format_ident!("{alias}"),
type_path,
}
})
.collect()
};
let input_tuple_types = runtime_api_inputs
.iter()
.map(|i| {
let ty = &i.type_alias;
quote!(#method_name::#ty)
})
.collect::<Vec<_>>();
let input_args = runtime_api_inputs
.iter()
.map(|i| {
let arg = &i.name;
let ty = &i.type_alias;
quote!(#arg: #method_name::#ty)
})
.collect::<Vec<_>>();
let input_param_names = runtime_api_inputs.iter().map(|i| &i.name);
let input_type_aliases = runtime_api_inputs.iter().map(|i| {
let ty = &i.type_alias;
let path = &i.type_path;
quote!(pub type #ty = #path;)
});
let output_type_path = type_gen
.resolve_type_path(method.output_ty())?
.to_token_stream(type_gen.settings());
let runtime_api_types = quote! {
pub mod #method_name {
use super::root_mod;
use super::#types_mod_ident;
#(#input_type_aliases)*
pub mod output {
use super::#types_mod_ident;
pub type Output = #output_type_path;
}
}
};
let runtime_api_method = quote!(
#docs
pub fn #method_name(
&self,
#(#input_args),*
) -> #crate_path::runtime_api::payload::StaticPayload<
(#(#input_tuple_types,)*),
#method_name::output::Output
> {
#crate_path::runtime_api::payload::StaticPayload::new_static(
#trait_name_str,
#method_name_str,
(#(#input_param_names,)*),
[#(#validation_hash,)*],
)
}
);
Ok((runtime_api_types, runtime_api_method))
})
.collect::<Result<Vec<_>, CodegenError>>()?;
let trait_name = format_ident!("{}", trait_name_str);
let types = types_and_methods.iter().map(|(types, _)| types);
let methods = types_and_methods.iter().map(|(_, methods)| methods);
let trait_defs = quote!(
pub mod #trait_name_snake {
use super::root_mod;
use super::#types_mod_ident;
#docs
pub struct #trait_name;
impl #trait_name {
#( #methods )*
}
#( #types )*
}
);
let trait_getter = quote!(
pub fn #trait_name_snake(&self) -> #trait_name_snake::#trait_name {
#trait_name_snake::#trait_name
}
);
Ok((trait_defs, trait_getter))
}
#[cfg(test)]
mod tests {
use crate::RuntimeGenerator;
use frame_metadata::v15::{
self, RuntimeApiMetadata, RuntimeApiMethodMetadata, RuntimeApiMethodParamMetadata,
};
use pezkuwi_subxt_metadata::Metadata;
use quote::quote;
use scale_info::meta_type;
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 expected_alias = quote!(
pub mod test {
use super::{root_mod, 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(&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 expected_alias = quote!(
pub mod test {
use super::{root_mod, 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(&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 expected_alias = quote!(
pub mod test {
use super::{root_mod, 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(&expected_alias.to_string()));
}
}