Add 'ruma-events-macros/' from commit '659dc963949cdb3d928d9f5d3ed9a49e1e2b6158'

git-subtree-dir: ruma-events-macros
git-subtree-mainline: 91d564dcf812196f7497fe93ea5591eab8d83d1d
git-subtree-split: 659dc963949cdb3d928d9f5d3ed9a49e1e2b6158
This commit is contained in:
Jonas Platte 2019-09-13 22:17:22 +02:00
commit 883a9566d2
9 changed files with 1442 additions and 0 deletions

3
ruma-events-macros/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

View File

@ -0,0 +1,20 @@
language: "rust"
cache: "cargo"
before_script:
- "rustup component add rustfmt"
- "rustup component add clippy"
- "cargo install --force cargo-audit"
- "cargo generate-lockfile"
script:
- "cargo audit"
- "cargo fmt --all -- --check"
- "cargo clippy --all-targets --all-features -- -D warnings"
- "cargo build --verbose"
- "cargo test --verbose"
if: "type != push OR (tag IS blank AND branch = master)"
notifications:
email: false
irc:
channels:
- secure: "HvfXk7XMbm+iDeGoNNO886q4xMOUqJncfAxqklG6FJMCVxyrf8afyyXveCxnTH1F5oDvJXw33m6YetEj1oc7RQcB3+36XkxhjC/IzmupfD9KsikGiamL9YDrfQopvY4RXqodTR3YEof7SkFkEAzuobT0QStemX6TCkC9a7BX1QpMvEbo1pS5wlswy2G2WDbiicoiS93su73AKTQ2jOmzFdwUDZdhpNnPNJqVm5TM2Am8tj6hbX6A2y2AecRZISf8rv8LhmgpZi97NjeeK4CbsQO7G4KANGr8RA7oxlgzbW2q7FbDupB6+zLT4a4/R5GjtJoi8pvaJSL9r2GYpP4VLTYF3+tJVfLbvmQVtUjhHE4masGYfnZgpgRtiH6o+DiF/ErSE/SjJEy/S8ujqXS9mjLFtSg6nLM4k4JdCr7MLrX0buNUsv5mtmhyUvYgJtd9E+ZxLHV5TG5lF28JPMrpKrEE5UvQr/xHZh+70AwCTI5jMoSPqpBwsyQ1agxTIDmiyuo60FhVUoLyiXn25m0ZIf7v1sg4A8vFq0zA9xnhpxtZATXa7StZQn1BH2k82kuyO0hkbFhEHTv25sWJdtaFy/vmrGdchxVy7ogdOXOjXkeg+0oAnOHMsRyZlVusQ4mixM/PYet860XNcW4P6P9Nz0u5ZNmagggXSKCpCqs3smY="
use_notice: true

View File

@ -0,0 +1,27 @@
[package]
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
categories = ["api-bindings", "web-programming"]
description = "A procedural macro used by the ruma-events crate."
documentation = "https://docs.rs/ruma-events-macros"
edition = "2018"
homepage = "https://github.com/ruma/ruma-events-macros"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "MIT"
name = "ruma-events-macros"
readme = "README.md"
repository = "https://github.com/ruma/ruma-api-macros"
version = "0.1.0"
[dependencies]
syn = { version = "1.0.4", features = ["full"] }
quote = "1.0.2"
proc-macro2 = "1.0.1"
[lib]
proc-macro = true
[dev-dependencies]
ruma-identifiers = "0.14.0"
serde_json = "1.0.40"
js_int = { version = "0.1.2", features = ["serde"] }
serde = { version = "1.0.99", features = ["derive"] }

View File

@ -0,0 +1,19 @@
Copyright (c) 2019 Jimmy Cuadra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,13 @@
# ruma-events-macros
[![Build Status](https://travis-ci.org/ruma/ruma-events-macros.svg?branch=master)](https://travis-ci.org/ruma/ruma-events-macros)
**ruma-events-macros** provides a procedural macro for easily generating event types for [ruma-events](https://github.com/ruma/ruma-events).
## Documentation
ruma-events-macros has [comprehensive documentation](https://docs.rs/ruma-events-macros) available on docs.rs.
## License
[MIT](http://opensource.org/licenses/MIT)

View File

@ -0,0 +1,587 @@
//! Details of generating code for the `ruma_event` procedural macro.
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use syn::{
parse::{self, Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
spanned::Spanned,
Attribute, Field, Ident, Path, Token, Type,
};
use crate::parse::{Content, EventKind, RumaEventInput};
/// The result of processing the `ruma_event` macro, ready for output back to source code.
pub struct RumaEvent {
/// Outer attributes on the field, such as a docstring.
attrs: Vec<Attribute>,
/// Information for generating the type used for the event's `content` field.
content: Content,
/// The name of the type of the event's `content` field.
content_name: Ident,
/// The variant of `ruma_events::EventType` for this event, determined by the `event_type`
/// field.
event_type: Path,
/// Struct fields of the event.
fields: Vec<Field>,
/// Whether or not the event type is `EventType::Custom`.
is_custom: bool,
/// The kind of event.
kind: EventKind,
/// The name of the event.
name: Ident,
}
impl From<RumaEventInput> for RumaEvent {
fn from(input: RumaEventInput) -> Self {
let kind = input.kind;
let name = input.name;
let content_name = Ident::new(&format!("{}Content", &name), Span::call_site());
let event_type = input.event_type;
let is_custom = is_custom_event_type(&event_type);
let mut fields = match kind {
EventKind::Event => populate_event_fields(
is_custom,
content_name.clone(),
input.fields.unwrap_or_else(Vec::new),
),
EventKind::RoomEvent => populate_room_event_fields(
is_custom,
content_name.clone(),
input.fields.unwrap_or_else(Vec::new),
),
EventKind::StateEvent => populate_state_fields(
is_custom,
content_name.clone(),
input.fields.unwrap_or_else(Vec::new),
),
};
fields.sort_unstable_by_key(|field| field.ident.clone().unwrap());
Self {
attrs: input.attrs,
content: input.content,
content_name,
event_type,
fields,
is_custom,
kind,
name,
}
}
}
impl ToTokens for RumaEvent {
// TODO: Maybe break this off into functions so it's not so large. Then remove the clippy
// allowance.
#[allow(clippy::cognitive_complexity)]
fn to_tokens(&self, tokens: &mut TokenStream) {
let attrs = &self.attrs;
let content_name = &self.content_name;
let event_fields = &self.fields;
let event_type = if self.is_custom {
quote! {
crate::EventType::Custom(self.event_type.clone())
}
} else {
let event_type = &self.event_type;
quote! {
#event_type
}
};
let name = &self.name;
let name_str = format!("{}", name);
let content_docstring = format!("The payload for `{}`.", name);
let content = match &self.content {
Content::Struct(fields) => {
quote! {
#[doc = #content_docstring]
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct #content_name {
#(#fields),*
}
}
}
Content::Typedef(typedef) => {
let content_attrs = &typedef.attrs;
let path = &typedef.path;
quote! {
#(#content_attrs)*
pub type #content_name = #path;
}
}
};
let raw_content = match &self.content {
Content::Struct(fields) => {
quote! {
#[doc = #content_docstring]
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
pub struct #content_name {
#(#fields),*
}
}
}
Content::Typedef(_) => TokenStream::new(),
};
// Custom events will already have an event_type field. All other events need to account
// for this field being manually inserted in `Serialize` impls.
let mut base_field_count: usize = if self.is_custom { 0 } else { 1 };
// Keep track of all the optional fields, because we'll need to check at runtime if they
// are `Some` in order to increase the number of fields we tell serde to serialize.
let mut optional_field_idents = Vec::with_capacity(event_fields.len());
let mut try_from_field_values: Vec<TokenStream> = Vec::with_capacity(event_fields.len());
let mut serialize_field_calls: Vec<TokenStream> = Vec::with_capacity(event_fields.len());
for field in event_fields {
let ident = field.ident.clone().unwrap();
let ident_str = if ident == "event_type" {
"type".to_string()
} else {
format!("{}", ident)
};
let span = field.span();
let try_from_field_value = if ident == "content" {
match &self.content {
Content::Struct(content_fields) => {
let mut content_field_values: Vec<TokenStream> =
Vec::with_capacity(content_fields.len());
for content_field in content_fields {
let content_field_ident = content_field.ident.clone().unwrap();
let span = content_field.span();
let token_stream = quote_spanned! {span=>
#content_field_ident: raw.content.#content_field_ident,
};
content_field_values.push(token_stream);
}
quote_spanned! {span=>
content: #content_name {
#(#content_field_values)*
},
}
}
Content::Typedef(_) => {
quote_spanned! {span=>
content: raw.content,
}
}
}
} else if ident == "prev_content" {
match &self.content {
Content::Struct(content_fields) => {
let mut content_field_values: Vec<TokenStream> =
Vec::with_capacity(content_fields.len());
for content_field in content_fields {
let content_field_ident = content_field.ident.clone().unwrap();
let span = content_field.span();
let token_stream = quote_spanned! {span=>
#content_field_ident: prev.#content_field_ident,
};
content_field_values.push(token_stream);
}
quote_spanned! {span=>
prev_content: raw.prev_content.map(|prev| {
#content_name {
#(#content_field_values)*
}
}),
}
}
Content::Typedef(_) => {
quote_spanned! {span=>
prev_content: raw.prev_content,
}
}
}
} else {
quote_spanned! {span=>
#ident: raw.#ident,
}
};
try_from_field_values.push(try_from_field_value);
// Does the same thing as #[serde(skip_serializing_if = "Option::is_none")]
let serialize_field_call = if is_option(&field.ty) {
optional_field_idents.push(ident.clone());
quote_spanned! {span=>
if self.#ident.is_some() {
state.serialize_field(#ident_str, &self.#ident)?;
}
}
} else {
base_field_count += 1;
quote_spanned! {span=>
state.serialize_field(#ident_str, &self.#ident)?;
}
};
serialize_field_calls.push(serialize_field_call);
}
let (manually_serialize_type_field, import_event_in_serialize_impl) = if self.is_custom {
(TokenStream::new(), TokenStream::new())
} else {
let manually_serialize_type_field = quote! {
state.serialize_field("type", &self.event_type())?;
};
let import_event_in_serialize_impl = quote! {
use crate::Event as _;
};
(
manually_serialize_type_field,
import_event_in_serialize_impl,
)
};
let increment_struct_len_statements: Vec<TokenStream> = optional_field_idents
.iter()
.map(|ident| {
let span = ident.span();
quote_spanned! {span=>
if self.#ident.is_some() {
len += 1;
}
}
})
.collect();
let set_up_struct_serializer = quote! {
let mut len = #base_field_count;
#(#increment_struct_len_statements)*
let mut state = serializer.serialize_struct(#name_str, len)?;
};
let impl_room_event = match self.kind {
EventKind::RoomEvent | EventKind::StateEvent => {
quote! {
impl crate::RoomEvent for #name {
/// The unique identifier for the event.
fn event_id(&self) -> &ruma_identifiers::EventId {
&self.event_id
}
/// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this event was
/// sent.
fn origin_server_ts(&self) -> js_int::UInt {
self.origin_server_ts
}
/// The unique identifier for the room associated with this event.
///
/// This can be `None` if the event came from a context where there is
/// no ambiguity which room it belongs to, like a `/sync` response for example.
fn room_id(&self) -> Option<&ruma_identifiers::RoomId> {
self.room_id.as_ref()
}
/// The unique identifier for the user who sent this event.
fn sender(&self) -> &ruma_identifiers::UserId {
&self.sender
}
/// Additional key-value pairs not signed by the homeserver.
fn unsigned(&self) -> Option<&serde_json::Value> {
self.unsigned.as_ref()
}
}
}
}
_ => TokenStream::new(),
};
let impl_state_event = if self.kind == EventKind::StateEvent {
quote! {
impl crate::StateEvent for #name {
/// The previous content for this state key, if any.
fn prev_content(&self) -> Option<&Self::Content> {
self.prev_content.as_ref()
}
/// A key that determines which piece of room state the event represents.
fn state_key(&self) -> &str {
&self.state_key
}
}
}
} else {
TokenStream::new()
};
let impl_conversions_for_content = if let Content::Struct(content_fields) = &self.content {
let mut content_field_values: Vec<TokenStream> =
Vec::with_capacity(content_fields.len());
for content_field in content_fields {
let content_field_ident = content_field.ident.clone().unwrap();
let span = content_field.span();
let token_stream = quote_spanned! {span=>
#content_field_ident: raw.#content_field_ident,
};
content_field_values.push(token_stream);
}
quote! {
impl<'de> serde::Deserialize<'de> for crate::EventResult<#content_name> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let json = serde_json::Value::deserialize(deserializer)?;
let raw: raw::#content_name = match serde_json::from_value(json.clone()) {
Ok(raw) => raw,
Err(error) => {
return Ok(crate::EventResult::Err(crate::InvalidEvent(
crate::InnerInvalidEvent::Validation {
json,
message: error.to_string(),
},
)));
}
};
Ok(crate::EventResult::Ok(#content_name {
#(#content_field_values)*
}))
}
}
}
} else {
TokenStream::new()
};
let output = quote!(
#(#attrs)*
#[derive(Clone, PartialEq, Debug)]
pub struct #name {
#(#event_fields),*
}
#content
impl<'de> serde::Deserialize<'de> for crate::EventResult<#name> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let json = serde_json::Value::deserialize(deserializer)?;
let raw: raw::#name = match serde_json::from_value(json.clone()) {
Ok(raw) => raw,
Err(error) => {
return Ok(crate::EventResult::Err(crate::InvalidEvent(
crate::InnerInvalidEvent::Validation {
json,
message: error.to_string(),
},
)));
}
};
Ok(crate::EventResult::Ok(#name {
#(#try_from_field_values)*
}))
}
}
#impl_conversions_for_content
use serde::ser::SerializeStruct as _;
impl serde::Serialize for #name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer
{
#import_event_in_serialize_impl
#set_up_struct_serializer
#(#serialize_field_calls)*
#manually_serialize_type_field
state.end()
}
}
impl crate::Event for #name {
/// The type of this event's `content` field.
type Content = #content_name;
/// The event's content.
fn content(&self) -> &Self::Content {
&self.content
}
/// The type of the event.
fn event_type(&self) -> crate::EventType {
#event_type
}
}
#impl_room_event
#impl_state_event
/// "Raw" versions of the event and its content which implement `serde::Deserialize`.
mod raw {
use super::*;
#(#attrs)*
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
pub struct #name {
#(#event_fields),*
}
#raw_content
}
);
output.to_tokens(tokens);
}
}
/// Fills in the event's struct definition with fields common to all basic events.
fn populate_event_fields(
is_custom: bool,
content_name: Ident,
mut fields: Vec<Field>,
) -> Vec<Field> {
let punctuated_fields: Punctuated<ParsableNamedField, Token![,]> = if is_custom {
parse_quote! {
/// The event's content.
pub content: #content_name,
/// The custom type of the event.
pub event_type: String,
}
} else {
parse_quote! {
/// The event's content.
pub content: #content_name,
}
};
fields.extend(punctuated_fields.into_iter().map(|p| p.field));
fields
}
/// Fills in the event's struct definition with fields common to all room events.
fn populate_room_event_fields(
is_custom: bool,
content_name: Ident,
fields: Vec<Field>,
) -> Vec<Field> {
let mut fields = populate_event_fields(is_custom, content_name, fields);
let punctuated_fields: Punctuated<ParsableNamedField, Token![,]> = parse_quote! {
/// The unique identifier for the event.
pub event_id: ruma_identifiers::EventId,
/// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this
/// event was sent.
pub origin_server_ts: js_int::UInt,
/// The unique identifier for the room associated with this event.
pub room_id: Option<ruma_identifiers::RoomId>,
/// The unique identifier for the user who sent this event.
pub sender: ruma_identifiers::UserId,
/// Additional key-value pairs not signed by the homeserver.
pub unsigned: Option<serde_json::Value>,
};
fields.extend(punctuated_fields.into_iter().map(|p| p.field));
fields
}
/// Fills in the event's struct definition with fields common to all state events.
fn populate_state_fields(is_custom: bool, content_name: Ident, fields: Vec<Field>) -> Vec<Field> {
let mut fields = populate_room_event_fields(is_custom, content_name.clone(), fields);
let punctuated_fields: Punctuated<ParsableNamedField, Token![,]> = parse_quote! {
/// The previous content for this state key, if any.
pub prev_content: Option<#content_name>,
/// A key that determines which piece of room state the event represents.
pub state_key: String,
};
fields.extend(punctuated_fields.into_iter().map(|p| p.field));
fields
}
/// Checks if the given `Path` refers to `EventType::Custom`.
fn is_custom_event_type(event_type: &Path) -> bool {
event_type.segments.last().unwrap().ident == "Custom"
}
/// Checks if a type is an `Option`.
fn is_option(ty: &Type) -> bool {
if let Type::Path(ref type_path) = ty {
type_path.path.segments.first().unwrap().ident == "Option"
} else {
panic!("struct field had unexpected non-path type");
}
}
/// A wrapper around `syn::Field` that makes it possible to parse `Punctuated<Field, Token![,]>`
/// from a `TokenStream`.
///
/// See https://github.com/dtolnay/syn/issues/651 for more context.
struct ParsableNamedField {
/// The wrapped `Field`.
pub field: Field,
}
impl Parse for ParsableNamedField {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let field = Field::parse_named(input)?;
Ok(Self { field })
}
}

View File

@ -0,0 +1,139 @@
//! Crate `ruma_events_macros` provides a procedural macro for generating
//! [ruma-events](https://github.com/ruma/ruma-events) events.
//!
//! See the documentation for the `ruma_event!` macro for usage details.
//!
#![deny(
missing_copy_implementations,
missing_debug_implementations,
// missing_docs, # Uncomment when https://github.com/rust-lang/rust/pull/60562 is released.
warnings
)]
#![warn(
clippy::empty_line_after_outer_attr,
clippy::expl_impl_clone_on_copy,
clippy::if_not_else,
clippy::items_after_statements,
clippy::match_same_arms,
clippy::mem_forget,
clippy::missing_docs_in_private_items,
clippy::multiple_inherent_impl,
clippy::mut_mut,
clippy::needless_borrow,
clippy::needless_continue,
clippy::single_match_else,
clippy::unicode_not_nfc,
clippy::use_self,
clippy::used_underscore_binding,
clippy::wrong_pub_self_convention,
clippy::wrong_self_convention
)]
#![recursion_limit = "128"]
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::ToTokens;
use crate::{gen::RumaEvent, parse::RumaEventInput};
mod gen;
mod parse;
// A note about the `example` modules that appears in doctests:
//
// This is necessary because otherwise the expanded code appears in function context, which makes
// the compiler interpret the output of the macro as a statement, and proc macros currently aren't
// allowed to expand to statements, resulting in a compiler error.
/// Generates a Rust type for a Matrix event.
///
/// # Examples
///
/// The most common form of event is a struct with all the standard fields for an event of its
/// kind and a struct for its `content` field:
///
/// ```ignore
/// # pub mod example {
/// # use ruma_events_macros::ruma_event;
/// ruma_event! {
/// /// Informs the room about what room aliases it has been given.
/// AliasesEvent {
/// kind: StateEvent,
/// event_type: RoomAliases,
/// content: {
/// /// A list of room aliases.
/// pub aliases: Vec<ruma_identifiers::RoomAliasId>,
/// }
/// }
/// }
/// # }
/// ```
///
/// Occasionally an event will have non-standard fields at its top level (outside the `content`
/// field). These extra fields are declared in block labeled with `fields`:
///
/// ```ignore
/// # pub mod example {
/// # use ruma_events_macros::ruma_event;
/// ruma_event! {
/// /// A redaction of an event.
/// RedactionEvent {
/// kind: RoomEvent,
/// event_type: RoomRedaction,
/// fields: {
/// /// The ID of the event that was redacted.
/// pub redacts: ruma_identifiers::EventId
/// },
/// content: {
/// /// The reason for the redaction, if any.
/// pub reason: Option<String>,
/// },
/// }
/// }
/// # }
/// ```
///
/// Sometimes the type of the `content` should be a type alias rather than a struct or enum. This
/// is designated with `content_type_alias`:
///
/// ```ignore
/// # pub mod example {
/// # use ruma_events_macros::ruma_event;
/// ruma_event! {
/// /// Informs the client about the rooms that are considered direct by a user.
/// DirectEvent {
/// kind: Event,
/// event_type: Direct,
/// content_type_alias: {
/// /// The payload of a `DirectEvent`.
/// ///
/// /// A mapping of `UserId`'s to a collection of `RoomId`'s which are considered
/// /// *direct* for that particular user.
/// std::collections::HashMap<ruma_identifiers::UserId, Vec<ruma_identifiers::RoomId>>
/// }
/// }
/// }
/// # }
/// ```
///
/// If `content` and `content_type_alias` are both supplied, the second one listed will overwrite
/// the first.
///
/// The event type and content type will have copies generated inside a private `raw` module. These
/// "raw" versions are the same, except they implement `serde::Deserialize`. An implementation of
/// `std::str::FromStr` (and for completeness, `std::convert::TryFrom<&str>`) will be provided,
/// which will allow the user to call `parse` on a string slice of JSON data in attempt to convert
/// into the event type. `FromStr` attempts to deserialize the type using the "raw" version. If
/// deserialization fails, an error is returned to the user. If deserialization succeeds, a value of
/// the public event type will be populated from the raw version's fields and returned. If any
/// semantic error is found after deserialization, a `serde_json::Value` of the deserialized data
/// will be returned in an `InvalidEvent`.
#[proc_macro]
pub fn ruma_event(input: TokenStream) -> TokenStream {
let ruma_event_input = syn::parse_macro_input!(input as RumaEventInput);
let ruma_event = RumaEvent::from(ruma_event_input);
ruma_event.into_token_stream().into()
}

View File

@ -0,0 +1,289 @@
//! Details of parsing input for the `ruma_event` procedural macro.
use proc_macro2::Span;
use syn::{
braced,
parse::{self, Parse, ParseStream},
punctuated::Punctuated,
token::Colon,
Attribute, Expr, Field, FieldValue, Ident, Member, Path, PathArguments, PathSegment, Token,
TypePath,
};
/// The entire `ruma_event!` macro structure directly as it appears in the source code..
pub struct RumaEventInput {
/// Outer attributes on the field, such as a docstring.
pub attrs: Vec<Attribute>,
/// The name of the event.
pub name: Ident,
/// The kind of event, determiend by the `kind` field.
pub kind: EventKind,
/// The variant of `ruma_events::EventType` for this event, determined by the `event_type`
/// field.
pub event_type: Path,
/// Additional named struct fields in the top level event struct.
pub fields: Option<Vec<Field>>,
/// A struct definition or type alias to be used as the event's `content` field.
pub content: Content,
}
impl Parse for RumaEventInput {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let name: Ident = input.parse()?;
let body;
braced!(body in input);
let mut kind = None;
let mut event_type = None;
let mut fields = None;
let mut content = None;
#[allow(clippy::identity_conversion)]
for field_value_inline_struct in
body.parse_terminated::<RumaEventField, Token![,]>(RumaEventField::parse)?
{
match field_value_inline_struct {
RumaEventField::Block(field_block) => {
let ident = match field_block.member {
Member::Named(ident) => ident,
Member::Unnamed(_) => panic!("fields with block values in `ruma_event!` must named `content_type_alias`"),
};
if ident == "content_type_alias" {
content = Some(Content::Typedef(field_block.typedef));
}
}
RumaEventField::InlineStruct(field_inline_struct) => {
let ident = match field_inline_struct.member {
Member::Named(ident) => ident,
Member::Unnamed(_) => panic!("fields with inline struct values in `ruma_event!` must be named `fields` or `content`."),
};
if ident == "fields" {
fields = Some(field_inline_struct.fields);
} else if ident == "content" {
content = Some(Content::Struct(field_inline_struct.fields));
}
}
RumaEventField::Value(field_value) => {
let ident = match field_value.member {
Member::Named(ident) => ident,
Member::Unnamed(_) => panic!("fields with expression values in `ruma_event!` must be named `kind` or `event_type`, ."),
};
if ident == "kind" {
let event_kind = match field_value.expr {
Expr::Path(expr_path) => {
if expr_path.path.is_ident("Event") {
EventKind::Event
} else if expr_path.path.is_ident("RoomEvent") {
EventKind::RoomEvent
} else if expr_path.path.is_ident("StateEvent") {
EventKind::StateEvent
} else {
panic!("value of field `kind` must be one of `Event`, `RoomEvent`, or `StateEvent`");
}
}
_ => panic!(
"value of field `kind` is required to be an ident by `ruma_event!`"
),
};
kind = Some(event_kind);
} else if ident == "event_type" {
match field_value.expr {
Expr::Path(expr_path) => {
if expr_path.path.segments.len() != 1 {
panic!("value of field `event_type` is required to be an ident by `ruma_event!`");
}
let path = expr_path.path;
let variant = path.segments.first().unwrap();
let mut punctuated = Punctuated::new();
punctuated.push(PathSegment {
ident: Ident::new("crate", Span::call_site()),
arguments: PathArguments::None,
});
punctuated.push(PathSegment {
ident: Ident::new("EventType", Span::call_site()),
arguments: PathArguments::None,
});
punctuated.push(variant.clone());
event_type = Some(Path {
leading_colon: None,
segments: punctuated,
});
}
_ => panic!(
"value of field `event_type` is required to be an ident by `ruma_event!`"
),
}
} else {
panic!("unexpected field-value pair with field name `{}`", ident);
}
}
}
}
if kind.is_none() {
panic!("field `kind` is required by `ruma_event!`");
} else if event_type.is_none() {
panic!("field `event_type` is required by `ruma_event!`");
} else if content.is_none() {
panic!(
"one field named `content` or `content_type_alias` is required by `ruma_event!`"
);
}
Ok(Self {
attrs,
name,
kind: kind.unwrap(),
event_type: event_type.unwrap(),
fields,
content: content.unwrap(),
})
}
}
/// Which kind of event is being generated.
///
/// Determined by the `kind` field in the macro body.
#[derive(PartialEq)]
pub enum EventKind {
/// A basic event.
Event,
/// A room event.
RoomEvent,
/// A state event.
StateEvent,
}
/// Information for generating the type used for the event's `content` field.
pub enum Content {
/// A struct, e.g. `ExampleEventContent { ... }`.
Struct(Vec<Field>),
/// A type alias, e.g. `type ExampleEventContent = SomeExistingType`
Typedef(Typedef),
}
/// The style of field within the macro body.
#[allow(clippy::large_enum_variant)]
enum RumaEventField {
/// The value of a field is a block with a type alias in it.
///
/// Used for `content_type_alias`.
Block(FieldBlock),
/// The value of a field is a block with named struct fields in it.
///
/// Used for `content`.
InlineStruct(FieldInlineStruct),
/// A standard named struct field.
///
/// Used for `kind` and `event_type`.
Value(FieldValue),
}
impl Parse for RumaEventField {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let ahead = input.fork();
let field_ident: Ident = ahead.parse()?;
match field_ident.to_string().as_ref() {
"content" | "fields" => {
let attrs = input.call(Attribute::parse_outer)?;
let member = input.parse()?;
let colon_token = input.parse()?;
let body;
braced!(body in input);
let fields = body
.parse_terminated::<Field, Token![,]>(Field::parse_named)?
.into_iter()
.collect();
Ok(RumaEventField::InlineStruct(FieldInlineStruct {
attrs,
member,
colon_token,
fields,
}))
}
"content_type_alias" => Ok(RumaEventField::Block(FieldBlock {
attrs: input.call(Attribute::parse_outer)?,
member: input.parse()?,
colon_token: input.parse()?,
typedef: input.parse()?,
})),
_ => Ok(RumaEventField::Value(input.parse()?)),
}
}
}
/// The value of a field is a block with a type alias in it.
///
/// Used for `content_type_alias`.
struct FieldBlock {
/// Outer attributes on the field, such as a docstring.
pub attrs: Vec<Attribute>,
/// The name of the field.
pub member: Member,
/// The colon that appears between the field name and type.
pub colon_token: Colon,
/// The path to the type that will be used in a type alias for the event's `content` type.
pub typedef: Typedef,
}
/// The value of a field is a block with named struct fields in it.
///
/// Used for `content`.
struct FieldInlineStruct {
/// Outer attributes on the field, such as a docstring.
pub attrs: Vec<Attribute>,
/// The name of the field.
pub member: Member,
/// The colon that appears between the field name and type.
pub colon_token: Colon,
/// The fields that define the `content` struct.
pub fields: Vec<Field>,
}
/// Path to a type to be used in a type alias for an event's `content` type.
pub struct Typedef {
/// Outer attributes on the field, such as a docstring.
pub attrs: Vec<Attribute>,
/// Path to the type.
pub path: TypePath,
}
impl Parse for Typedef {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let body;
braced!(body in input);
Ok(Self {
attrs: body.call(Attribute::parse_outer)?,
path: body.parse()?,
})
}
}

View File

@ -0,0 +1,345 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use serde::{
de::{Error as SerdeError, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
/// The type of an event.
#[derive(Clone, Debug, PartialEq)]
pub enum EventType {
/// m.direct
Direct,
/// m.room.aliases
RoomAliases,
/// m.room.redaction
RoomRedaction,
/// Any event that is not part of the specification.
Custom(String),
}
impl Display for EventType {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let event_type_str = match *self {
EventType::Direct => "m.direct",
EventType::RoomAliases => "m.room.aliases",
EventType::RoomRedaction => "m.room.redaction",
EventType::Custom(ref event_type) => event_type,
};
write!(f, "{}", event_type_str)
}
}
impl<'a> From<&'a str> for EventType {
fn from(s: &'a str) -> EventType {
match s {
"m.direct" => EventType::Direct,
"m.room.aliases" => EventType::RoomAliases,
"m.room.redaction" => EventType::RoomRedaction,
event_type => EventType::Custom(event_type.to_string()),
}
}
}
impl Serialize for EventType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for EventType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct EventTypeVisitor;
impl<'de> Visitor<'de> for EventTypeVisitor {
type Value = EventType;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
write!(formatter, "a Matrix event type as a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: SerdeError,
{
Ok(EventType::from(v))
}
}
deserializer.deserialize_str(EventTypeVisitor)
}
}
/// The result of deserializing an event, which may or may not be valid.
#[derive(Debug)]
pub enum EventResult<T> {
/// `T` deserialized and validated successfully.
Ok(T),
/// `T` deserialized but was invalid.
///
/// `InvalidEvent` contains the original input.
Err(InvalidEvent),
}
impl<T> EventResult<T> {
/// Convert `EventResult<T>` into the equivalent `std::result::Result<T, InvalidEvent>`.
pub fn into_result(self) -> Result<T, InvalidEvent> {
match self {
EventResult::Ok(t) => Ok(t),
EventResult::Err(invalid_event) => Err(invalid_event),
}
}
}
/// A basic event.
pub trait Event
where
Self: Debug + Serialize,
{
/// The type of this event's `content` field.
type Content: Debug + Serialize;
/// The event's content.
fn content(&self) -> &Self::Content;
/// The type of the event.
fn event_type(&self) -> EventType;
}
/// An event within the context of a room.
pub trait RoomEvent: Event {
/// The unique identifier for the event.
fn event_id(&self) -> &ruma_identifiers::EventId;
/// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this event was
/// sent.
fn origin_server_ts(&self) -> js_int::UInt;
/// The unique identifier for the room associated with this event.
///
/// This can be `None` if the event came from a context where there is
/// no ambiguity which room it belongs to, like a `/sync` response for example.
fn room_id(&self) -> Option<&ruma_identifiers::RoomId>;
/// The unique identifier for the user who sent this event.
fn sender(&self) -> &ruma_identifiers::UserId;
/// Additional key-value pairs not signed by the homeserver.
fn unsigned(&self) -> Option<&serde_json::Value>;
}
/// An event that describes persistent state about a room.
pub trait StateEvent: RoomEvent {
/// The previous content for this state key, if any.
fn prev_content(&self) -> Option<&Self::Content>;
/// A key that determines which piece of room state the event represents.
fn state_key(&self) -> &str;
}
/// An event that is malformed or otherwise invalid.
///
/// When attempting to create an event from a string of JSON data, an error in the input data may
/// cause deserialization to fail, or the JSON structure may not corresponded to ruma-events's
/// strict definition of the event's schema. If deserialization completely fails, this type will
/// provide a message with details about the deserialization error. If deserialization succeeds but
/// the event is otherwise invalid, a similar message will be provided, as well as a
/// `serde_json::Value` containing the raw JSON data as it was deserialized.
#[derive(Debug)]
pub struct InvalidEvent(InnerInvalidEvent);
/// An event that is malformed or otherwise invalid.
#[derive(Debug)]
enum InnerInvalidEvent {
/// An event that deserialized but failed validation.
Validation {
/// The raw `serde_json::Value` representation of the invalid event.
json: serde_json::Value,
/// An message describing why the event was invalid.
message: String,
},
}
// See note about wrapping macro expansion in a module from `src/lib.rs`
pub mod common_case {
use std::convert::TryFrom;
use js_int::UInt;
use ruma_events_macros::ruma_event;
use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId};
use serde_json::Value;
use super::EventResult;
ruma_event! {
/// Informs the room about what room aliases it has been given.
AliasesEvent {
kind: StateEvent,
event_type: RoomAliases,
content: {
/// A list of room aliases.
pub aliases: Vec<ruma_identifiers::RoomAliasId>,
}
}
}
#[test]
fn serialization_with_optional_fields_as_none() {
let event = AliasesEvent {
content: AliasesEventContent {
aliases: Vec::with_capacity(0),
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UInt::try_from(1).unwrap(),
prev_content: None,
room_id: None,
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "example.com".to_string(),
unsigned: None,
};
let actual = serde_json::to_string(&event).unwrap();
let expected = r#"{"content":{"aliases":[]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"sender":"@carl:example.com","state_key":"example.com","type":"m.room.aliases"}"#;
assert_eq!(actual, expected);
}
#[test]
fn serialization_with_some_optional_fields_as_some() {
let event = AliasesEvent {
content: AliasesEventContent {
aliases: vec![RoomAliasId::try_from("#room:example.org").unwrap()],
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UInt::try_from(1).unwrap(),
prev_content: Some(AliasesEventContent {
aliases: Vec::with_capacity(0),
}),
room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "example.com".to_string(),
unsigned: None,
};
let actual = serde_json::to_string(&event).unwrap();
let expected = r##"{"content":{"aliases":["#room:example.org"]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"prev_content":{"aliases":[]},"room_id":"!n8f893n9:example.com","sender":"@carl:example.com","state_key":"example.com","type":"m.room.aliases"}"##;
assert_eq!(actual, expected);
}
#[test]
fn serialization_with_all_optional_fields_as_some() {
let event = AliasesEvent {
content: AliasesEventContent {
aliases: vec![RoomAliasId::try_from("#room:example.org").unwrap()],
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UInt::try_from(1).unwrap(),
prev_content: Some(AliasesEventContent {
aliases: Vec::with_capacity(0),
}),
room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "example.com".to_string(),
unsigned: Some(serde_json::from_str::<Value>(r#"{"foo":"bar"}"#).unwrap()),
};
let actual = serde_json::to_string(&event).unwrap();
let expected = r##"{"content":{"aliases":["#room:example.org"]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"prev_content":{"aliases":[]},"room_id":"!n8f893n9:example.com","sender":"@carl:example.com","state_key":"example.com","unsigned":{"foo":"bar"},"type":"m.room.aliases"}"##;
assert_eq!(actual, expected);
}
#[test]
fn deserialization() {
let json = r##"{"content":{"aliases":["#room:example.org"]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"prev_content":{"aliases":[]},"room_id":"!n8f893n9:example.com","sender":"@carl:example.com","state_key":"example.com","unsigned":{"foo":"bar"},"type":"m.room.aliases"}"##;
let event_result: EventResult<AliasesEvent> = serde_json::from_str(json).unwrap();
let actual: AliasesEvent = event_result.into_result().unwrap();
let expected = AliasesEvent {
content: AliasesEventContent {
aliases: vec![RoomAliasId::try_from("#room:example.org").unwrap()],
},
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: UInt::try_from(1).unwrap(),
prev_content: Some(AliasesEventContent {
aliases: Vec::with_capacity(0),
}),
room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()),
sender: UserId::try_from("@carl:example.com").unwrap(),
state_key: "example.com".to_string(),
unsigned: Some(serde_json::from_str::<Value>(r#"{"foo":"bar"}"#).unwrap()),
};
assert_eq!(actual, expected);
}
}
pub mod custom_event_type {
use ruma_events_macros::ruma_event;
use serde_json::Value;
ruma_event! {
/// A custom event.
CustomEvent {
kind: Event,
event_type: Custom,
content_type_alias: {
/// The payload for `CustomEvent`.
Value
},
}
}
}
pub mod extra_fields {
use ruma_events_macros::ruma_event;
ruma_event! {
/// A redaction of an event.
RedactionEvent {
kind: RoomEvent,
event_type: RoomRedaction,
fields: {
/// The ID of the event that was redacted.
pub redacts: ruma_identifiers::EventId
},
content: {
/// The reason for the redaction, if any.
pub reason: Option<String>,
},
}
}
}
pub mod type_alias {
use ruma_events_macros::ruma_event;
ruma_event! {
/// Informs the client about the rooms that are considered direct by a user.
DirectEvent {
kind: Event,
event_type: Direct,
content_type_alias: {
/// The payload of a `DirectEvent`.
///
/// A mapping of `UserId`'s to a collection of `RoomId`'s which are considered
/// *direct* for that particular user.
std::collections::HashMap<ruma_identifiers::UserId, Vec<ruma_identifiers::RoomId>>
}
}
}
}