Skip to content

Commit

Permalink
abci: add TypedEvent conversion trait
Browse files Browse the repository at this point in the history
This commit adds a marker trait for types that can be converted to and from
[`Event`]s.  It doesn't make any assumptions about how the conversion is
performed, but it does allow downstream users to declare a single source of
truth about how event data is structured.
  • Loading branch information
hdevalence committed Mar 30, 2023
1 parent 12e83a7 commit c27244f
Showing 1 changed file with 155 additions and 2 deletions.
157 changes: 155 additions & 2 deletions tendermint/src/abci/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@ impl Event {
///
/// let event = Event::new(
/// "app",
/// vec![
/// [
/// ("key1", "value1").index(),
/// ("key2", "value2").index(),
/// ("key3", "value3").no_index(), // will not be indexed
/// ],
/// );
/// ```
// XXX(hdevalence): remove vec! from example after https://github.com/rust-lang/rust/pull/65819
pub fn new<K, I>(kind: K, attributes: I) -> Self
where
K: Into<String>,
Expand All @@ -53,6 +52,49 @@ impl Event {
attributes: attributes.into_iter().map(Into::into).collect(),
}
}

/// Checks whether `&self` is equal to `other`, ignoring the `index` field on any attributes.
pub fn eq_ignoring_index(&self, other: &Self) -> bool {
self.kind == other.kind
// IMPORTANT! We need to check the lengths before calling zip,
// in order to not drop any attributes.
&& self.attributes.len() == other.attributes.len()
&& self
.attributes
.iter()
.zip(other.attributes.iter())
.all(|(a, b)| a.eq_ignoring_index(b))
}
}

/// A marker trait for types that can be converted to and from [`Event`]s.
///
/// This trait doesn't make any assumptions about how the conversion is
/// performed, or how the type's data is encoded in event attributes. Instead,
/// it just declares the conversion methods used to serialize the type to an
/// [`Event`] and to deserialize it from an [`Event`], allowing downstream users
/// to declare a single source of truth about how event data is structured.
///
/// # Contract
///
/// If `T: TypedEvent`, then:
///
/// - `T::try_from(e) == Ok(t)` for all `t: T, e: Event` where `Event::from(t).eq_ignoring_index(e)
/// == true`.
/// - `Event::from(T::try_from(e).unwrap()).eq_ignoring_index(e) == true` for all `e: Event` where
/// `T::try_from(e)` returns `Ok(_)`.
///
/// In other words, the conversion methods should round-trip on the attributes,
/// but are not required to preserve the (nondeterministic) index information.
pub trait TypedEvent
where
Self: TryFrom<Event>,
Event: From<Self>,
{
/// Convenience wrapper around `Into::into` that doesn't require type inference.
fn into_event(self) -> Event {
self.into()
}
}

/// A key-value pair describing an [`Event`].
Expand All @@ -73,6 +115,13 @@ pub struct EventAttribute {
pub index: bool,
}

impl EventAttribute {
/// Checks whether `&self` is equal to `other`, ignoring the `index` field.
pub fn eq_ignoring_index(&self, other: &Self) -> bool {
self.key == other.key && self.value == other.value
}
}

impl<K: Into<String>, V: Into<String>> From<(K, V, bool)> for EventAttribute {
fn from((key, value, index): (K, V, bool)) -> Self {
EventAttribute {
Expand Down Expand Up @@ -250,3 +299,107 @@ mod v0_37 {

impl Protobuf<pb::Event> for Event {}
}

#[cfg(test)]
mod tests {
use serde::Deserialize;

use super::*;

#[test]
fn event_eq_ignoring_index_ignores_index() {
let event_a = Event::new("test", [("foo", "bar").index()]);
let event_b = Event::new("test", [("foo", "bar").no_index()]);
let event_c = Event::new("test", [("foo", "baz").index()]);

assert_eq!(event_a.eq_ignoring_index(&event_b), true);
assert_eq!(event_a.eq_ignoring_index(&event_c), false);
}

#[test]
fn exercise_typed_event() {
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
struct Payload {
x: u32,
y: u32,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct MyEvent {
a: Payload,
b: Payload,
}

impl From<MyEvent> for Event {
fn from(event: MyEvent) -> Self {
Event::new(
"my_event",
vec![
("a", serde_json::to_string(&event.a).unwrap()).index(),
("b", serde_json::to_string(&event.b).unwrap()).index(),
],
)
}
}

impl TryFrom<Event> for MyEvent {
type Error = (); // Avoid depending on a specific error library in test code

fn try_from(event: Event) -> Result<Self, Self::Error> {
if event.kind != "my_event" {
return Err(());
}

let a = event
.attributes
.iter()
.find(|attr| attr.key == "a")
.ok_or(())
.and_then(|attr| serde_json::from_str(&attr.value).map_err(|_| ()))?;
let b = event
.attributes
.iter()
.find(|attr| attr.key == "b")
.ok_or(())
.and_then(|attr| serde_json::from_str(&attr.value).map_err(|_| ()))?;

Ok(MyEvent { a, b })
}
}

impl TypedEvent for MyEvent {}

let t = MyEvent {
a: Payload { x: 1, y: 2 },
b: Payload { x: 3, y: 4 },
};

let e1 = Event::from(t.clone());
// e2 is like e1 but with different indexing.
let e2 = {
let mut e = e1.clone();
e.attributes[0].index = false;
e.attributes[1].index = false;
e
};

// Contract:

// - `T::try_from(e) == Ok(t)` for all `t: T, e: Event` where
// `Event::from(t).eq_ignoring_index(e) == true`.
assert_eq!(e1.eq_ignoring_index(&e2), true);
assert_eq!(MyEvent::try_from(e1.clone()), Ok(t.clone()));
assert_eq!(MyEvent::try_from(e2.clone()), Ok(t.clone()));

// - `Event::from(T::try_from(e).unwrap()).eq_ignoring_index(e) == true` for all `e: Event`
// where `T::try_from(e)` returns `Ok(_)`.
assert_eq!(
Event::from(MyEvent::try_from(e1.clone()).unwrap()).eq_ignoring_index(&e1),
true
);
assert_eq!(
Event::from(MyEvent::try_from(e2.clone()).unwrap()).eq_ignoring_index(&e2),
true
);
}
}

0 comments on commit c27244f

Please sign in to comment.