diff --git a/Cargo.lock b/Cargo.lock index 764d4ed01aa5..70b0d43120f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21176,6 +21176,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.38", + "trybuild", ] [[package]] diff --git a/polkadot/xcm/procedural/Cargo.toml b/polkadot/xcm/procedural/Cargo.toml index 56df0d94f586..33c2a94be0e4 100644 --- a/polkadot/xcm/procedural/Cargo.toml +++ b/polkadot/xcm/procedural/Cargo.toml @@ -1,9 +1,11 @@ [package] name = "xcm-procedural" +description = "Procedural macros for XCM" authors.workspace = true edition.workspace = true license.workspace = true version = "1.0.0" +publish = true [lib] proc-macro = true @@ -13,3 +15,6 @@ proc-macro2 = "1.0.56" quote = "1.0.28" syn = "2.0.38" Inflector = "0.11.4" + +[dev-dependencies] +trybuild = { version = "1.0.74", features = ["diff"] } diff --git a/polkadot/xcm/procedural/src/builder_pattern.rs b/polkadot/xcm/procedural/src/builder_pattern.rs new file mode 100644 index 000000000000..ebad54e972b6 --- /dev/null +++ b/polkadot/xcm/procedural/src/builder_pattern.rs @@ -0,0 +1,115 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Derive macro for creating XCMs with a builder pattern + +use inflector::Inflector; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + parse_macro_input, Data, DeriveInput, Error, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue, +}; + +pub fn derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let builder_impl = match &input.data { + Data::Enum(data_enum) => generate_methods_for_enum(input.ident, data_enum), + _ => + return Error::new_spanned(&input, "Expected the `Instruction` enum") + .to_compile_error() + .into(), + }; + let output = quote! { + pub struct XcmBuilder(Vec>); + impl Xcm { + pub fn builder() -> XcmBuilder { + XcmBuilder::(Vec::new()) + } + } + #builder_impl + }; + output.into() +} + +fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> TokenStream2 { + let methods = data_enum.variants.iter().map(|variant| { + let variant_name = &variant.ident; + let method_name_string = &variant_name.to_string().to_snake_case(); + let method_name = syn::Ident::new(&method_name_string, variant_name.span()); + let docs: Vec<_> = variant + .attrs + .iter() + .filter_map(|attr| match &attr.meta { + Meta::NameValue(MetaNameValue { + value: Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }), + .. + }) if attr.path().is_ident("doc") => Some(literal.value()), + _ => None, + }) + .map(|doc| syn::parse_str::(&format!("/// {}", doc)).unwrap()) + .collect(); + let method = match &variant.fields { + Fields::Unit => { + quote! { + pub fn #method_name(mut self) -> Self { + self.0.push(#name::::#variant_name); + self + } + } + }, + Fields::Unnamed(fields) => { + let arg_names: Vec<_> = fields + .unnamed + .iter() + .enumerate() + .map(|(index, _)| format_ident!("arg{}", index)) + .collect(); + let arg_types: Vec<_> = fields.unnamed.iter().map(|field| &field.ty).collect(); + quote! { + pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self { + self.0.push(#name::::#variant_name(#(#arg_names),*)); + self + } + } + }, + Fields::Named(fields) => { + let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect(); + let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect(); + quote! { + pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self { + self.0.push(#name::::#variant_name { #(#arg_names),* }); + self + } + } + }, + }; + quote! { + #(#docs)* + #method + } + }); + let output = quote! { + impl XcmBuilder { + #(#methods)* + + pub fn build(self) -> Xcm { + Xcm(self.0) + } + } + }; + output +} diff --git a/polkadot/xcm/procedural/src/lib.rs b/polkadot/xcm/procedural/src/lib.rs index 2ebccadf50be..83cc6cdf98ff 100644 --- a/polkadot/xcm/procedural/src/lib.rs +++ b/polkadot/xcm/procedural/src/lib.rs @@ -18,6 +18,7 @@ use proc_macro::TokenStream; +mod builder_pattern; mod v2; mod v3; mod weight_info; @@ -47,3 +48,15 @@ pub fn impl_conversion_functions_for_junctions_v3(input: TokenStream) -> TokenSt .unwrap_or_else(syn::Error::into_compile_error) .into() } + +/// This is called on the `Instruction` enum, not on the `Xcm` struct, +/// and allows for the following syntax for building XCMs: +/// let message = Xcm::builder() +/// .withdraw_asset(assets) +/// .buy_execution(fees, weight_limit) +/// .deposit_asset(assets, beneficiary) +/// .build(); +#[proc_macro_derive(Builder)] +pub fn derive_builder(input: TokenStream) -> TokenStream { + builder_pattern::derive(input) +} diff --git a/polkadot/xcm/procedural/tests/ui.rs b/polkadot/xcm/procedural/tests/ui.rs new file mode 100644 index 000000000000..a6ec35d0862a --- /dev/null +++ b/polkadot/xcm/procedural/tests/ui.rs @@ -0,0 +1,32 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! UI tests for XCM procedural macros + +#[cfg(not(feature = "disable-ui-tests"))] +#[test] +fn ui() { + // Only run the ui tests when `RUN_UI_TESTS` is set. + if std::env::var("RUN_UI_TESTS").is_err() { + return; + } + + // As trybuild is using `cargo check`, we don't need the real WASM binaries. + std::env::set_var("SKIP_WASM_BUILD", "1"); + + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern.rs new file mode 100644 index 000000000000..e4bcda572ad7 --- /dev/null +++ b/polkadot/xcm/procedural/tests/ui/builder_pattern.rs @@ -0,0 +1,25 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Test error when attaching the derive builder macro to something +//! other than the XCM `Instruction` enum. + +use xcm_procedural::Builder; + +#[derive(Builder)] +struct SomeStruct; + +fn main() {} diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr new file mode 100644 index 000000000000..439b40f31cae --- /dev/null +++ b/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr @@ -0,0 +1,5 @@ +error: Expected the `Instruction` enum + --> tests/ui/builder_pattern.rs:23:1 + | +23 | struct SomeStruct; + | ^^^^^^^^^^^^^^^^^^ diff --git a/polkadot/xcm/src/v3/mod.rs b/polkadot/xcm/src/v3/mod.rs index aa5bfe169ac1..7ec3f6f40af2 100644 --- a/polkadot/xcm/src/v3/mod.rs +++ b/polkadot/xcm/src/v3/mod.rs @@ -404,7 +404,14 @@ impl XcmContext { /// /// This is the inner XCM format and is version-sensitive. Messages are typically passed using the /// outer XCM format, known as `VersionedXcm`. -#[derive(Derivative, Encode, Decode, TypeInfo, xcm_procedural::XcmWeightInfoTrait)] +#[derive( + Derivative, + Encode, + Decode, + TypeInfo, + xcm_procedural::XcmWeightInfoTrait, + xcm_procedural::Builder, +)] #[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))] #[codec(encode_bound())] #[codec(decode_bound())] diff --git a/polkadot/xcm/xcm-simulator/example/src/lib.rs b/polkadot/xcm/xcm-simulator/example/src/lib.rs index 85b8ad1c5cb7..03e7c19a9148 100644 --- a/polkadot/xcm/xcm-simulator/example/src/lib.rs +++ b/polkadot/xcm/xcm-simulator/example/src/lib.rs @@ -649,4 +649,23 @@ mod tests { ); }); } + + #[test] + fn builder_pattern_works() { + let asset: MultiAsset = (Here, 100u128).into(); + let beneficiary: MultiLocation = AccountId32 { id: [0u8; 32], network: None }.into(); + let message: Xcm<()> = Xcm::builder() + .withdraw_asset(asset.clone().into()) + .buy_execution(asset.clone(), Unlimited) + .deposit_asset(asset.clone().into(), beneficiary) + .build(); + assert_eq!( + message, + Xcm(vec![ + WithdrawAsset(asset.clone().into()), + BuyExecution { fees: asset.clone(), weight_limit: Unlimited }, + DepositAsset { assets: asset.into(), beneficiary }, + ]) + ); + } } diff --git a/prdoc/pr_2107.prdoc b/prdoc/pr_2107.prdoc new file mode 100644 index 000000000000..0e33680555ac --- /dev/null +++ b/prdoc/pr_2107.prdoc @@ -0,0 +1,24 @@ +# Schema: Parity PR Documentation Schema (prdoc) +# See doc at https://github.com/paritytech/prdoc + +title: Add a builder pattern to create XCM programs + +doc: + - audience: Core Dev + description: | + XCMs can now be built using a builder pattern like so: + Xcm::builder() + .withdraw_asset(assets) + .buy_execution(fees, weight_limit) + .deposit_asset(assets, beneficiary) + .build(); + +migrations: + db: [] + + runtime: [] + +crates: + - name: xcm + +host_functions: []