From ec853e968ac36e256a4f3114a85df9e81424950c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 12 Oct 2022 17:35:26 +0200 Subject: [PATCH] events: Generate structs without relation for events that can be replaced --- crates/ruma-common/src/events/audio.rs | 2 +- crates/ruma-common/src/events/emote.rs | 2 +- crates/ruma-common/src/events/file.rs | 2 +- crates/ruma-common/src/events/image.rs | 2 +- crates/ruma-common/src/events/location.rs | 2 +- crates/ruma-common/src/events/message.rs | 2 +- crates/ruma-common/src/events/notice.rs | 2 +- crates/ruma-common/src/events/room/message.rs | 6 ++ crates/ruma-common/src/events/video.rs | 2 +- crates/ruma-common/src/events/voice.rs | 2 +- .../ruma-common/tests/events/event_content.rs | 2 + crates/ruma-common/tests/events/mod.rs | 1 + .../events/ui/03-invalid-event-type.stderr | 2 +- ...1-content-without-relation-sanity-check.rs | 11 +++ .../tests/events/ui/12-no-relates_to.rs | 10 ++ .../tests/events/ui/12-no-relates_to.stderr | 7 ++ .../tests/events/without_relation.rs | 70 ++++++++++++++ .../ruma-macros/src/events/event_content.rs | 93 +++++++++++++++++++ 18 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 crates/ruma-common/tests/events/ui/11-content-without-relation-sanity-check.rs create mode 100644 crates/ruma-common/tests/events/ui/12-no-relates_to.rs create mode 100644 crates/ruma-common/tests/events/ui/12-no-relates_to.stderr create mode 100644 crates/ruma-common/tests/events/without_relation.rs diff --git a/crates/ruma-common/src/events/audio.rs b/crates/ruma-common/src/events/audio.rs index 2ff924cc..061fb4a8 100644 --- a/crates/ruma-common/src/events/audio.rs +++ b/crates/ruma-common/src/events/audio.rs @@ -36,7 +36,7 @@ use super::{ /// [`MessageType::Audio`]: super::room::message::MessageType::Audio #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The text representation of the message. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/emote.rs b/crates/ruma-common/src/events/emote.rs index 5c4e04a3..6bf0e544 100644 --- a/crates/ruma-common/src/events/emote.rs +++ b/crates/ruma-common/src/events/emote.rs @@ -25,7 +25,7 @@ use super::{ /// [`MessageType::Emote`]: super::room::message::MessageType::Emote #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The message's text content. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/file.rs b/crates/ruma-common/src/events/file.rs index 28fe6bce..0ed71001 100644 --- a/crates/ruma-common/src/events/file.rs +++ b/crates/ruma-common/src/events/file.rs @@ -34,7 +34,7 @@ use crate::{serde::Base64, OwnedMxcUri}; /// [`MessageType::File`]: super::room::message::MessageType::File #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The text representation of the message. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/image.rs b/crates/ruma-common/src/events/image.rs index 6e971c78..e21ffc18 100644 --- a/crates/ruma-common/src/events/image.rs +++ b/crates/ruma-common/src/events/image.rs @@ -31,7 +31,7 @@ use crate::OwnedMxcUri; /// [`MessageType::Image`]: super::room::message::MessageType::Image #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The text representation of the message. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/location.rs b/crates/ruma-common/src/events/location.rs index 5851ef8a..72bb0238 100644 --- a/crates/ruma-common/src/events/location.rs +++ b/crates/ruma-common/src/events/location.rs @@ -29,7 +29,7 @@ use crate::{MilliSecondsSinceUnixEpoch, PrivOwnedStr}; /// [`MessageType::Location`]: super::room::message::MessageType::Location #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The text representation of the message. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/message.rs b/crates/ruma-common/src/events/message.rs index f492330c..30b518c9 100644 --- a/crates/ruma-common/src/events/message.rs +++ b/crates/ruma-common/src/events/message.rs @@ -81,7 +81,7 @@ use super::room::message::{ /// [`MessageType::Text`]: super::room::message::MessageType::Text #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The message's text content. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/notice.rs b/crates/ruma-common/src/events/notice.rs index fad8c447..3d2fc755 100644 --- a/crates/ruma-common/src/events/notice.rs +++ b/crates/ruma-common/src/events/notice.rs @@ -25,7 +25,7 @@ use super::{ /// [`MessageType::Notice`]: super::room::message::MessageType::Notice #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The message's text content. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs index 64057ed9..20d278ed 100644 --- a/crates/ruma-common/src/events/room/message.rs +++ b/crates/ruma-common/src/events/room/message.rs @@ -457,6 +457,12 @@ impl From for RoomMessageEventContent { } } +impl From for MessageType { + fn from(content: RoomMessageEventContent) -> Self { + content.msgtype + } +} + /// Message event relationship. #[derive(Clone, Debug)] #[allow(clippy::manual_non_exhaustive)] diff --git a/crates/ruma-common/src/events/video.rs b/crates/ruma-common/src/events/video.rs index e730705b..15f976a7 100644 --- a/crates/ruma-common/src/events/video.rs +++ b/crates/ruma-common/src/events/video.rs @@ -32,7 +32,7 @@ use super::{ /// [`MessageType::Video`]: super::room::message::MessageType::Video #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The text representation of the message. #[serde(flatten)] diff --git a/crates/ruma-common/src/events/voice.rs b/crates/ruma-common/src/events/voice.rs index c395396b..57d54765 100644 --- a/crates/ruma-common/src/events/voice.rs +++ b/crates/ruma-common/src/events/voice.rs @@ -29,7 +29,7 @@ use super::{ /// [`MessageType::Audio`]: super::room::message::MessageType::Audio #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[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 { /// The text representation of the message. #[serde(flatten)] diff --git a/crates/ruma-common/tests/events/event_content.rs b/crates/ruma-common/tests/events/event_content.rs index e1a1be67..f8c4fa16 100644 --- a/crates/ruma-common/tests/events/event_content.rs +++ b/crates/ruma-common/tests/events/event_content.rs @@ -5,4 +5,6 @@ fn ui() { t.compile_fail("tests/events/ui/02-no-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/11-content-without-relation-sanity-check.rs"); + t.compile_fail("tests/events/ui/12-no-relates_to.rs"); } diff --git a/crates/ruma-common/tests/events/mod.rs b/crates/ruma-common/tests/events/mod.rs index 796312fb..49639d65 100644 --- a/crates/ruma-common/tests/events/mod.rs +++ b/crates/ruma-common/tests/events/mod.rs @@ -25,3 +25,4 @@ mod stripped; mod to_device; mod video; mod voice; +mod without_relation; diff --git a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr index 2aa3895b..d15dc467 100644 --- a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr +++ b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr @@ -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) -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 | 11 | #[ruma_event(event = "m.macro.test", kind = State)] diff --git a/crates/ruma-common/tests/events/ui/11-content-without-relation-sanity-check.rs b/crates/ruma-common/tests/events/ui/11-content-without-relation-sanity-check.rs new file mode 100644 index 00000000..9e695468 --- /dev/null +++ b/crates/ruma-common/tests/events/ui/11-content-without-relation-sanity-check.rs @@ -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, +} + +fn main() {} diff --git a/crates/ruma-common/tests/events/ui/12-no-relates_to.rs b/crates/ruma-common/tests/events/ui/12-no-relates_to.rs new file mode 100644 index 00000000..489ce5b1 --- /dev/null +++ b/crates/ruma-common/tests/events/ui/12-no-relates_to.rs @@ -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() {} diff --git a/crates/ruma-common/tests/events/ui/12-no-relates_to.stderr b/crates/ruma-common/tests/events/ui/12-no-relates_to.stderr new file mode 100644 index 00000000..baeeac6c --- /dev/null +++ b/crates/ruma-common/tests/events/ui/12-no-relates_to.stderr @@ -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) diff --git a/crates/ruma-common/tests/events/without_relation.rs b/crates/ruma-common/tests/events/without_relation.rs new file mode 100644 index 00000000..33998ad7 --- /dev/null +++ b/crates/ruma-common/tests/events/without_relation.rs @@ -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::(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); +} diff --git a/crates/ruma-macros/src/events/event_content.rs b/crates/ruma-macros/src/events/event_content.rs index 461ebfce..db27b8f9 100644 --- a/crates/ruma-macros/src/events/event_content.rs +++ b/crates/ruma-macros/src/events/event_content.rs @@ -28,6 +28,8 @@ mod kw { syn::custom_keyword!(unsigned_type); // Another type string accepted for deserialization. syn::custom_keyword!(alias); + // The content has a form without relation. + syn::custom_keyword!(without_relation); } /// Parses struct attributes for `*EventContent` derives. @@ -49,6 +51,9 @@ enum EventStructMeta { /// Variant that holds alternate event type accepted for deserialization. Alias(LitStr), + + /// This attribute signals that a form without relation should be generated. + WithoutRelation, } impl EventStructMeta { @@ -114,6 +119,9 @@ impl Parse for EventStructMeta { let _: kw::alias = input.parse()?; let _: Token![=] = input.parse()?; input.parse().map(EventStructMeta::Alias) + } else if lookahead.peek(kw::without_relation) { + let _: kw::without_relation = input.parse()?; + Ok(EventStructMeta::WithoutRelation) } else { Err(lookahead.error()) } @@ -174,6 +182,10 @@ impl MetaAttrs { fn get_aliases(&self) -> impl Iterator { 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 { @@ -320,6 +332,22 @@ pub fn expand_event_content( .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( ident, fields, @@ -340,6 +368,7 @@ pub fn expand_event_content( Ok(quote! { #redacted_event_content + #event_content_without_relation #event_content_impl #static_event_content_impl #type_aliases @@ -506,6 +535,70 @@ fn generate_redacted_event_content<'a>( }) } +fn generate_event_content_without_relation<'a>( + ident: &Ident, + fields: impl Iterator, + ruma_common: &TokenStream, +) -> syn::Result { + 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::, _>(|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::>(); + 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( event_kind: EventKind, ident: &Ident,