Skip to content

Commit

Permalink
feat: loam macro (#4)
Browse files Browse the repository at this point in the history
This macro takes a trait prefixed with `Is` with instance references, e.g. &self, and generates a new trait without the `Is` which has matching static methods and loads the instance as needed as well as setting it back.
  • Loading branch information
willemneal authored Apr 27, 2023
1 parent 3d85f46 commit 33fc1af
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 71 deletions.
31 changes: 11 additions & 20 deletions crates/loam-core/src/owner.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use loam_sdk::soroban_sdk::{self, contracttype, Address, IntoKey, Lazy};
use loam_sdk::{
loam,
soroban_sdk::{self, contracttype, Address, IntoKey, Lazy},
};

#[contracttype]
#[derive(IntoKey, Default)]
Expand All @@ -13,9 +16,7 @@ pub enum Kind {
None,
}

//#[loam]

impl AnOwnable for Owner {
impl IsOwnable for Owner {
fn owner_get(&self) -> Option<Address> {
match &self.0 {
Kind::Address(address) => Some(address.clone()),
Expand All @@ -28,22 +29,12 @@ impl AnOwnable for Owner {
}
}

pub trait AnOwnable {
#[loam]
pub trait IsOwnable {
/// Get current owner
fn owner_get(&self) -> Option<Address>;
/// Transfer ownership if already set.
/// Should be called in the same transaction as deploying the contract to ensure that
/// a different account doesn't claim ownership
fn owner_set(&mut self, new_owner: Address);
}

pub trait Ownable {
type Impl: Lazy + AnOwnable + Default;
fn owner_get() -> Option<Address> {
Self::Impl::get_lazy()?.owner_get()
}
fn owner_set(owner: Address) {
let mut impl_ = Self::Impl::get_lazy().unwrap_or_default();
if let Some(current_owner) = impl_.owner_get() {
current_owner.require_auth();
}
impl_.owner_set(owner);
Self::Impl::set_lazy(impl_);
}
}
1 change: 1 addition & 0 deletions crates/loam-core/src/redeploy.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(unused_variables)]
use loam_sdk::soroban_sdk::{get_env, BytesN};

pub trait Redeployable: crate::Ownable {
fn redeploy(wasm_hash: BytesN<32>) {
Self::owner_get().unwrap().require_auth();
Expand Down
4 changes: 2 additions & 2 deletions crates/macro-wrapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use syn::{AttributeArgs, Item};
#[proc_macro_attribute]
pub fn loam(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr: AttributeArgs = syn::parse_macro_input!(attr);
let item: Item = syn::parse(item).unwrap();
loam::generate(item, Some(attr)).into()
let parsed: Item = syn::parse(item).unwrap();
loam::generate(parsed, Some(attr)).into()
}

#[proc_macro_derive(IntoKey)]
Expand Down
216 changes: 171 additions & 45 deletions crates/macro-wrapper/src/loam.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{AttributeArgs, Item};
use syn::{punctuated::Punctuated, Attribute, AttributeArgs, FnArg, Item, Signature, Token};

pub mod into_key;

Expand All @@ -21,49 +21,149 @@ impl From<Error> for TokenStream {
pub fn generate(item: Item, attr: Option<AttributeArgs>) -> TokenStream {
inner_generate(item, attr).unwrap_or_else(Into::into)
}

#[allow(clippy::unnecessary_wraps)]
fn inner_generate(item: Item, _attr: Option<AttributeArgs>) -> Result<TokenStream, Error> {
Ok(match &item {
Item::Const(_) => todo!(),
Item::Enum(_) => todo!(),
Item::ExternCrate(_) => todo!(),
Item::Fn(_) => todo!(),
Item::ForeignMod(_) => todo!(),
Item::Impl(impl_) => {
let name = &impl_.self_ty;
quote! {
#impl_
mod test {
pub fn #name() {
todo!("It worked")
}
}
fn is_result_type(output: &syn::ReturnType) -> bool {
if let syn::ReturnType::Type(_, ty) = output {
if let syn::Type::Path(type_path) = &**ty {
// Check if the return type is a Result.
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Result";
}
}
Item::Macro(_) => todo!(),
Item::Macro2(_) => todo!(),
Item::Mod(_) => todo!(),
Item::Static(_) => todo!(),
Item::Struct(strukt) => {
let name = &strukt.ident;
quote! {
#strukt
mod test {
pub fn #name() {
todo!("It worked")
}
}
false
}
fn generate_method(trait_item: &syn::TraitItem) -> Option<TokenStream> {
if let syn::TraitItem::Method(method) = trait_item {
let sig = &method.sig;
let name = &sig.ident;
let output = &sig.output;
let self_ty = get_receiver(sig.inputs.iter().next()?)?;

let is_result = is_result_type(output);
let args_without_self = get_args_without_self(&sig.inputs);
let attrs = &method.attrs;
let return_question_mark = if is_result { Some(quote!(?)) } else { None };

if is_mutable_method(self_ty) {
Some(generate_mutable_method(
sig,
attrs,
name,
&args_without_self,
&return_question_mark,
))
} else {
Some(generate_immutable_method(
sig,
attrs,
name,
&args_without_self,
))
}
} else {
None
}
}

fn get_receiver(arg: &syn::FnArg) -> Option<&syn::Receiver> {
if let syn::FnArg::Receiver(receiver) = arg {
Some(receiver)
} else {
None
}
}

fn get_args_without_self(inputs: &Punctuated<FnArg, Token!(,)>) -> Vec<Ident> {
inputs
.iter()
.skip(1)
.filter_map(|arg| {
if let syn::FnArg::Typed(syn::PatType { pat, .. }) = arg {
match &**pat {
syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.clone()),
_ => None,
}
} else {
None
}
})
.collect::<Vec<_>>()
}

fn is_mutable_method(receiver: &syn::Receiver) -> bool {
receiver.reference.is_some() && receiver.mutability.is_some()
}
fn generate_immutable_method(
sig: &Signature,
attrs: &[Attribute],
name: &Ident,
args_without_self: &[Ident],
) -> TokenStream {
let inputs = sig.inputs.iter().skip(1);
let output = &sig.output;
quote! {
#(#attrs)*
fn #name(#(#inputs),*) #output {
Self::Impl::get_lazy().unwrap_or_default().#name(#(#args_without_self),*)
}
}
}

fn generate_mutable_method(
sig: &Signature,
attrs: &[Attribute],
name: &Ident,
args_without_self: &[Ident],
return_question_mark: &Option<TokenStream>,
) -> TokenStream {
let inputs = sig.inputs.iter().skip(1);
let output = &sig.output;
let result = if return_question_mark.is_some() {
quote!(Ok(res))
} else {
quote!(res)
};
quote! {
#(#attrs)*
fn #name(#(#inputs),*) #output {
let mut impl_ = Self::Impl::get_lazy().unwrap_or_default();
let res = impl_.#name(#(#args_without_self),*) #return_question_mark;
Self::Impl::set_lazy(impl_);
#result
}
Item::Trait(_) => todo!(),
Item::TraitAlias(_) => todo!(),
Item::Type(_) => todo!(),
Item::Union(_) => todo!(),
Item::Use(_) => todo!(),
Item::Verbatim(_) => todo!(),
_ => todo!(),
})
}
}

fn inner_generate(item: Item, _attr: Option<AttributeArgs>) -> Result<TokenStream, Error> {
if let Item::Trait(input_trait) = &item {
let generated_methods = input_trait
.items
.iter()
.filter_map(generate_method)
.collect::<Vec<_>>();

let trait_ident = &input_trait.ident;
let new_trait_ident = syn::Ident::new(
trait_ident.to_string().strip_prefix("Is").ok_or_else(|| {
Error::Stream(quote! { compile_error!("Trait must start with `Is`"); })
})?,
trait_ident.span(),
);
let (_, ty_generics, _) = input_trait.generics.split_for_impl();

let output = quote! {
#item
pub trait #new_trait_ident #ty_generics {
type Impl: Lazy + #trait_ident #ty_generics + Default;
#(#generated_methods)*
}
};
Ok(output)
} else {
Err(Error::Stream(
quote! { compile_error!("Input must be a trait"); },
))
}
}

#[cfg(test)]
Expand Down Expand Up @@ -107,17 +207,43 @@ mod tests {
#[test]
fn first() {
let input: Item = syn::parse_quote! {
struct Foo;
pub trait IsOwnable {
/// Get current owner
fn owner_get(&self) -> Option<Address>;
fn owner_set(&mut self, new_owner: Address) -> Result<(), Error>;
fn owner_set_two(&mut self, new_owner: Address);
}
};
let result = generate(input, None);
println!("{}", format_snippet(&result.to_string()));

let output = quote! {
struct Foo;
mod test {
pub fn Foo() {
todo!("It worked")
pub trait IsOwnable {
/// Get current owner
fn owner_get(&self) -> Option<Address>;
fn owner_set(&mut self, new_owner: Address) -> Result<(), Error>;
fn owner_set_two(&mut self, new_owner: Address);
}
pub trait Ownable {
type Impl: Lazy + IsOwnable + Default;
/// Get current owner
fn owner_get() -> Option<Address> {
Self::Impl::get_lazy().unwrap_or_default().owner_get()
}
fn owner_set(new_owner: Address) -> Result<(), Error> {
let mut impl_ = Self::Impl::get_lazy().unwrap_or_default();
let res = impl_.owner_set(new_owner)?;
Self::Impl::set_lazy(impl_);
Ok(res)
}
fn owner_set_two(new_owner: Address) {
let mut impl_ = Self::Impl::get_lazy().unwrap_or_default();
let res = impl_.owner_set_two(new_owner);
Self::Impl::set_lazy(impl_);
res
}
}

};
equal_tokens(&output, &result);
// let impl_ = syn::parse_str::<ItemImpl>(result.as_str()).unwrap();
Expand Down
6 changes: 4 additions & 2 deletions examples/soroban/base/src/gen.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(clippy::semicolon_if_nothing_returned)]

use loam_sdk::soroban_sdk::{self, contractimpl, set_env, Address, BytesN, Env};
use loam_sdk_core_riffs::{Ownable, Redeployable};

Expand All @@ -9,7 +11,7 @@ pub struct SorobanContract;
impl SorobanContract {
pub fn owner_set(env: Env, owner: Address) {
set_env(env);
Contract::owner_set(owner);
Contract::owner_set(owner)
}
pub fn owner_get(env: Env) -> Option<Address> {
set_env(env);
Expand All @@ -18,6 +20,6 @@ impl SorobanContract {

pub fn redeploy(env: Env, wasm_hash: BytesN<32>) {
set_env(env);
Contract::redeploy(wasm_hash);
Contract::redeploy(wasm_hash)
}
}
1 change: 0 additions & 1 deletion examples/soroban/base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use loam_sdk_core_riffs::{owner::Owner, Ownable, Redeployable};

pub mod gen;

//#[loam]
pub struct Contract;

impl Ownable for Contract {
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export CONFIG_DIR := 'target/'
hash := `soroban contract install --wasm ./target/wasm32-unknown-unknown/contracts/example_status_message.wasm --config-dir ./target`

path:
echo ${PATH}
echo {{ if path_exists('target/bin/soroban') == "true" { "true" } else { "false" } }}

build:
cargo build --package 'example*' --profile contracts --target wasm32-unknown-unknown
Expand Down

0 comments on commit 33fc1af

Please sign in to comment.