Skip to content

Commit

Permalink
XCM builder pattern (#2107)
Browse files Browse the repository at this point in the history
Added a proc macro to be able to write XCMs using the builder pattern.
This means we go from having to do this:

```rust
let message: Xcm<()> = Xcm(vec![
  WithdrawAsset(assets),
  BuyExecution { fees: asset, weight_limit: Unlimited },
  DepositAsset { assets, beneficiary },
]);
```

to this:

```rust
let message: Xcm<()> = Xcm::builder()
  .withdraw_asset(assets)
  .buy_execution(asset, Unlimited),
  .deposit_asset(assets, beneficiary)
  .build();
```

---------

Co-authored-by: Keith Yeung <[email protected]>
Co-authored-by: command-bot <>
  • Loading branch information
franciscoaguirre and KiChjang authored Nov 8, 2023
1 parent 8ebb5c3 commit 0524aa5
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions polkadot/xcm/procedural/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"] }
115 changes: 115 additions & 0 deletions polkadot/xcm/procedural/src/builder_pattern.rs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

//! 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<Call>(Vec<Instruction<Call>>);
impl<Call> Xcm<Call> {
pub fn builder() -> XcmBuilder<Call> {
XcmBuilder::<Call>(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::<TokenStream2>(&format!("/// {}", doc)).unwrap())
.collect();
let method = match &variant.fields {
Fields::Unit => {
quote! {
pub fn #method_name(mut self) -> Self {
self.0.push(#name::<Call>::#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::<Call>::#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::<Call>::#variant_name { #(#arg_names),* });
self
}
}
},
};
quote! {
#(#docs)*
#method
}
});
let output = quote! {
impl<Call> XcmBuilder<Call> {
#(#methods)*

pub fn build(self) -> Xcm<Call> {
Xcm(self.0)
}
}
};
output
}
13 changes: 13 additions & 0 deletions polkadot/xcm/procedural/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use proc_macro::TokenStream;

mod builder_pattern;
mod v2;
mod v3;
mod weight_info;
Expand Down Expand Up @@ -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)
}
32 changes: 32 additions & 0 deletions polkadot/xcm/procedural/tests/ui.rs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

//! 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");
}
25 changes: 25 additions & 0 deletions polkadot/xcm/procedural/tests/ui/builder_pattern.rs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

//! 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() {}
5 changes: 5 additions & 0 deletions polkadot/xcm/procedural/tests/ui/builder_pattern.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: Expected the `Instruction` enum
--> tests/ui/builder_pattern.rs:23:1
|
23 | struct SomeStruct;
| ^^^^^^^^^^^^^^^^^^
9 changes: 8 additions & 1 deletion polkadot/xcm/src/v3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())]
Expand Down
19 changes: 19 additions & 0 deletions polkadot/xcm/xcm-simulator/example/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
])
);
}
}
24 changes: 24 additions & 0 deletions prdoc/pr_2107.prdoc
Original file line number Diff line number Diff line change
@@ -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: []

0 comments on commit 0524aa5

Please sign in to comment.