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 the SorobanArbitrary trait and docs #878

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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: 21 additions & 0 deletions Cargo.lock

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

341 changes: 341 additions & 0 deletions soroban-sdk-macros/src/arbitrary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{DataEnum, DataStruct, Ident, Path, Visibility};

pub fn derive_arbitrary_struct(
path: &Path,
vis: &Visibility,
ident: &Ident,
data: &DataStruct,
) -> TokenStream2 {
derive_arbitrary_struct_common(path, vis, ident, data, FieldType::Named)
}

pub fn derive_arbitrary_struct_tuple(
path: &Path,
vis: &Visibility,
ident: &Ident,
data: &DataStruct,
) -> TokenStream2 {
derive_arbitrary_struct_common(path, vis, ident, data, FieldType::Unnamed)
}

enum FieldType {
Named,
Unnamed,
}

fn derive_arbitrary_struct_common(
path: &Path,
vis: &Visibility,
ident: &Ident,
data: &DataStruct,
field_type: FieldType,
) -> TokenStream2 {
let arbitrary_type_ident = format_ident!("Arbitrary{}", ident);

let arbitrary_type_fields: Vec<TokenStream2> = data
.fields
.iter()
.map(|field| {
let field_type = &field.ty;
match &field.ident {
Some(ident) => {
quote! {
#ident: <#field_type as #path::arbitrary::SorobanArbitrary>::Prototype
}
}
None => {
quote! {
<#field_type as #path::arbitrary::SorobanArbitrary>::Prototype
}
}
}
})
.collect();

let field_conversions: Vec<TokenStream2> = data
.fields
.iter()
.enumerate()
.map(|(i, field)| match &field.ident {
Some(ident) => {
quote! {
#ident: #path::IntoVal::into_val(&v.#ident, env)
}
}
None => {
let i = syn::Index::from(i);
quote! {
#path::IntoVal::into_val(&v.#i, env)
}
}
})
.collect();

let arbitrary_type_decl = match field_type {
FieldType::Named => quote! {
struct #arbitrary_type_ident {
#(#arbitrary_type_fields,)*
}
},
FieldType::Unnamed => quote! {
struct #arbitrary_type_ident (
#(#arbitrary_type_fields,)*
);
},
};

let arbitrary_ctor = match field_type {
FieldType::Named => quote! {
#ident {
#(#field_conversions,)*
}
},
FieldType::Unnamed => quote! {
#ident (
#(#field_conversions,)*
)
},
};

quote_arbitrary(
path,
vis,
ident,
arbitrary_type_ident,
arbitrary_type_decl,
arbitrary_ctor,
)
}

pub fn derive_arbitrary_enum(
path: &Path,
vis: &Visibility,
ident: &Ident,
data: &DataEnum,
) -> TokenStream2 {
let arbitrary_type_ident = format_ident!("Arbitrary{}", ident);

let arbitrary_type_variants: Vec<TokenStream2> = data
.variants
.iter()
.map(|variant| {
let mut field_types = None;
let variant_ident = &variant.ident;
let fields: Vec<TokenStream2> = variant
.fields
.iter()
.map(|field| {
let field_type = &field.ty;
match &field.ident {
Some(ident) => {
field_types = Some(FieldType::Named);
quote! {
#ident: <#field_type as #path::arbitrary::SorobanArbitrary>::Prototype
}
}
None => {
field_types = Some(FieldType::Unnamed);
quote! {
<#field_type as #path::arbitrary::SorobanArbitrary>::Prototype
}
}
}
})
.collect();
match field_types {
None => {
quote! {
#variant_ident
}
},
Some(FieldType::Named) => {
quote! {
#variant_ident { #(#fields,)* }
}
}
Some(FieldType::Unnamed) => {
quote! {
#variant_ident ( #(#fields,)* )
}
}
}
})
.collect();

let variant_conversions: Vec<TokenStream2> = data
.variants
.iter()
.map(|variant| {
let mut field_types = None;
let variant_ident = &variant.ident;
let fields: Vec<TokenStream2> = variant
.fields
.iter()
.enumerate()
.map(|(i, field)| {
match &field.ident {
Some(ident) => {
quote! {
#ident
}
}
None => {
let ident = format_ident!("field_{}", i);
quote! {
#ident
}
}
}
})
.collect();
let field_conversions: Vec<TokenStream2> = variant
.fields
.iter()
.enumerate()
.map(|(i, field)| {
match &field.ident {
Some(ident) => {
field_types = Some(FieldType::Named);
quote! {
#ident: #path::IntoVal::into_val(#ident, env)
}
}
None => {
field_types = Some(FieldType::Unnamed);
let ident = format_ident!("field_{}", i);
quote! {
#path::IntoVal::into_val(#ident, env)
}
}
}
})
.collect();
match field_types {
None => {
quote! {
#arbitrary_type_ident::#variant_ident => #ident::#variant_ident
}
},
Some(FieldType::Named) => {
quote! {
#arbitrary_type_ident::#variant_ident { #(#fields,)* } => #ident::#variant_ident { #(#field_conversions,)* }
}
}
Some(FieldType::Unnamed) => {
quote! {
#arbitrary_type_ident::#variant_ident ( #(#fields,)* ) => #ident::#variant_ident ( #(#field_conversions,)* )
}
}
}
})
.collect();

let arbitrary_type_decl = quote! {
enum #arbitrary_type_ident {
#(#arbitrary_type_variants,)*
}
};
let arbitrary_ctor = quote! {
match v {
#(#variant_conversions,)*
}
};

quote_arbitrary(
path,
vis,
ident,
arbitrary_type_ident,
arbitrary_type_decl,
arbitrary_ctor,
)
}

pub fn derive_arbitrary_enum_int(
path: &Path,
vis: &Visibility,
ident: &Ident,
data: &DataEnum,
) -> TokenStream2 {
let arbitrary_type_ident = format_ident!("Arbitrary{}", ident);

let arbitrary_type_variants: Vec<TokenStream2> = data
.variants
.iter()
.map(|variant| {
let variant_ident = &variant.ident;
quote! {
#variant_ident
}
})
.collect();

let variant_conversions: Vec<TokenStream2> = data
.variants
.iter()
.map(|variant| {
let variant_ident = &variant.ident;
quote! {
#arbitrary_type_ident::#variant_ident => #ident::#variant_ident
}
})
.collect();

let arbitrary_type_decl = quote! {
enum #arbitrary_type_ident {
#(#arbitrary_type_variants,)*
}
};
let arbitrary_ctor = quote! {
match v {
#(#variant_conversions,)*
}
};

quote_arbitrary(
path,
vis,
ident,
arbitrary_type_ident,
arbitrary_type_decl,
arbitrary_ctor,
)
}

fn quote_arbitrary(
path: &Path,
vis: &Visibility,
ident: &Ident,
arbitrary_type_ident: Ident,
arbitrary_type_decl: TokenStream2,
arbitrary_ctor: TokenStream2,
) -> TokenStream2 {
quote! {
// This allows us to create a scope to import std and arbitrary, while
// also keeping everything from the current scope. This is better than a
// module because: modules inside functions have surprisingly
// inconsistent scoping rules and visibility management is harder.
#[cfg(feature = "testutils")]
const _: () = {
// derive(Arbitrary) expects these two to be in scope
use #path::arbitrary::std;
use #path::arbitrary::arbitrary;

#[derive(#path::arbitrary::arbitrary::Arbitrary, Debug)]
#vis #arbitrary_type_decl

impl #path::arbitrary::SorobanArbitrary for #ident {
type Prototype = #arbitrary_type_ident;
}

impl #path::TryFromVal<#path::Env, #arbitrary_type_ident> for #ident {
type Error = #path::ConversionError;
fn try_from_val(env: &#path::Env, v: &#arbitrary_type_ident) -> std::result::Result<Self, Self::Error> {
Ok(#arbitrary_ctor)
}
}
};
}
}
Loading