From f5f5367a11c6fcc5d65aabacd183d1528e085746 Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Thu, 29 Feb 2024 16:59:20 +0300 Subject: [PATCH] [refactor] #1981, #4195, #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 --- data_model/derive/src/event_matcher.rs | 300 +++++++++++++++++++++++ data_model/derive/src/lib.rs | 88 +++++++ data_model/derive/tests/event_matcher.rs | 91 +++++++ 3 files changed, 479 insertions(+) create mode 100644 data_model/derive/src/event_matcher.rs create mode 100644 data_model/derive/tests/event_matcher.rs diff --git a/data_model/derive/src/event_matcher.rs b/data_model/derive/src/event_matcher.rs new file mode 100644 index 00000000000..8093d484022 --- /dev/null +++ b/data_model/derive/src/event_matcher.rs @@ -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 { + 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, +} + +impl FromDeriveInput for EventMatcherEnum { + fn from_derive_input(input: &DeriveInput) -> darling::Result { + 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::::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::>(); + // 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_ + } +} diff --git a/data_model/derive/src/lib.rs b/data_model/derive/src/lib.rs index 839455a33c9..a5942ba064a 100644 --- a/data_model/derive/src/lib.rs +++ b/data_model/derive/src/lib.rs @@ -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; @@ -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) +} diff --git a/data_model/derive/tests/event_matcher.rs b/data_model/derive/tests/event_matcher.rs new file mode 100644 index 00000000000..65eb1fc4252 --- /dev/null +++ b/data_model/derive/tests/event_matcher.rs @@ -0,0 +1,91 @@ +mod events { + use iroha_data_model_derive::EventMatcher; + #[derive(EventMatcher)] + pub enum TestEvent { + Event1, + Event2, + NestedEvent(AnotherEvent), + } + + pub struct AnotherEvent; +} + +use events::{AnotherEvent, TestEvent, TestEventMatcher}; + +#[test] +fn any_matcher() { + let any_matcher = TestEventMatcher::any(); + assert_eq!( + any_matcher, + TestEventMatcher::Event1 | TestEventMatcher::Event2 | TestEventMatcher::AnyNestedEvent + ); + + assert_eq!( + format!("{any_matcher:?}"), + "TestEventMatcher[Event1, Event2, AnyNestedEvent]" + ); + + assert!(any_matcher.contains(TestEventMatcher::Event1)); + assert!(any_matcher.contains(TestEventMatcher::Event2)); + assert!(any_matcher.contains(TestEventMatcher::AnyNestedEvent)); + + assert!(any_matcher.contains(TestEventMatcher::Event1 | TestEventMatcher::Event2)); + + assert!(any_matcher.matches(&TestEvent::Event1)); + assert!(any_matcher.matches(&TestEvent::Event2)); + assert!(any_matcher.matches(&TestEvent::NestedEvent(AnotherEvent))); +} + +#[test] +fn none_matcher() { + let none_matcher = TestEventMatcher::none(); + + assert_eq!(format!("{none_matcher:?}"), "TestEventMatcher[]"); + + assert!(!none_matcher.contains(TestEventMatcher::Event1)); + assert!(!none_matcher.contains(TestEventMatcher::Event2)); + assert!(!none_matcher.contains(TestEventMatcher::AnyNestedEvent)); + + assert!(!none_matcher.contains(TestEventMatcher::Event1 | TestEventMatcher::Event2)); + + assert!(!none_matcher.matches(&TestEvent::Event1)); + assert!(!none_matcher.matches(&TestEvent::Event2)); + assert!(!none_matcher.matches(&TestEvent::NestedEvent(AnotherEvent))); +} + +#[test] +fn event1_matcher() { + let event1_matcher = TestEventMatcher::Event1; + + assert_eq!(format!("{event1_matcher:?}"), "TestEventMatcher[Event1]"); + + assert!(event1_matcher.contains(TestEventMatcher::Event1)); + assert!(!event1_matcher.contains(TestEventMatcher::Event2)); + assert!(!event1_matcher.contains(TestEventMatcher::AnyNestedEvent)); + + assert!(!event1_matcher.contains(TestEventMatcher::Event1 | TestEventMatcher::Event2)); + + assert!(event1_matcher.matches(&TestEvent::Event1)); + assert!(!event1_matcher.matches(&TestEvent::Event2)); + assert!(!event1_matcher.matches(&TestEvent::NestedEvent(AnotherEvent))); +} + +#[test] +fn event1_or_2_matcher() { + let event1_or_2_matcher = TestEventMatcher::Event1 | TestEventMatcher::Event2; + + assert_eq!( + format!("{event1_or_2_matcher:?}"), + "TestEventMatcher[Event1, Event2]" + ); + + assert!(event1_or_2_matcher.contains(TestEventMatcher::Event1)); + assert!(event1_or_2_matcher.contains(TestEventMatcher::Event2)); + assert!(!event1_or_2_matcher.contains(TestEventMatcher::AnyNestedEvent)); + + assert!(event1_or_2_matcher.contains(TestEventMatcher::Event1 | TestEventMatcher::Event2)); + + assert!(event1_or_2_matcher.matches(&TestEvent::Event1)); + assert!(event1_or_2_matcher.matches(&TestEvent::Event2)); + assert!(!event1_or_2_matcher.matches(&TestEvent::NestedEvent(AnotherEvent))); +}