Skip to content

Commit b184e22

Browse files
add contractevent
1 parent 8053660 commit b184e22

27 files changed

+2591
-32
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

soroban-sdk-macros/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ proc-macro2 = "1.0"
2929
itertools = "0.10.5"
3030
darling = "0.20.0"
3131
sha2 = "0.10.7"
32+
heck = "0.5.0"
3233

3334
[features]
3435
testutils = []

soroban-sdk-macros/src/attribute.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use syn::Attribute;
1+
use syn::{punctuated::Punctuated, Attribute, Data, Fields, FieldsNamed, FieldsUnnamed};
22

33
/// Returns true if the attribute is an attribute that should be preserved and
44
/// passed through to code generated for the item the attribute is on.
@@ -8,3 +8,84 @@ pub fn pass_through_attr_to_gen_code(attr: &Attribute) -> bool {
88
|| attr.path().is_ident("allow")
99
|| attr.path().is_ident("deny")
1010
}
11+
12+
/// Modifies the input, removing any attributes on struct fields that match the attrs name list.
13+
///
14+
/// Currently implemented only for struct data.
15+
pub fn remove_attributes_from_item(data: &mut Data, attrs: &[&str]) {
16+
let fields = match data {
17+
Data::Struct(data) => match &mut data.fields {
18+
Fields::Named(FieldsNamed { named, .. }) => named,
19+
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => unnamed,
20+
Fields::Unit => &mut Punctuated::default(), // Unit structs have no fields, nothing to do.
21+
},
22+
_ => unimplemented!("Only structs are supported by remove_attributes_from_item"),
23+
};
24+
for field in fields {
25+
field.attrs.retain(|attr| {
26+
!attr
27+
.path()
28+
.get_ident()
29+
.is_some_and(|ident| attrs.contains(&ident.to_string().as_str()))
30+
});
31+
}
32+
}
33+
34+
#[cfg(test)]
35+
mod test {
36+
use quote::{quote, ToTokens};
37+
use syn::DeriveInput;
38+
39+
use super::remove_attributes_from_item;
40+
41+
#[test]
42+
fn test_remove_attributes_from_item_struct_named() {
43+
let input = quote! {
44+
struct Struct {
45+
f1: u32,
46+
#[topic]
47+
f2: u32,
48+
f3: u32,
49+
#[data]
50+
f4: u32,
51+
}
52+
};
53+
let expect = quote! {
54+
struct Struct {
55+
f1: u32,
56+
f2: u32,
57+
f3: u32,
58+
f4: u32,
59+
}
60+
};
61+
let mut input = syn::parse2::<DeriveInput>(input.into()).unwrap();
62+
remove_attributes_from_item(&mut input.data, &["topic", "data"]);
63+
assert_eq!(input.to_token_stream().to_string(), expect.to_string());
64+
}
65+
66+
#[test]
67+
fn test_remove_attributes_from_item_struct_unnamed() {
68+
let input = quote! {
69+
struct Struct(#[topic] u32, u32, #[data] u64);
70+
};
71+
let expect = quote! {
72+
struct Struct(u32, u32, u64);
73+
};
74+
let mut input = syn::parse2::<DeriveInput>(input.into()).unwrap();
75+
remove_attributes_from_item(&mut input.data, &["topic", "data"]);
76+
assert_eq!(input.to_token_stream().to_string(), expect.to_string());
77+
}
78+
79+
#[test]
80+
fn test_remove_attributes_from_item_struct_unit() {
81+
let input = quote! {
82+
struct Struct;
83+
};
84+
let expect = quote! {
85+
struct Struct;
86+
};
87+
let mut input = syn::parse2::<DeriveInput>(input.into()).unwrap();
88+
remove_attributes_from_item(&mut input.data, &["topic", "data"]);
89+
assert_eq!(input.to_token_stream().to_string(), expect.to_string());
90+
}
91+
}
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
use crate::{
2+
attribute::remove_attributes_from_item, default_crate_path, doc::docs_from_attrs,
3+
map_type::map_type, symbol, DEFAULT_XDR_RW_LIMITS,
4+
};
5+
use darling::{ast::NestedMeta, Error, FromMeta};
6+
use heck::ToSnakeCase;
7+
use proc_macro2::Span;
8+
use proc_macro2::TokenStream as TokenStream2;
9+
use quote::{format_ident, quote};
10+
use stellar_xdr::curr::{
11+
ScSpecEntry, ScSpecEventDataFormat, ScSpecEventParamLocationV0, ScSpecEventParamV0,
12+
ScSpecEventV0, WriteXdr,
13+
};
14+
use syn::{parse2, spanned::Spanned, Data, DeriveInput, Fields, LitStr, Path};
15+
16+
#[derive(Debug, FromMeta)]
17+
struct ContractEventArgs {
18+
#[darling(default = "default_crate_path")]
19+
crate_path: Path,
20+
lib: Option<String>,
21+
export: Option<bool>,
22+
#[darling(default)]
23+
data_format: DataFormat,
24+
#[darling(default)]
25+
prefix_topics: Option<Vec<LitStr>>,
26+
}
27+
28+
#[derive(Copy, Clone, Debug, Default)]
29+
pub enum DataFormat {
30+
SingleValue,
31+
Vec,
32+
#[default]
33+
Map,
34+
}
35+
36+
impl FromMeta for DataFormat {
37+
fn from_string(v: &str) -> Result<Self, Error> {
38+
match v {
39+
"single-value" => Ok(Self::SingleValue),
40+
"vec" => Ok(Self::Vec),
41+
"map" => Ok(Self::Map),
42+
_ => Err(Error::custom(format!(
43+
r#"data_format {v} must be one of: "single-value", "vec", or "map"."#
44+
))),
45+
}
46+
}
47+
}
48+
49+
impl Into<ScSpecEventDataFormat> for DataFormat {
50+
fn into(self) -> ScSpecEventDataFormat {
51+
match self {
52+
Self::SingleValue => ScSpecEventDataFormat::SingleValue,
53+
Self::Vec => ScSpecEventDataFormat::Vec,
54+
Self::Map => ScSpecEventDataFormat::Map,
55+
}
56+
}
57+
}
58+
59+
pub fn derive_event(metadata: TokenStream2, input: TokenStream2) -> TokenStream2 {
60+
match derive_event_or_err(metadata, input) {
61+
Ok(tokens) => tokens,
62+
Err(err) => err.write_errors(),
63+
}
64+
}
65+
66+
fn derive_event_or_err(metadata: TokenStream2, input: TokenStream2) -> Result<TokenStream2, Error> {
67+
let args = NestedMeta::parse_meta_list(metadata.into())?;
68+
let args = ContractEventArgs::from_list(&args)?;
69+
let input = parse2::<DeriveInput>(input)?;
70+
let derived = derive_impls(&args, &input)?;
71+
let mut input = input;
72+
remove_attributes_from_item(&mut input.data, &["topic", "data"]);
73+
Ok(quote! {
74+
#input
75+
#derived
76+
}
77+
.into())
78+
}
79+
80+
fn derive_impls(args: &ContractEventArgs, input: &DeriveInput) -> Result<TokenStream2, Error> {
81+
// Collect errors as they are encountered and emit them at the end.
82+
let mut errors = Error::accumulator();
83+
84+
let ident = &input.ident;
85+
let path = &args.crate_path;
86+
87+
let prefix_topics = if let Some(prefix_topics) = &args.prefix_topics {
88+
prefix_topics.iter().map(|t| t.value()).collect()
89+
} else {
90+
vec![input.ident.to_string().to_snake_case()]
91+
};
92+
93+
let fields =
94+
match &input.data {
95+
Data::Struct(struct_) => match &struct_.fields {
96+
Fields::Named(fields) => fields.named.iter(),
97+
Fields::Unnamed(_) => Err(Error::custom(
98+
"structs with unnamed fields are not supported as contract events",
99+
)
100+
.with_span(&struct_.fields.span()))?,
101+
Fields::Unit => Err(Error::custom(
102+
"structs with no fields are not supported as contract events",
103+
)
104+
.with_span(&struct_.fields.span()))?,
105+
},
106+
Data::Enum(_) => Err(Error::custom("enums are not supported as contract events")
107+
.with_span(&input.span()))?,
108+
Data::Union(_) => Err(Error::custom("unions are not supported as contract events")
109+
.with_span(&input.span()))?,
110+
};
111+
112+
// Map each field of the struct to a spec for a param.
113+
let params = fields
114+
.map(|field| {
115+
let ident = field.ident.as_ref().unwrap();
116+
let is_topic = field.attrs.iter().any(|a| a.path().is_ident("topic"));
117+
let location = if is_topic {
118+
ScSpecEventParamLocationV0::TopicList
119+
} else {
120+
ScSpecEventParamLocationV0::Data
121+
};
122+
let doc = docs_from_attrs(&field.attrs);
123+
let name = errors
124+
.handle(ident.to_string().try_into().map_err(|_| {
125+
Error::custom("event field name is too long").with_span(&field.ident.span())
126+
}))
127+
.unwrap_or_default();
128+
let type_ = errors
129+
.handle_in(|| Ok(map_type(&field.ty, false)?))
130+
.unwrap_or_default();
131+
ScSpecEventParamV0 {
132+
location,
133+
doc,
134+
name,
135+
type_,
136+
}
137+
})
138+
.collect::<Vec<_>>();
139+
140+
// If errors have occurred, return them.
141+
let errors = errors.checkpoint()?;
142+
143+
// Generated code spec.
144+
145+
let export = args.export.unwrap_or(true);
146+
let spec_gen = if export {
147+
let spec_entry = ScSpecEntry::EventV0(ScSpecEventV0 {
148+
data_format: args.data_format.into(),
149+
doc: docs_from_attrs(&input.attrs),
150+
lib: args.lib.as_deref().unwrap_or_default().try_into().unwrap(),
151+
name: input.ident.to_string().try_into().unwrap(),
152+
prefix_topics: prefix_topics
153+
.iter()
154+
.map(|t| t.try_into().unwrap())
155+
.collect::<Vec<_>>()
156+
.try_into()
157+
.unwrap(),
158+
params: params
159+
.iter()
160+
.map(|p| p.clone())
161+
.collect::<Vec<_>>()
162+
.try_into()
163+
.unwrap(),
164+
});
165+
let spec_xdr = spec_entry.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap();
166+
let spec_xdr_lit = proc_macro2::Literal::byte_string(spec_xdr.as_slice());
167+
let spec_xdr_len = spec_xdr.len();
168+
let spec_ident = format_ident!(
169+
"__SPEC_XDR_EVENT_{}",
170+
input.ident.to_string().to_uppercase()
171+
);
172+
let ident = &input.ident;
173+
Some(quote! {
174+
#[cfg_attr(target_family = "wasm", link_section = "contractspecv0")]
175+
pub static #spec_ident: [u8; #spec_xdr_len] = #ident::spec_xdr();
176+
177+
impl #ident {
178+
pub const fn spec_xdr() -> [u8; #spec_xdr_len] {
179+
*#spec_xdr_lit
180+
}
181+
}
182+
})
183+
} else {
184+
None
185+
};
186+
187+
// Prepare Topics Conversion to Vec<Val>.
188+
let prefix_topics_symbols = prefix_topics.iter().map(|t| {
189+
symbol::short_or_long(
190+
&args.crate_path,
191+
quote!(env),
192+
&LitStr::new(&t, Span::call_site()),
193+
)
194+
});
195+
let topic_idents = params
196+
.iter()
197+
.filter(|p| p.location == ScSpecEventParamLocationV0::TopicList)
198+
.map(|p| format_ident!("{}", p.name.to_string()))
199+
.collect::<Vec<_>>();
200+
let topics_to_vec_val = quote! {
201+
(
202+
#(&#prefix_topics_symbols,)*
203+
#(&self.#topic_idents,)*
204+
).into_val(env)
205+
};
206+
207+
// Prepare Data Conversion to Val.
208+
let data_params = params
209+
.iter()
210+
.filter(|p| p.location == ScSpecEventParamLocationV0::Data)
211+
.collect::<Vec<_>>();
212+
let data_params_count = data_params.len();
213+
let data_idents = data_params
214+
.iter()
215+
.map(|p| format_ident!("{}", p.name.to_string()))
216+
.collect::<Vec<_>>();
217+
let data_strs = data_idents
218+
.iter()
219+
.map(|i| i.to_string())
220+
.collect::<Vec<_>>();
221+
let data_to_val = match args.data_format {
222+
DataFormat::SingleValue if data_params_count == 0 => {
223+
quote! {
224+
#path::Val::VOID.to_val()
225+
}
226+
}
227+
DataFormat::SingleValue => {
228+
quote! {
229+
use #path::IntoVal;
230+
#(self.#data_idents.into_val(env))*
231+
}
232+
}
233+
DataFormat::Vec if data_params_count == 0 => quote! {
234+
use #path::IntoVal;
235+
#path::Vec::<#path::Val>::new(env).into_val(env)
236+
},
237+
DataFormat::Vec => quote! {
238+
use #path::IntoVal;
239+
(#(&self.#data_idents,)*).into_val(env)
240+
},
241+
DataFormat::Map => quote! {
242+
use #path::{EnvBase,IntoVal,unwrap::UnwrapInfallible};
243+
const KEYS: [&'static str; #data_params_count] = [#(#data_strs),*];
244+
let vals: [#path::Val; #data_params_count] = [
245+
#(self.#data_idents.into_val(env)),*
246+
];
247+
env.map_new_from_slices(&KEYS, &vals).unwrap_infallible().into()
248+
},
249+
};
250+
251+
// Output.
252+
let output = quote! {
253+
#spec_gen
254+
255+
impl #path::Event for #ident {
256+
fn topics(&self, env: &#path::Env) -> #path::Vec<#path::Val> {
257+
#topics_to_vec_val
258+
}
259+
fn data(&self, env: &#path::Env) -> #path::Val {
260+
#data_to_val
261+
}
262+
}
263+
};
264+
265+
errors.finish_with(output)
266+
}

0 commit comments

Comments
 (0)