// Copyright 2019-2025 Parity Technologies (UK) Ltd. // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. use crate::utils::{FileOrUrl, validate_url_security}; use clap::Parser as ClapParser; use color_eyre::eyre::eyre; use scale_typegen_description::scale_typegen::typegen::{ settings::substitutes::path_segments, validation::{registry_contains_type_path, similar_type_paths_in_registry}, }; use std::path::PathBuf; use subxt_codegen::CodegenBuilder; use subxt_metadata::Metadata; /// Generate runtime API client code from metadata. /// /// # Example (with code formatting) /// /// `subxt codegen | rustfmt --edition=2018 --emit=stdout` #[derive(Debug, ClapParser)] pub struct Opts { #[command(flatten)] file_or_url: FileOrUrl, /// Additional derives #[clap(long = "derive")] derives: Vec, /// Additional attributes #[clap(long = "attribute")] attributes: Vec, /// Path to legacy type definitions (required for metadatas pre-V14) #[clap(long)] legacy_types: Option, /// The spec version of the legacy metadata (required for metadatas pre-V14) #[clap(long)] legacy_spec_version: Option, /// Additional derives for a given type. /// /// Example 1: `--derive-for-type my_module::my_type=serde::Serialize`. /// Example 2: `--derive-for-type my_module::my_type=serde::Serialize,recursive`. #[clap(long = "derive-for-type", value_parser = derive_for_type_parser)] derives_for_type: Vec, /// Additional attributes for a given type. /// /// Example 1: `--attributes-for-type my_module::my_type=#[allow(clippy::all)]`. /// Example 2: `--attributes-for-type my_module::my_type=#[allow(clippy::all)],recursive`. #[clap(long = "attributes-for-type", value_parser = attributes_for_type_parser)] attributes_for_type: Vec, /// Substitute a type for another. /// /// Example `--substitute-type sp_runtime::MultiAddress=subxt::utils::Static<::sp_runtime::MultiAddress>` #[clap(long = "substitute-type", value_parser = substitute_type_parser)] substitute_types: Vec<(String, String)>, /// The `subxt` crate access path in the generated code. /// Defaults to `::subxt::ext::subxt_core`. #[clap(long = "crate")] crate_path: Option, /// Do not generate documentation for the runtime API code. /// /// Defaults to `false` (documentation is generated). #[clap(long, action)] no_docs: bool, /// Whether to limit code generation to only runtime types. /// /// Defaults to `false` (all types are generated). #[clap(long)] runtime_types_only: bool, /// Do not provide default trait derivations for the generated types. /// /// Defaults to `false` (default trait derivations are provided). #[clap(long)] no_default_derives: bool, /// Do not provide default substitutions for the generated types. /// /// Defaults to `false` (default substitutions are provided). #[clap(long)] no_default_substitutions: bool, /// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption #[clap(long, short)] allow_insecure: bool, } #[derive(Debug, Clone)] struct DeriveForType { type_path: String, trait_path: String, recursive: bool, } #[derive(Debug, Clone)] struct AttributeForType { type_path: String, attribute: String, recursive: bool, } fn derive_for_type_parser(src: &str) -> Result { let (type_path, trait_path, recursive) = type_map_parser(src) .ok_or_else(|| String::from("Invalid pattern for `derive-for-type`. It should be `type=derive` or `type=derive,recursive`, like `my_type=serde::Serialize` or `my_type=serde::Serialize,recursive`"))?; Ok(DeriveForType { type_path: type_path.to_string(), trait_path: trait_path.to_string(), recursive, }) } fn attributes_for_type_parser(src: &str) -> Result { let (type_path, attribute, recursive) = type_map_parser(src) .ok_or_else(|| String::from("Invalid pattern for `attributes-for-type`. It should be `type=attribute` like `my_type=serde::#[allow(clippy::all)]` or `type=attribute,recursive` like `my_type=serde::#[allow(clippy::all)],recursive`"))?; Ok(AttributeForType { type_path: type_path.to_string(), attribute: attribute.to_string(), recursive, }) } /// Parses a `&str` of the form `str1=str2` into `(str1, str2, false)` or `str1=str2,recursive` into `(str1, str2, true)`. /// /// A `None` value returned is a parsing error. fn type_map_parser(src: &str) -> Option<(&str, &str, bool)> { let (str1, rest) = src.split_once('=')?; let mut split_rest = rest.split(','); let str2 = split_rest .next() .expect("split iter always returns at least one element; qed"); let mut recursive = false; for r in split_rest { match r { // Note: later we can add other attributes to this match "recursive" => { recursive = true; } _ => return None, } } Some((str1, str2, recursive)) } fn substitute_type_parser(src: &str) -> Result<(String, String), String> { let (from, to) = src .split_once('=') .ok_or_else(|| String::from("Invalid pattern for `substitute-type`. It should be something like `input::Type=replacement::Type`"))?; Ok((from.to_string(), to.to_string())) } pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> { validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?; let bytes = opts.file_or_url.fetch().await?; let legacy_types = opts .legacy_types .map(|path| { let bytes = std::fs::read(path).map_err(|e| eyre!("Cannot read legacy_types: {e}"))?; let types = frame_decode::legacy_types::from_bytes(&bytes) .map_err(|e| eyre!("Cannot deserialize legacy_types: {e}"))?; Ok::<_, color_eyre::eyre::Error>(types) }) .transpose()?; codegen( &bytes, legacy_types, opts.legacy_spec_version, opts.derives, opts.attributes, opts.derives_for_type, opts.attributes_for_type, opts.substitute_types, opts.crate_path, opts.no_docs, opts.runtime_types_only, opts.no_default_derives, opts.no_default_substitutions, output, )?; Ok(()) } #[derive(Clone, Debug)] struct OuterAttribute(syn::Attribute); impl syn::parse::Parse for OuterAttribute { fn parse(input: syn::parse::ParseStream) -> syn::Result { Ok(Self(input.call(syn::Attribute::parse_outer)?[0].clone())) } } #[allow(clippy::too_many_arguments)] fn codegen( metadata_bytes: &[u8], legacy_types: Option, legacy_spec_version: Option, raw_derives: Vec, raw_attributes: Vec, derives_for_type: Vec, attributes_for_type: Vec, substitute_types: Vec<(String, String)>, crate_path: Option, no_docs: bool, runtime_types_only: bool, no_default_derives: bool, no_default_substitutions: bool, output: &mut impl std::io::Write, ) -> color_eyre::Result<()> { let mut codegen = CodegenBuilder::new(); // Use the provided crate path: if let Some(crate_path) = crate_path { let crate_path = syn::parse_str(&crate_path).map_err(|e| eyre!("Cannot parse crate path: {e}"))?; codegen.set_subxt_crate_path(crate_path); } // Respect the boolean flags: if runtime_types_only { codegen.runtime_types_only() } if no_default_derives { codegen.disable_default_derives() } if no_default_substitutions { codegen.disable_default_substitutes() } if no_docs { codegen.no_docs() } let metadata = { let runtime_metadata = subxt_metadata::decode_runtime_metadata(metadata_bytes)?; let mut metadata = match runtime_metadata { // Too old to work with: frame_metadata::RuntimeMetadata::V0(_) | frame_metadata::RuntimeMetadata::V1(_) | frame_metadata::RuntimeMetadata::V2(_) | frame_metadata::RuntimeMetadata::V3(_) | frame_metadata::RuntimeMetadata::V4(_) | frame_metadata::RuntimeMetadata::V5(_) | frame_metadata::RuntimeMetadata::V6(_) | frame_metadata::RuntimeMetadata::V7(_) => { Err(eyre!("Metadata V1-V7 cannot be decoded from")) } // Converting legacy metadatas: frame_metadata::RuntimeMetadata::V8(md) => { let legacy_types = legacy_types .ok_or_else(|| eyre!("--legacy-types needed to load V8 metadata"))?; let legacy_spec = legacy_spec_version .ok_or_else(|| eyre!("--legacy-spec-version needed to load V8 metadata"))?; Metadata::from_v8(&md, &legacy_types.for_spec_version(legacy_spec)) .map_err(|e| eyre!("Cannot load V8 metadata: {e}")) } frame_metadata::RuntimeMetadata::V9(md) => { let legacy_types = legacy_types .ok_or_else(|| eyre!("--legacy-types needed to load V9 metadata"))?; let legacy_spec = legacy_spec_version .ok_or_else(|| eyre!("--legacy-spec-version needed to load V9 metadata"))?; Metadata::from_v9(&md, &legacy_types.for_spec_version(legacy_spec)) .map_err(|e| eyre!("Cannot load V9 metadata: {e}")) } frame_metadata::RuntimeMetadata::V10(md) => { let legacy_types = legacy_types .ok_or_else(|| eyre!("--legacy-types needed to load V10 metadata"))?; let legacy_spec = legacy_spec_version .ok_or_else(|| eyre!("--legacy-spec-version needed to load V10 metadata"))?; Metadata::from_v10(&md, &legacy_types.for_spec_version(legacy_spec)) .map_err(|e| eyre!("Cannot load V10 metadata: {e}")) } frame_metadata::RuntimeMetadata::V11(md) => { let legacy_types = legacy_types .ok_or_else(|| eyre!("--legacy-types needed to load V11 metadata"))?; let legacy_spec = legacy_spec_version .ok_or_else(|| eyre!("--legacy-spec-version needed to load V11 metadata"))?; Metadata::from_v11(&md, &legacy_types.for_spec_version(legacy_spec)) .map_err(|e| eyre!("Cannot load V11 metadata: {e}")) } frame_metadata::RuntimeMetadata::V12(md) => { let legacy_types = legacy_types .ok_or_else(|| eyre!("--legacy-types needed to load V12 metadata"))?; let legacy_spec = legacy_spec_version .ok_or_else(|| eyre!("--legacy-spec-version needed to load V12 metadata"))?; Metadata::from_v12(&md, &legacy_types.for_spec_version(legacy_spec)) .map_err(|e| eyre!("Cannot load V12 metadata: {e}")) } frame_metadata::RuntimeMetadata::V13(md) => { let legacy_types = legacy_types .ok_or_else(|| eyre!("--legacy-types needed to load V13 metadata"))?; let legacy_spec = legacy_spec_version .ok_or_else(|| eyre!("--legacy-spec-version needed to load V13 metadata"))?; Metadata::from_v13(&md, &legacy_types.for_spec_version(legacy_spec)) .map_err(|e| eyre!("Cannot load V13 metadata: {e}")) } // Converting modern metadatas: frame_metadata::RuntimeMetadata::V14(md) => { Metadata::from_v14(md).map_err(|e| eyre!("Cannot load V14 metadata: {e}")) } frame_metadata::RuntimeMetadata::V15(md) => { Metadata::from_v15(md).map_err(|e| eyre!("Cannot load V15 metadata: {e}")) } frame_metadata::RuntimeMetadata::V16(md) => { Metadata::from_v16(md).map_err(|e| eyre!("Cannot load V16 metadata: {e}")) } }?; // Run this first to ensure type paths are unique (which may result in 1,2,3 suffixes being added // to type paths), so that when we validate derives/substitutions below, they are allowed for such // types. See . scale_typegen::utils::ensure_unique_type_paths(metadata.types_mut()) .expect("ensure_unique_type_paths should not fail; please report an issue."); metadata }; // Configure derives: let global_derives = raw_derives .iter() .map(|raw| syn::parse_str(raw)) .collect::, _>>() .map_err(|e| eyre!("Cannot parse global derives: {e}"))?; codegen.set_additional_global_derives(global_derives); for d in derives_for_type { let ty_str = &d.type_path; let ty: syn::TypePath = syn::parse_str(ty_str) .map_err(|e| eyre!("Cannot parse derive for type {ty_str}: {e}"))?; let derive = syn::parse_str(&d.trait_path) .map_err(|e| eyre!("Cannot parse derive for type {ty_str}: {e}"))?; validate_path_with_metadata(&ty.path, &metadata)?; // Note: recursive derives and attributes not supported in the CLI => recursive: false codegen.add_derives_for_type(ty, std::iter::once(derive), d.recursive); } // Configure attributes: let universal_attributes = raw_attributes .iter() .map(|raw| syn::parse_str(raw)) .map(|attr: syn::Result| attr.map(|attr| attr.0)) .collect::, _>>() .map_err(|e| eyre!("Cannot parse global attributes: {e}"))?; codegen.set_additional_global_attributes(universal_attributes); for a in attributes_for_type { let ty_str = &a.type_path; let ty: syn::TypePath = syn::parse_str(ty_str) .map_err(|e| eyre!("Cannot parse attribute for type {ty_str}: {e}"))?; let attribute: OuterAttribute = syn::parse_str(&a.attribute) .map_err(|e| eyre!("Cannot parse attribute for type {ty_str}: {e}"))?; validate_path_with_metadata(&ty.path, &metadata)?; // Note: recursive derives and attributes not supported in the CLI => recursive: false codegen.add_attributes_for_type(ty, std::iter::once(attribute.0), a.recursive); } // Insert type substitutions: for (from_str, to_str) in substitute_types { let from: syn::Path = syn::parse_str(&from_str) .map_err(|e| eyre!("Cannot parse type substitution for path {from_str}: {e}"))?; let to: syn::Path = syn::parse_str(&to_str) .map_err(|e| eyre!("Cannot parse type substitution for path {from_str}: {e}"))?; validate_path_with_metadata(&from, &metadata)?; codegen.set_type_substitute(from, to); } let code = codegen .generate(metadata) .map_err(|e| eyre!("Cannot generate code: {e}"))?; writeln!(output, "{code}")?; Ok(()) } /// Validates that the type path is part of the metadata. fn validate_path_with_metadata(path: &syn::Path, metadata: &Metadata) -> color_eyre::Result<()> { fn pretty_path(path: &syn::Path) -> String { use quote::ToTokens; path.to_token_stream().to_string().replace(' ', "") } let path_segments = path_segments(path); let ident = &path .segments .last() .expect("Empty path should be filtered out before already") .ident; if !registry_contains_type_path(metadata.types(), &path_segments) { let alternatives = similar_type_paths_in_registry(metadata.types(), path); let alternatives: String = if alternatives.is_empty() { format!("There is no Type with name `{ident}` in the provided metadata.") } else { let mut s = "A type with the same name is present at: ".to_owned(); for p in alternatives { s.push('\n'); s.push_str(&pretty_path(&p)); } s }; color_eyre::eyre::bail!( "Type `{}` does not exist at path `{}`\n{}", ident.to_string(), pretty_path(path), alternatives ); } Ok(()) } #[cfg(test)] mod tests { #[test] fn parse_types() { use crate::commands::codegen::type_map_parser; assert_eq!(type_map_parser("Foo"), None); assert_eq!(type_map_parser("Foo=Bar"), Some(("Foo", "Bar", false))); assert_eq!( type_map_parser("Foo=Bar,recursive"), Some(("Foo", "Bar", true)) ); assert_eq!(type_map_parser("Foo=Bar,a"), None); assert_eq!(type_map_parser("Foo=Bar,a,b,c,recursive"), None); } async fn run(args_str: &str) -> color_eyre::Result { let mut args = vec![ "codegen", "--file=../artifacts/polkadot_metadata_small.scale", ]; args.extend(args_str.split(' ').filter(|e| !e.is_empty())); let opts: super::Opts = clap::Parser::try_parse_from(args)?; let mut output: Vec = Vec::new(); let r = super::run(opts, &mut output) .await .map(|_| String::from_utf8(output).unwrap())?; Ok(r) } #[tokio::test] async fn invalid_type_paths() { let valid_type = "sp_runtime::multiaddress::MultiAddress"; let invalid_type = "my_module::MultiAddress"; let valid_cases = [ format!("--derive-for-type {valid_type}=serde::Serialize"), format!("--attributes-for-type {valid_type}=#[allow(clippy::all)]"), format!("--substitute-type {valid_type}=::my_crate::MultiAddress"), ]; for case in valid_cases.iter() { let output = run(case).await; assert!(output.is_ok()); } let invalid_cases = [ format!("--derive-for-type {invalid_type}=serde::Serialize"), format!("--attributes-for-type {invalid_type}=#[allow(clippy::all)]"), format!("--substitute-type {invalid_type}=my_module::MultiAddress"), ]; for case in invalid_cases.iter() { let output = run(case).await; // assert that we make suggestions pointing the user to the valid type assert!(output.unwrap_err().to_string().contains(valid_type)); } } }