events: Generate structs without relation for events that can be replaced

This commit is contained in:
Kévin Commaille 2022-10-12 17:35:26 +02:00 committed by Kévin Commaille
parent 6ec7fc09ea
commit ec853e968a
18 changed files with 210 additions and 10 deletions

View File

@ -36,7 +36,7 @@ use super::{
/// [`MessageType::Audio`]: super::room::message::MessageType::Audio /// [`MessageType::Audio`]: super::room::message::MessageType::Audio
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.audio", kind = MessageLike)] #[ruma_event(type = "m.audio", kind = MessageLike, without_relation)]
pub struct AudioEventContent { pub struct AudioEventContent {
/// The text representation of the message. /// The text representation of the message.
#[serde(flatten)] #[serde(flatten)]

View File

@ -25,7 +25,7 @@ use super::{
/// [`MessageType::Emote`]: super::room::message::MessageType::Emote /// [`MessageType::Emote`]: super::room::message::MessageType::Emote
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.emote", kind = MessageLike)] #[ruma_event(type = "m.emote", kind = MessageLike, without_relation)]
pub struct EmoteEventContent { pub struct EmoteEventContent {
/// The message's text content. /// The message's text content.
#[serde(flatten)] #[serde(flatten)]

View File

@ -34,7 +34,7 @@ use crate::{serde::Base64, OwnedMxcUri};
/// [`MessageType::File`]: super::room::message::MessageType::File /// [`MessageType::File`]: super::room::message::MessageType::File
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.file", kind = MessageLike)] #[ruma_event(type = "m.file", kind = MessageLike, without_relation)]
pub struct FileEventContent { pub struct FileEventContent {
/// The text representation of the message. /// The text representation of the message.
#[serde(flatten)] #[serde(flatten)]

View File

@ -31,7 +31,7 @@ use crate::OwnedMxcUri;
/// [`MessageType::Image`]: super::room::message::MessageType::Image /// [`MessageType::Image`]: super::room::message::MessageType::Image
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.image", kind = MessageLike)] #[ruma_event(type = "m.image", kind = MessageLike, without_relation)]
pub struct ImageEventContent { pub struct ImageEventContent {
/// The text representation of the message. /// The text representation of the message.
#[serde(flatten)] #[serde(flatten)]

View File

@ -29,7 +29,7 @@ use crate::{MilliSecondsSinceUnixEpoch, PrivOwnedStr};
/// [`MessageType::Location`]: super::room::message::MessageType::Location /// [`MessageType::Location`]: super::room::message::MessageType::Location
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.location", kind = MessageLike)] #[ruma_event(type = "m.location", kind = MessageLike, without_relation)]
pub struct LocationEventContent { pub struct LocationEventContent {
/// The text representation of the message. /// The text representation of the message.
#[serde(flatten)] #[serde(flatten)]

View File

@ -81,7 +81,7 @@ use super::room::message::{
/// [`MessageType::Text`]: super::room::message::MessageType::Text /// [`MessageType::Text`]: super::room::message::MessageType::Text
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.message", kind = MessageLike)] #[ruma_event(type = "m.message", kind = MessageLike, without_relation)]
pub struct MessageEventContent { pub struct MessageEventContent {
/// The message's text content. /// The message's text content.
#[serde(flatten)] #[serde(flatten)]

View File

@ -25,7 +25,7 @@ use super::{
/// [`MessageType::Notice`]: super::room::message::MessageType::Notice /// [`MessageType::Notice`]: super::room::message::MessageType::Notice
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.notice", kind = MessageLike)] #[ruma_event(type = "m.notice", kind = MessageLike, without_relation)]
pub struct NoticeEventContent { pub struct NoticeEventContent {
/// The message's text content. /// The message's text content.
#[serde(flatten)] #[serde(flatten)]

View File

@ -457,6 +457,12 @@ impl From<MessageType> for RoomMessageEventContent {
} }
} }
impl From<RoomMessageEventContent> for MessageType {
fn from(content: RoomMessageEventContent) -> Self {
content.msgtype
}
}
/// Message event relationship. /// Message event relationship.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(clippy::manual_non_exhaustive)] #[allow(clippy::manual_non_exhaustive)]

View File

@ -32,7 +32,7 @@ use super::{
/// [`MessageType::Video`]: super::room::message::MessageType::Video /// [`MessageType::Video`]: super::room::message::MessageType::Video
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.video", kind = MessageLike)] #[ruma_event(type = "m.video", kind = MessageLike, without_relation)]
pub struct VideoEventContent { pub struct VideoEventContent {
/// The text representation of the message. /// The text representation of the message.
#[serde(flatten)] #[serde(flatten)]

View File

@ -29,7 +29,7 @@ use super::{
/// [`MessageType::Audio`]: super::room::message::MessageType::Audio /// [`MessageType::Audio`]: super::room::message::MessageType::Audio
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.voice", kind = MessageLike)] #[ruma_event(type = "m.voice", kind = MessageLike, without_relation)]
pub struct VoiceEventContent { pub struct VoiceEventContent {
/// The text representation of the message. /// The text representation of the message.
#[serde(flatten)] #[serde(flatten)]

View File

@ -5,4 +5,6 @@ fn ui() {
t.compile_fail("tests/events/ui/02-no-event-type.rs"); t.compile_fail("tests/events/ui/02-no-event-type.rs");
t.compile_fail("tests/events/ui/03-invalid-event-type.rs"); t.compile_fail("tests/events/ui/03-invalid-event-type.rs");
t.pass("tests/events/ui/10-content-wildcard.rs"); t.pass("tests/events/ui/10-content-wildcard.rs");
t.pass("tests/events/ui/11-content-without-relation-sanity-check.rs");
t.compile_fail("tests/events/ui/12-no-relates_to.rs");
} }

View File

@ -25,3 +25,4 @@ mod stripped;
mod to_device; mod to_device;
mod video; mod video;
mod voice; mod voice;
mod without_relation;

View File

@ -6,7 +6,7 @@ error: no event type attribute found, add `#[ruma_event(type = "any.room.event",
| |
= note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info) = note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected one of: `type`, `kind`, `custom_redacted`, `state_key_type`, `unsigned_type`, `alias` error: expected one of: `type`, `kind`, `custom_redacted`, `state_key_type`, `unsigned_type`, `alias`, `without_relation`
--> tests/events/ui/03-invalid-event-type.rs:11:14 --> tests/events/ui/03-invalid-event-type.rs:11:14
| |
11 | #[ruma_event(event = "m.macro.test", kind = State)] 11 | #[ruma_event(event = "m.macro.test", kind = State)]

View File

@ -0,0 +1,11 @@
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.macro.test", kind = MessageLike, without_relation)]
pub struct MacroTestContent {
pub url: String,
pub relates_to: Option<String>,
}
fn main() {}

View File

@ -0,0 +1,10 @@
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.macro.test", kind = MessageLike, without_relation)]
pub struct MacroTestContent {
pub url: String,
}
fn main() {}

View File

@ -0,0 +1,7 @@
error: `without_relation` can only be used on events with a `relates_to` field
--> tests/events/ui/12-no-relates_to.rs:4:48
|
4 | #[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
| ^^^^^^^^^^^^
|
= note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -0,0 +1,70 @@
use assert_matches::assert_matches;
use ruma_common::{
event_id,
events::room::message::{
InReplyTo, MessageType, Relation, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
},
};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[test]
fn serialize_room_message_content_without_relation() {
let mut content = RoomMessageEventContent::text_plain("Hello, world!");
content.relates_to =
Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$eventId").to_owned()) });
let without_relation = RoomMessageEventContentWithoutRelation::from(content);
#[cfg(not(feature = "unstable-msc3246"))]
assert_eq!(
to_json_value(&without_relation).unwrap(),
json!({
"body": "Hello, world!",
"msgtype": "m.text",
})
);
#[cfg(feature = "unstable-msc3246")]
assert_eq!(
to_json_value(&without_relation).unwrap(),
json!({
"body": "Hello, world!",
"msgtype": "m.text",
"org.matrix.msc1767.text": "Hello, world!",
})
);
}
#[test]
fn deserialize_room_message_content_without_relation() {
let json_data = json!({
"body": "Hello, world!",
"msgtype": "m.text",
});
let text = assert_matches!(
from_json_value::<RoomMessageEventContentWithoutRelation>(json_data),
Ok(RoomMessageEventContentWithoutRelation::Text(text)) => text
);
assert_eq!(text.body, "Hello, world!");
}
#[test]
fn convert_room_message_content_without_relation_to_full() {
let mut content = RoomMessageEventContent::text_plain("Hello, world!");
content.relates_to =
Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id!("$eventId").to_owned()) });
let new_content =
RoomMessageEventContent::from(RoomMessageEventContentWithoutRelation::from(content));
let (text, relates_to) = assert_matches!(
new_content,
RoomMessageEventContent {
msgtype: MessageType::Text(text),
relates_to,
..
} => (text, relates_to)
);
assert_eq!(text.body, "Hello, world!");
assert_matches!(relates_to, None);
}

View File

@ -28,6 +28,8 @@ mod kw {
syn::custom_keyword!(unsigned_type); syn::custom_keyword!(unsigned_type);
// Another type string accepted for deserialization. // Another type string accepted for deserialization.
syn::custom_keyword!(alias); syn::custom_keyword!(alias);
// The content has a form without relation.
syn::custom_keyword!(without_relation);
} }
/// Parses struct attributes for `*EventContent` derives. /// Parses struct attributes for `*EventContent` derives.
@ -49,6 +51,9 @@ enum EventStructMeta {
/// Variant that holds alternate event type accepted for deserialization. /// Variant that holds alternate event type accepted for deserialization.
Alias(LitStr), Alias(LitStr),
/// This attribute signals that a form without relation should be generated.
WithoutRelation,
} }
impl EventStructMeta { impl EventStructMeta {
@ -114,6 +119,9 @@ impl Parse for EventStructMeta {
let _: kw::alias = input.parse()?; let _: kw::alias = input.parse()?;
let _: Token![=] = input.parse()?; let _: Token![=] = input.parse()?;
input.parse().map(EventStructMeta::Alias) input.parse().map(EventStructMeta::Alias)
} else if lookahead.peek(kw::without_relation) {
let _: kw::without_relation = input.parse()?;
Ok(EventStructMeta::WithoutRelation)
} else { } else {
Err(lookahead.error()) Err(lookahead.error())
} }
@ -174,6 +182,10 @@ impl MetaAttrs {
fn get_aliases(&self) -> impl Iterator<Item = &LitStr> { fn get_aliases(&self) -> impl Iterator<Item = &LitStr> {
self.0.iter().filter_map(|a| a.get_alias()) self.0.iter().filter_map(|a| a.get_alias())
} }
fn has_without_relation(&self) -> bool {
self.0.iter().any(|a| matches!(*a, EventStructMeta::WithoutRelation))
}
} }
impl Parse for MetaAttrs { impl Parse for MetaAttrs {
@ -320,6 +332,22 @@ pub fn expand_event_content(
.unwrap_or_else(syn::Error::into_compile_error) .unwrap_or_else(syn::Error::into_compile_error)
}); });
let without_relations: Vec<_> =
content_attr.iter().filter(|attrs| attrs.has_without_relation()).collect();
let event_content_without_relation = match without_relations.as_slice() {
[] => None,
[_] => Some(
generate_event_content_without_relation(ident, fields.clone(), ruma_common)
.unwrap_or_else(syn::Error::into_compile_error),
),
_ => {
return Err(syn::Error::new(
Span::call_site(),
"multiple without_relation attributes found, there can only be one",
))
}
};
let event_content_impl = generate_event_content_impl( let event_content_impl = generate_event_content_impl(
ident, ident,
fields, fields,
@ -340,6 +368,7 @@ pub fn expand_event_content(
Ok(quote! { Ok(quote! {
#redacted_event_content #redacted_event_content
#event_content_without_relation
#event_content_impl #event_content_impl
#static_event_content_impl #static_event_content_impl
#type_aliases #type_aliases
@ -506,6 +535,70 @@ fn generate_redacted_event_content<'a>(
}) })
} }
fn generate_event_content_without_relation<'a>(
ident: &Ident,
fields: impl Iterator<Item = &'a Field>,
ruma_common: &TokenStream,
) -> syn::Result<TokenStream> {
let serde = quote! { #ruma_common::exports::serde };
let type_doc = format!(
"Form of [`{ident}`] without relation.\n\n\
To construct this type, construct a [`{ident}`] and then use one of its `::from() / .into()` methods."
);
let without_relation_ident = format_ident!("{ident}WithoutRelation");
let with_relation_fn_doc =
format!("Transform `self` into a [`{ident}`] with the given relation.");
let (relates_to, other_fields) = fields.partition::<Vec<_>, _>(|f| {
f.ident.as_ref().filter(|ident| *ident == "relates_to").is_some()
});
let relates_to_type = relates_to.into_iter().next().map(|f| &f.ty).ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"`without_relation` can only be used on events with a `relates_to` field",
)
})?;
let without_relation_fields = other_fields.iter().flat_map(|f| &f.ident).collect::<Vec<_>>();
let without_relation_struct = if other_fields.is_empty() {
quote! { ; }
} else {
quote! {
{ #( #other_fields, )* }
}
};
Ok(quote! {
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::std::convert::From<#ident> for #without_relation_ident {
fn from(c: #ident) -> Self {
Self {
#( #without_relation_fields: c.#without_relation_fields, )*
}
}
}
#[doc = #type_doc]
#[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct #without_relation_ident #without_relation_struct
impl #without_relation_ident {
#[doc = #with_relation_fn_doc]
pub fn with_relation(self, relates_to: #relates_to_type) -> #ident {
#ident {
#( #without_relation_fields: self.#without_relation_fields, )*
relates_to,
}
}
}
})
}
fn generate_event_type_aliases( fn generate_event_type_aliases(
event_kind: EventKind, event_kind: EventKind,
ident: &Ident, ident: &Ident,