Skip to content

Instantly share code, notes, and snippets.

@jplatte
Last active October 11, 2019 17:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jplatte/976f5a17e709263d58270257ea10e5c4 to your computer and use it in GitHub Desktop.
Save jplatte/976f5a17e709263d58270257ea10e5c4 to your computer and use it in GitHub Desktop.
ruma-events notes

ruma-events

Overview

  • There is a hierarchy of event traits: Event > RoomEvent > StateEvent
  • There is a Rust type for each kind of event (each allowed value for the 'type' field of the json representation), including custom [room / state] events
  • There is also a content type for each event type
  • In addition, there are "special" event types:
    • the Event, RoomEvent and StateEvent enums from collections::all contain every event type event / room event type / state event type respectively
      • => RoomEvent contains all the same variants as StateEvent, and more
      • => Event contains all the same variants as RoomEvent, and more
    • collections::only::Event contains every event type that is not a room events
    • collections::only::RoomEvent contains every room event type that is not a state event
    • StrippedStateContent is a generic wrapper for state event content types that has a smaller set of the common state-event fields
      • it is actually an event type rather than an event content type though, contrary to what the name suggests. I only started thinking about that when writing this though, no idea why it's named like this. Potentially, it's because the following enum already took the obvious name:
    • StrippedState is an equivalent to collections::all::StateEvent where each variant is wrapped in StrippedStateContent

ruma-events 0.13 & 0.14

In ruma-client, we had the issue that some endpoint responses that returned lists of events would fail deserialization if a single event contained would fail deserialization. It was a massive pain in the ass for a long time (both concerning usability in general and concerning figuring out the root cause of the deserialization fails). Thus we figured we need to allow deserialization of event lists to succeed even if individual events fail deserialization.

ruma-events 0.13 and 0.14 do this by removing Deserialize impls, and adding FromStr impls instead, and also, along the way, added event validation, in order to enforce additional constraints the spec puts on events that are not represented in the types of the deserialized fields. In order to implement event validation, each event type and event content type gained an additional raw version of itself (under a private raw submodule). This also helped keeping the FromStr implementations small, because the raw types, due to being private, could just derive Deserialize.

When trying to port ruma-client-api to ruma-events 0.14, I discovered that it wasn't really possible. We couldn't derive Deserialize for the endpoint response types anymore, which contained event lists indirectly. So a new solution had to be found.

EventResult API

Somewhat recently, someone (I don't remember who) had the idea of changing the lists of event types to lists of something akin to Result<EventT, serde_json::Value>. (I discovered later that that exact thing would actually work, but wouldn't keep the error message in case the event type's deserialization logic would fail. Since serde only supports single-pass deserialization, this fallback works by first deserializing to a generic value type that can represent anything serde can deserialize, then trying to deserialize that to the Ok type, and if that fails, the Err type.

We wanted to do the same thing with error message preservation and also validation on top, so we created EventResult. Since everything else is tied to json currently and serde's value type is private, we decided to implement this by first deserializing to json and trying to deserialize to the event type afterwards. The fallback doesn't have to do any more deserialization work then, because we already have a json value.

Validation in EventResult until very recently was based on TryInto (we wouldn't actually try deserializing the json value to the final event type but its raw form, trying to convert via TryInto afterwards). However, that lead to coherence issues with the generic StrippedStateContent, which I solved by getting rid of the TryInto and instead requiring an fn try_from_raw for each event type.

My work doesn't touch the Event trait, but rather has all the abstractions required for EventResult deserialization to work in a separate trait EventResultCompatible. This is because a bunch of types (all of the event content types + StrippedEventContent) don't actually implement Event, but need to be deserializable as EventResult<T>. Because many EventResultCompatible don't require validation, try_from_raw has a generic error type (to allow implementations where the compiler knows they're actually infallible).

Note to self: (I just realized this): the custom Void type used for infallible conversion could most likely be replaced by std::convert::Infallible; potentially we could even have a blanket impl for try_from_raw (which would then again need to be in a separate trait I think) for types with a From<RawT> impl.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment