Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…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
DCNick3 committed Feb 29, 2024
1 parent 2548353 commit f5f5367
Show file tree
Hide file tree
Showing 3 changed files with 479 additions and 0 deletions.
300 changes: 300 additions & 0 deletions data_model/derive/src/event_matcher.rs
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_
}
}
88 changes: 88 additions & 0 deletions data_model/derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! A crate containing various derive macros for `data_model`
mod enum_ref;
mod event_matcher;
mod has_origin;
mod id;
mod model;
Expand Down Expand Up @@ -477,3 +478,90 @@ pub fn has_origin_derive(input: TokenStream) -> TokenStream {

emitter.finish_token_stream_with(result)
}

/// Create an event matcher from an event enum.
///
/// Event matcher is a set of multiple event types, which can be used to match against an event.
///
/// For this event enum:
///
/// ```rust
/// # use iroha_data_model_derive::EventMatcher;
/// #[derive(EventMatcher)]
/// pub enum TestEvent {
/// Event1,
/// Event2,
/// NestedEvent(AnotherEvent),
/// }
/// pub struct AnotherEvent;
/// ```
///
/// The macro will generate a `TestEventMatcher` struct.
///
/// It will have associated constants that correspond to a matcher of each event that can be accessed like `TestEventMatcher::Event1`.
///
/// The matches can be combined either with a `|` operator or with the `or` method to match multiple events at once: `TestEventMatcher::Event1 | TestEventMatcher::Event2`.
///
/// Variants that:
/// 1. have a single unnamed field
/// 2. have the type name end with `Event`
///
/// are treated as nested events and their matcher constant has `Any` prepended to the name. For example, `TestEventMatcher::AnyNestedEvent`.
///
/// The matcher has the following methods:
/// ```ignore
/// impl TestEventMatcher {
/// /// Returns a matcher that doesn't match any events
/// const fn none() -> TestEventMatcher;
/// /// Returns a matcher that matches any event
/// const fn any() -> TestEventMatcher;
/// /// Combines two matchers into one, const form of the `|` operator
/// const fn or(self, other: TestEventMatcher) -> TestEventMatcher;
/// /// Checks if the `other` matcher is a subset of `self` matcher
/// const fn contains(&self, other: Self) -> bool;
/// /// Checks if the matcher matches a specific event
/// const fn matches(&self, event: &TestEvent) -> bool;
/// }
/// ```
///
/// Implemented traits:
/// ```ignore
/// #[derive(
/// Copy,
/// Clone,
/// PartialEq,
/// Eq,
/// PartialOrd,
/// Ord,
/// Hash,
/// parity_scale_codec::Decode,
/// parity_scale_codec::Encode,
/// serde::Deserialize
/// serde::Serialize,
/// iroha_schema::IntoSchema,
/// )]
///
/// /// Default matcher matches any event
/// impl core::default::Default;
///
/// /// Prints the list of matched events
/// ///
/// /// For example: `TestEventMatcher[Event1, AnyNestedEvent]`
/// impl core::fmt::Debug;
///
/// /// Combines two matches
/// impl core::ops::BitOr;
/// ```
#[manyhow]
#[proc_macro_derive(EventMatcher)]
pub fn event_matcher_derive(input: TokenStream) -> TokenStream {
let mut emitter = Emitter::new();

let Some(input) = emitter.handle(syn2::parse2(input)) else {
return emitter.finish_token_stream();
};

let result = event_matcher::impl_event_matcher_derive(&mut emitter, &input);

emitter.finish_token_stream_with(result)
}
Loading

0 comments on commit f5f5367

Please sign in to comment.