forked from hyperledger-iroha/iroha
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
…r-iroha#3068: Add an event matcher proc macro This proc macro generates a wrapper around a bit mask to specifying a set of events to match Signed-off-by: Nikita Strygin <[email protected]>
- Loading branch information
Showing
3 changed files
with
479 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
#![allow(unused)] | ||
|
||
use darling::{FromDeriveInput, FromVariant}; | ||
use iroha_macro_utils::Emitter; | ||
use proc_macro2::TokenStream; | ||
use quote::{quote, ToTokens}; | ||
use syn2::{DeriveInput, Variant}; | ||
|
||
enum FieldsStyle { | ||
Unit, | ||
Unnamed, | ||
Named, | ||
} | ||
|
||
/// Converts the `FieldStyle` to an ignoring pattern (to be put after the variant name) | ||
impl ToTokens for FieldsStyle { | ||
fn to_tokens(&self, tokens: &mut TokenStream) { | ||
match self { | ||
FieldsStyle::Unit => {} | ||
FieldsStyle::Unnamed => tokens.extend(quote!((..))), | ||
FieldsStyle::Named => tokens.extend(quote!({ .. })), | ||
} | ||
} | ||
} | ||
|
||
struct EventMatcherVariant { | ||
event_ident: syn2::Ident, | ||
flag_ident: syn2::Ident, | ||
fields_style: FieldsStyle, | ||
} | ||
|
||
impl FromVariant for EventMatcherVariant { | ||
fn from_variant(variant: &Variant) -> darling::Result<Self> { | ||
let syn2::Variant { | ||
attrs: _, | ||
ident: event_ident, | ||
fields, | ||
discriminant: _, | ||
} = variant; | ||
|
||
// a nested event is an event within an event (like `AccountEvent::Asset`, which bears an `AssetEvent`) | ||
// we detect those by checking whether the payload type (if any) ends with `Event` | ||
let is_nested = match fields { | ||
syn2::Fields::Unnamed(fields) => { | ||
fields.unnamed.len() == 1 | ||
&& matches!(&fields.unnamed[0].ty, syn2::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string().ends_with("Event")) | ||
} | ||
syn2::Fields::Unit | | ||
// just a fail-safe, we don't use named fields in events | ||
syn2::Fields::Named(_) => false, | ||
}; | ||
|
||
// we have a different naming convention for nested events | ||
// to signify that there are actually multiple types of events inside | ||
let flag_ident = if is_nested { | ||
syn2::Ident::new(&format!("Any{event_ident}"), event_ident.span()) | ||
} else { | ||
event_ident.clone() | ||
}; | ||
|
||
let fields_style = match fields { | ||
syn2::Fields::Unnamed(_) => FieldsStyle::Unnamed, | ||
syn2::Fields::Named(_) => FieldsStyle::Named, | ||
syn2::Fields::Unit => FieldsStyle::Unit, | ||
}; | ||
|
||
Ok(Self { | ||
event_ident: event_ident.clone(), | ||
flag_ident, | ||
fields_style, | ||
}) | ||
} | ||
} | ||
|
||
struct EventMatcherEnum { | ||
vis: syn2::Visibility, | ||
event_enum_ident: syn2::Ident, | ||
matcher_ident: syn2::Ident, | ||
variants: Vec<EventMatcherVariant>, | ||
} | ||
|
||
impl FromDeriveInput for EventMatcherEnum { | ||
fn from_derive_input(input: &DeriveInput) -> darling::Result<Self> { | ||
let syn2::DeriveInput { | ||
attrs: _, | ||
vis, | ||
ident: event_ident, | ||
generics, | ||
data, | ||
} = &input; | ||
|
||
let mut accumulator = darling::error::Accumulator::default(); | ||
|
||
if !generics.params.is_empty() { | ||
accumulator.push(darling::Error::custom( | ||
"EventMatcher cannot be derived on generic enums", | ||
)); | ||
} | ||
|
||
let Some(variants) = | ||
darling::ast::Data::<EventMatcherVariant, ()>::try_from(data)?.take_enum() | ||
else { | ||
accumulator.push(darling::Error::custom( | ||
"EventMatcher can be derived only on enums", | ||
)); | ||
|
||
return Err(accumulator.finish().unwrap_err()); | ||
}; | ||
|
||
if variants.len() > 32 { | ||
accumulator.push(darling::Error::custom( | ||
"EventMatcher can be derived only on enums with up to 32 variants", | ||
)); | ||
} | ||
|
||
accumulator.finish_with(Self { | ||
vis: vis.clone(), | ||
event_enum_ident: event_ident.clone(), | ||
matcher_ident: syn2::Ident::new(&format!("{event_ident}Matcher"), event_ident.span()), | ||
variants, | ||
}) | ||
} | ||
} | ||
|
||
impl ToTokens for EventMatcherEnum { | ||
#[allow(clippy::too_many_lines)] // splitting it is not really feasible, it's all tightly coupled =( | ||
fn to_tokens(&self, tokens: &mut TokenStream) { | ||
let Self { | ||
vis, | ||
event_enum_ident, | ||
matcher_ident, | ||
variants, | ||
} = self; | ||
|
||
// definitions of consts for each event | ||
let flag_defs = variants.iter().enumerate().map( | ||
|( | ||
i, | ||
EventMatcherVariant { | ||
flag_ident, | ||
event_ident, | ||
.. | ||
}, | ||
)| { | ||
let i = u32::try_from(i).unwrap(); | ||
let doc = format!(" Matches [`{event_enum_ident}::{event_ident}`]"); | ||
quote! { | ||
#[doc = #doc] | ||
#vis const #flag_ident: Self = Self(1 << #i); | ||
} | ||
}, | ||
); | ||
// identifiers of all the flag constants to use in impls | ||
let flag_idents = variants | ||
.iter() | ||
.map( | ||
|EventMatcherVariant { | ||
flag_ident: ident, .. | ||
}| quote!(Self::#ident), | ||
) | ||
.collect::<Vec<_>>(); | ||
// names of all the flag (as string literals) to use in debug impl | ||
let flag_names = variants.iter().map( | ||
|EventMatcherVariant { | ||
flag_ident: ident, .. | ||
}| { | ||
let flag_name = ident.to_string(); | ||
quote! { | ||
#flag_name | ||
} | ||
}, | ||
); | ||
// patterns for matching events in the `matches` method | ||
let event_patterns = variants.iter().map( | ||
|EventMatcherVariant { | ||
event_ident, | ||
fields_style, | ||
.. | ||
}| { | ||
quote! { | ||
#event_enum_ident::#event_ident #fields_style | ||
} | ||
}, | ||
); | ||
|
||
let doc = format!(" An event matcher for [`{event_enum_ident}`]s\n\nEvent matchers of the same type can be combined with a custom `|` operator"); | ||
|
||
tokens.extend(quote! { | ||
#[derive( | ||
Copy, | ||
Clone, | ||
PartialEq, | ||
Eq, | ||
PartialOrd, | ||
Ord, | ||
Hash, | ||
// this introduces tight coupling with these crates | ||
// but it's the easiest way to make sure those traits are implemented | ||
parity_scale_codec::Decode, | ||
parity_scale_codec::Encode, | ||
serde::Deserialize, | ||
serde::Serialize, | ||
// TODO: we probably want to represent the bit values for each variant in the schema | ||
iroha_schema::IntoSchema, | ||
)] | ||
#[repr(transparent)] | ||
#[doc = #doc] | ||
#vis struct #matcher_ident(u32); | ||
|
||
// we want to imitate an enum here, so not using the SCREAMING_SNAKE_CASE here | ||
#[allow(non_upper_case_globals)] | ||
impl #matcher_ident { | ||
#( #flag_defs )* | ||
} | ||
|
||
impl #matcher_ident { | ||
/// Creates an event matcher that matches no events | ||
pub const fn none() -> Self { | ||
Self(0) | ||
} | ||
|
||
/// Creates an event matcher that matches any event | ||
pub const fn any() -> Self { | ||
Self( | ||
#( | ||
#flag_idents.0 | ||
)|* | ||
) | ||
} | ||
|
||
/// Combines two event matchers into a single event matcher that will match either of the original matchers | ||
/// | ||
/// A const method version of the `|` operator | ||
pub const fn or(self, other: Self) -> Self { | ||
Self(self.0 | other.0) | ||
} | ||
|
||
/// Checks whether an event matcher is a superset of another event matcher | ||
/// | ||
/// That is, whether `self` will match all events that `other` will match | ||
pub const fn contains(&self, other: Self) -> bool { | ||
(self.0 & other.0) == other.0 | ||
} | ||
|
||
/// Checks whether an event matcher matches a specific event | ||
pub const fn matches(&self, event: &#event_enum_ident) -> bool { | ||
match event { | ||
#( | ||
#event_patterns => self.contains(#flag_idents), | ||
)* | ||
} | ||
} | ||
} | ||
|
||
|
||
impl core::default::Default for #matcher_ident { | ||
fn default() -> Self { | ||
Self::any() | ||
} | ||
} | ||
|
||
impl core::fmt::Debug for #matcher_ident { | ||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { | ||
write!(f, "{}[", stringify!(#matcher_ident))?; | ||
|
||
let mut need_comma = false; | ||
|
||
#(if self.contains(#flag_idents) { | ||
if need_comma { | ||
write!(f, ", ")?; | ||
} else { | ||
need_comma = true; | ||
} | ||
write!(f, "{}", #flag_names)? | ||
})* | ||
|
||
write!(f, "]") | ||
} | ||
} | ||
|
||
impl core::ops::BitOr for #matcher_ident { | ||
type Output = Self; | ||
|
||
fn bitor(self, rhs: Self) -> Self { | ||
self.or(rhs) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
pub fn impl_event_matcher_derive(emitter: &mut Emitter, input: &syn2::DeriveInput) -> TokenStream { | ||
let Some(enum_) = emitter.handle(EventMatcherEnum::from_derive_input(input)) else { | ||
return quote! {}; | ||
}; | ||
|
||
quote! { | ||
#enum_ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.