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:
commit
883a9566d2
3
ruma-events-macros/.gitignore
vendored
Normal file
3
ruma-events-macros/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
20
ruma-events-macros/.travis.yml
Normal file
20
ruma-events-macros/.travis.yml
Normal 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
|
27
ruma-events-macros/Cargo.toml
Normal file
27
ruma-events-macros/Cargo.toml
Normal 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"] }
|
19
ruma-events-macros/LICENSE
Normal file
19
ruma-events-macros/LICENSE
Normal 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.
|
13
ruma-events-macros/README.md
Normal file
13
ruma-events-macros/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# ruma-events-macros
|
||||
|
||||
[](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)
|
587
ruma-events-macros/src/gen.rs
Normal file
587
ruma-events-macros/src/gen.rs
Normal 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 })
|
||||
}
|
||||
}
|
139
ruma-events-macros/src/lib.rs
Normal file
139
ruma-events-macros/src/lib.rs
Normal 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()
|
||||
}
|
289
ruma-events-macros/src/parse.rs
Normal file
289
ruma-events-macros/src/parse.rs
Normal 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()?,
|
||||
})
|
||||
}
|
||||
}
|
345
ruma-events-macros/tests/ruma_events_macros.rs
Normal file
345
ruma-events-macros/tests/ruma_events_macros.rs
Normal 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>>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user