Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ContractArgs for building function args #1382

Merged
merged 23 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 4 additions & 17 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ features = ["curr"]
#git = "https://github.com/stellar/rs-stellar-xdr"
#rev = "67be5955a15f1d3a4df83fe86e6ae107f687141b"

#[patch."https://github.com/stellar/rs-soroban-env"]
#soroban-env-common = { path = "../rs-soroban-env/soroban-env-common" }
#soroban-env-guest = { path = "../rs-soroban-env/soroban-env-guest" }
#soroban-env-host = { path = "../rs-soroban-env/soroban-env-host" }
[patch.crates-io]
soroban-env-common = { path = "../rs-soroban-env/soroban-env-common" }
soroban-env-guest = { path = "../rs-soroban-env/soroban-env-guest" }
soroban-env-host = { path = "../rs-soroban-env/soroban-env-host" }
#[patch."https://github.com/stellar/rs-stellar-xdr"]
#stellar-xdr = { path = "../rs-stellar-xdr/" }

Expand Down
103 changes: 103 additions & 0 deletions soroban-sdk-macros/src/derive_args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use itertools::MultiUnzip;
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{Error, FnArg, Lifetime, Type, TypePath, TypeReference};

use crate::syn_ext;

pub fn derive_args_type(ty: &str, name: &str) -> TokenStream {
let ty_str = quote!(#ty).to_string();
let args_doc =
format!("{name} is a type for building arg lists for functions defined in {ty_str}.");
let args_ident = format_ident!("{name}");
quote! {
#[doc = #args_doc]
pub struct #args_ident;
}
}

pub fn derive_args_impl(name: &str, fns: &[syn_ext::Fn]) -> TokenStream {
// Map the traits methods to methods for the Args.
let mut errors = Vec::<Error>::new();
let fns: Vec<_> = fns
.iter()
.map(|f| {
let fn_ident = &f.ident;

// Check for the Env argument.
let env_input = f.inputs.first().and_then(|a| match a {
FnArg::Typed(pat_type) => {
let mut ty = &*pat_type.ty;
if let Type::Reference(TypeReference { elem, .. }) = ty {
ty = elem;
}
if let Type::Path(TypePath {
path: syn::Path { segments, .. },
..
}) = ty
{
if segments.last().map_or(false, |s| s.ident == "Env") {
Some(())
} else {
None
}
} else {
None
}
}
FnArg::Receiver(_) => None,
});

// Map all remaining inputs.
let fn_input_lifetime = Lifetime::new("'i", Span::call_site());
let (fn_input_names, fn_input_types, fn_input_fn_args): (Vec<_>, Vec<_>, Vec<_>) = f
.inputs
.iter()
.skip(if env_input.is_some() { 1 } else { 0 })
.map(|t| {
let ident = match syn_ext::fn_arg_ident(t) {
Ok(ident) => ident,
Err(e) => {
errors.push(e);
format_ident!("_")
}
};
let ty = match syn_ext::fn_arg_ref_type(t, Some(&fn_input_lifetime)) {
Ok(ty) => Some(ty),
Err(e) => {
errors.push(e);
None
}
};
(
ident,
ty,
syn_ext::fn_arg_make_ref(t, Some(&fn_input_lifetime)),
)
})
.multiunzip();

quote! {
pub fn #fn_ident<#fn_input_lifetime>(#(#fn_input_fn_args),*)
-> (#(#fn_input_types,)*)
{
(#(#fn_input_names,)*)
}
}
})
.collect();

// If errors have occurred, render them instead.
if !errors.is_empty() {
let compile_errors = errors.iter().map(Error::to_compile_error);
return quote! { #(#compile_errors)* };
}

// Render the Client.
let args_ident = format_ident!("{}", name);
quote! {
impl #args_ident {
#(#fns)*
}
}
}
2 changes: 1 addition & 1 deletion soroban-sdk-macros/src/derive_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ pub fn derive_client_impl(crate_path: &Path, name: &str, fns: &[syn_ext::Fn]) ->
format_ident!("_")
}
};
(ident, syn_ext::fn_arg_make_ref(t))
(ident, syn_ext::fn_arg_make_ref(t, None))
})
.unzip();
let fn_output = f.output();
Expand Down
50 changes: 50 additions & 0 deletions soroban-sdk-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
extern crate proc_macro;

mod arbitrary;
mod derive_args;
mod derive_client;
mod derive_enum;
mod derive_enum_int;
Expand All @@ -15,6 +16,7 @@ mod path;
mod symbol;
mod syn_ext;

use derive_args::{derive_args_impl, derive_args_type};
use derive_client::{derive_client_impl, derive_client_type};
use derive_enum::derive_type_enum;
use derive_enum_int::derive_type_enum_int;
Expand Down Expand Up @@ -136,8 +138,11 @@ pub fn contract(metadata: TokenStream, input: TokenStream) -> TokenStream {
let fn_set_registry_ident = format_ident!("__{}_fn_set_registry", ty_str.to_lowercase());
let crate_path = &args.crate_path;
let client = derive_client_type(&args.crate_path, &ty_str, &client_ident);
let args_ident = format!("{ty_str}Args");
let contract_args = derive_args_type(&ty_str, &args_ident);
let mut output = quote! {
#input2
#contract_args
#client
};
if cfg!(feature = "testutils") {
Expand Down Expand Up @@ -206,6 +211,18 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
let ty = &imp.self_ty;
let ty_str = quote!(#ty).to_string();

// TODO: Use imp.trait_ in generating the args ident, to create a unique
// args for each trait impl for a contract, to avoid conflicts.
let args_ident = if let Type::Path(path) = &**ty {
path.path
.segments
.last()
.map(|name| format!("{}Args", name.ident))
} else {
None
}
.unwrap_or_else(|| "Args".to_string());

// TODO: Use imp.trait_ in generating the client ident, to create a unique
// client for each trait impl for a contract, to avoid conflicts.
let client_ident = if let Type::Path(path) = &**ty {
Expand Down Expand Up @@ -239,6 +256,7 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
match derived {
Ok(derived_ok) => {
let mut output = quote! {
#[#crate_path::contractargs(name = #args_ident, impl_only = true)]
#[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
#[#crate_path::contractspecfn(name = #ty_str)]
#imp
Expand Down Expand Up @@ -533,6 +551,38 @@ pub fn contractfile(metadata: TokenStream) -> TokenStream {
quote! { #contents_lit }.into()
}

#[derive(Debug, FromMeta)]
struct ContractArgsArgs {
name: String,
#[darling(default)]
impl_only: bool,
}

#[proc_macro_attribute]
pub fn contractargs(metadata: TokenStream, input: TokenStream) -> TokenStream {
let args = match NestedMeta::parse_meta_list(metadata.into()) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(darling::Error::from(e).write_errors());
}
};
let args = match ContractArgsArgs::from_list(&args) {
Ok(v) => v,
Err(e) => return e.write_errors().into(),
};
let input2: TokenStream2 = input.clone().into();
let item = parse_macro_input!(input as HasFnsItem);
let methods: Vec<_> = item.fns();
let args_type = (!args.impl_only).then(|| derive_args_type(&item.name(), &args.name));
let args_impl = derive_args_impl(&args.name, &methods);
quote! {
#input2
#args_type
#args_impl
}
.into()
}

#[derive(Debug, FromMeta)]
struct ContractClientArgs {
#[darling(default = "default_crate_path")]
Expand Down
27 changes: 24 additions & 3 deletions soroban-sdk-macros/src/syn_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use syn::{
};
use syn::{
spanned::Spanned, token::And, Error, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, ItemTrait,
Pat, PatType, TraitItem, TraitItemFn, Type, TypeReference, Visibility,
Lifetime, Pat, PatType, TraitItem, TraitItemFn, Type, TypeReference, Visibility,
};

/// Gets methods from the implementation that have public visibility. For
Expand Down Expand Up @@ -47,9 +47,30 @@ pub fn fn_arg_ident(arg: &FnArg) -> Result<Ident, Error> {
))
}

/// Returns a clone of the type from the FnArg.
pub fn fn_arg_ref_type(arg: &FnArg, lifetime: Option<&Lifetime>) -> Result<Type, Error> {
if let FnArg::Typed(pat_type) = arg {
if !matches!(*pat_type.ty, Type::Reference(_)) {
Ok(Type::Reference(TypeReference {
and_token: And::default(),
lifetime: lifetime.cloned(),
mutability: None,
elem: pat_type.ty.clone(),
}))
} else {
Ok((*pat_type.ty).clone())
}
} else {
Err(Error::new(
arg.span(),
"argument in this form is not supported, use simple named arguments only",
))
}
}

/// Returns a clone of FnArg with the type as a reference if the arg is a typed
/// arg and its type is not already a reference.
pub fn fn_arg_make_ref(arg: &FnArg) -> FnArg {
pub fn fn_arg_make_ref(arg: &FnArg, lifetime: Option<&Lifetime>) -> FnArg {
if let FnArg::Typed(pat_type) = arg {
if !matches!(*pat_type.ty, Type::Reference(_)) {
return FnArg::Typed(PatType {
Expand All @@ -58,7 +79,7 @@ pub fn fn_arg_make_ref(arg: &FnArg) -> FnArg {
colon_token: pat_type.colon_token,
ty: Box::new(Type::Reference(TypeReference {
and_token: And::default(),
lifetime: None,
lifetime: lifetime.cloned(),
mutability: None,
elem: pat_type.ty.clone(),
})),
Expand Down
3 changes: 3 additions & 0 deletions soroban-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,9 @@ pub use soroban_sdk_macros::contractmeta;
/// ```
pub use soroban_sdk_macros::contracttype;

/// Generates a type that helps build function args for a contract trait.
pub use soroban_sdk_macros::contractargs;

/// Generates a client for a contract trait.
///
/// Can be used to create clients for contracts that live outside the current
Expand Down
2 changes: 2 additions & 0 deletions soroban-spec-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub fn generate_without_file(specs: &[ScSpecEntry]) -> TokenStream {
let error_enums = spec_error_enums.iter().map(|s| generate_error_enum(s));

quote! {
#[soroban_sdk::contractargs(name = "Args")]
#[soroban_sdk::contractclient(name = "Client")]
#trait_

Expand Down Expand Up @@ -134,6 +135,7 @@ mod test {
assert_eq!(
rust,
r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "<file>", sha256 = "<sha256>");
#[soroban_sdk::contractargs(name = "Args")]
#[soroban_sdk::contractclient(name = "Client")]
pub trait Contract {
fn add(env: soroban_sdk::Env, a: UdtEnum, b: UdtEnum) -> i64;
Expand Down
2 changes: 1 addition & 1 deletion tests/constructor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl Contract {
#[test]
fn test_constructor() {
let env = Env::default();
let contract_id = env.register(Contract, (100_u32, 1000_i64));
let contract_id = env.register(Contract, ContractArgs::__constructor(&100_u32, &1000_i64));
let client = ContractClient::new(&env, &contract_id);
assert_eq!(client.get_data(&DataKey::Persistent(100)), Some(1000));
assert_eq!(client.get_data(&DataKey::Temp(200)), Some(2000));
Expand Down
1 change: 1 addition & 0 deletions tests/modular/src/feat1.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use soroban_sdk::contractimpl;

use crate::Contract;
use crate::ContractArgs;
use crate::ContractClient;

#[contractimpl]
Expand Down
1 change: 1 addition & 0 deletions tests/modular/src/feat2.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use soroban_sdk::contractimpl;

use crate::ContractArgs;
use crate::ContractClient;

#[contractimpl]
Expand Down
Loading