use std::borrow::Cow;
use as_variant::as_variant;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::parse_quote;
mod parse;
use super::common::{
CommonEventKind, EventContentTraitVariation, EventType, EventTypes, EventVariation,
};
use crate::util::{
PrivateField, RumaCommon, RumaEvents, RumaEventsReexport, SerdeMetaItem, StructFieldExt,
TypeExt,
};
pub(crate) fn expand_event_content(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let event_content = EventContent::parse(input)?;
let redacted_event_content = event_content.expand_redacted_event_content();
let possibly_redacted_event_content = event_content.expand_possibly_redacted_event_content();
let event_content_without_relation = event_content.expand_event_content_without_relation();
let event_content_impl = event_content.expand_event_content_impl(
EventContentVariation::Original,
&event_content.ident,
event_content.fields.as_ref(),
);
let static_event_content_impl =
event_content.expand_static_event_content_impl(&event_content.ident);
let json_castable_impl = generate_json_castable_impl(&event_content.ident, &[]);
let event_type_aliases = event_content.expand_event_type_aliases();
Ok(quote! {
#redacted_event_content
#possibly_redacted_event_content
#event_content_without_relation
#event_content_impl
#static_event_content_impl
#json_castable_impl
#event_type_aliases
})
}
struct EventContent {
ident: syn::Ident,
vis: syn::Visibility,
fields: Option<Vec<EventContentField>>,
types: EventTypes,
kind: EventContentKind,
has_without_relation: bool,
ruma_events: RumaEvents,
}
impl EventContent {
fn type_fragment_field(&self) -> Option<&syn::Ident> {
self.fields
.as_ref()?
.iter()
.find(|field| field.is_type_fragment)
.and_then(|field| field.inner.ident.as_ref())
}
fn expand_redacted_event_content(&self) -> Option<TokenStream> {
if !self.kind.should_generate_redacted() {
return None;
}
let ruma_events = &self.ruma_events;
let ruma_common = ruma_events.ruma_common();
let serde = ruma_events.reexported(RumaEventsReexport::Serde);
let ident = &self.ident;
let vis = &self.vis;
let redacted_doc = format!("Redacted form of [`{ident}`]");
let redacted_ident = EventContentVariation::Redacted.variation_ident(ident);
let redacted_fields =
self.fields.iter().flatten().filter(|field| field.skip_redaction).collect::<Vec<_>>();
let redacted_fields_idents = redacted_fields.iter().flat_map(|field| &field.inner.ident);
let constructor = redacted_fields.is_empty().then(|| {
let constructor_doc = format!("Creates an empty {redacted_ident}.");
quote! {
impl #redacted_ident {
#[doc = #constructor_doc]
#vis fn new() -> Self {
Self {}
}
}
}
});
let redacted_event_content = self.expand_event_content_impl(
EventContentVariation::Redacted,
&redacted_ident,
Some(redacted_fields.iter().copied()),
);
let static_event_content_impl = self.expand_static_event_content_impl(&redacted_ident);
let json_castable_impl = generate_json_castable_impl(&redacted_ident, &[ident]);
Some(quote! {
#[automatically_derived]
impl #ruma_events::RedactContent for #ident {
type Redacted = #redacted_ident;
fn redact(self, _rules: &#ruma_common::room_version_rules::RedactionRules) -> #redacted_ident {
#redacted_ident {
#( #redacted_fields_idents: self.#redacted_fields_idents, )*
}
}
}
#[doc = #redacted_doc]
#[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#vis struct #redacted_ident {
#( #redacted_fields, )*
}
#constructor
#redacted_event_content
#static_event_content_impl
#json_castable_impl
})
}
fn expand_possibly_redacted_event_content(&self) -> Option<TokenStream> {
if !self.kind.should_generate_possibly_redacted() {
return None;
}
let serde = self.ruma_events.reexported(RumaEventsReexport::Serde);
let ident = &self.ident;
let vis = &self.vis;
let possibly_redacted_doc = format!(
"The possibly redacted form of [`{ident}`].\n\n\
This type is used when it's not obvious whether the content is redacted or not."
);
let possibly_redacted_ident =
EventContentVariation::PossiblyRedacted.variation_ident(ident);
let mut field_changed = false;
let possibly_redacted_fields = self
.fields
.iter()
.flatten()
.map(|field| {
if field.keep_in_possibly_redacted() {
return Cow::Borrowed(field);
}
field_changed = true;
let mut field = field.clone();
let wrapped_type = &field.inner.ty;
field.inner.ty = parse_quote! { Option<#wrapped_type> };
field
.inner
.attrs
.push(parse_quote! { #[serde(skip_serializing_if = "Option::is_none")] });
Cow::Owned(field)
})
.collect::<Vec<_>>();
let should_generate_redacted = self.kind.should_generate_redacted();
let redacted_ident = should_generate_redacted
.then(|| EventContentVariation::Redacted.variation_ident(ident));
let redacted_field_idents = should_generate_redacted
.then(|| {
possibly_redacted_fields
.iter()
.filter(|field| field.skip_redaction)
.map(|field| &field.inner.ident)
})
.into_iter()
.flatten()
.collect::<Vec<_>>();
let from_redacted_field_exprs = should_generate_redacted
.then(|| {
possibly_redacted_fields.iter().map(|field| {
let ident = &field.inner.ident;
if field.skip_redaction {
quote! { #ident }
} else if let Some(default_expr) = field.inner.serde_default_expr() {
quote! { #ident: #default_expr() }
} else {
quote! { #ident: Default::default() }
}
})
})
.into_iter()
.flatten()
.collect::<Vec<_>>();
let from_redacted_impl = should_generate_redacted.then(|| {
quote! {
impl From<#redacted_ident> for #possibly_redacted_ident {
fn from(value: #redacted_ident) -> #possibly_redacted_ident {
let #redacted_ident {
#( #redacted_field_idents, )*
} = value;
Self {
#( #from_redacted_field_exprs, )*
}
}
}
}
});
if field_changed {
let possibly_redacted_event_content = self.expand_event_content_impl(
EventContentVariation::PossiblyRedacted,
&possibly_redacted_ident,
Some(possibly_redacted_fields.iter().map(|field| field.as_ref())),
);
let static_event_content_impl =
self.expand_static_event_content_impl(&possibly_redacted_ident);
let json_castable_impl = if self.kind.should_generate_redacted() {
let redacted_ident = EventContentVariation::PossiblyRedacted.variation_ident(ident);
generate_json_castable_impl(&possibly_redacted_ident, &[ident, &redacted_ident])
} else {
generate_json_castable_impl(&possibly_redacted_ident, &[ident])
};
let field_idents = possibly_redacted_fields.iter().map(|field| &field.inner.ident);
let from_original_field_exprs = possibly_redacted_fields.iter().map(|field| {
let ident = &field.inner.ident;
if matches!(field, Cow::Borrowed(_)) {
quote! { #ident }
} else {
quote! { #ident: Some(#ident) }
}
});
let redact_content_impl = self.kind.should_generate_redacted().then(|| {
let ruma_events = &self.ruma_events;
let ruma_common = ruma_events.ruma_common();
let maybe_remaining_fields = (redacted_field_idents.len() != from_redacted_field_exprs.len()).then(|| quote! { .. });
quote! {
#[automatically_derived]
impl #ruma_events::RedactContent for #possibly_redacted_ident {
type Redacted = #possibly_redacted_ident;
fn redact(self, _rules: &#ruma_common::room_version_rules::RedactionRules) -> #possibly_redacted_ident {
let Self {
#( #redacted_field_idents, )*
#maybe_remaining_fields
} = self;
Self {
#( #from_redacted_field_exprs, )*
}
}
}
}
});
Some(quote! {
#[doc = #possibly_redacted_doc]
#[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#vis struct #possibly_redacted_ident {
#( #possibly_redacted_fields, )*
}
impl From<#ident> for #possibly_redacted_ident {
fn from(value: #ident) -> #possibly_redacted_ident {
let #ident {
#( #field_idents, )*
} = value;
Self {
#( #from_original_field_exprs, )*
}
}
}
#redact_content_impl
#from_redacted_impl
#possibly_redacted_event_content
#static_event_content_impl
#json_castable_impl
})
} else {
let event_content_kind_trait_impl = self.expand_event_content_kind_trait_impl(
EventContentTraitVariation::PossiblyRedacted,
ident,
);
Some(quote! {
#[doc = #possibly_redacted_doc]
#vis type #possibly_redacted_ident = #ident;
#from_redacted_impl
#event_content_kind_trait_impl
})
}
}
fn expand_event_content_without_relation(&self) -> Option<TokenStream> {
if !self.has_without_relation {
return None;
}
let serde = self.ruma_events.reexported(RumaEventsReexport::Serde);
let ident = &self.ident;
let vis = &self.vis;
let without_relation_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!("Convert `self` into a [`{ident}`] with the given relation.");
let (relates_to_field, without_relation_fields) =
self.fields.iter().flatten().partition::<Vec<_>, _>(|field| {
field.inner.ident.as_ref().is_some_and(|ident| *ident == "relates_to")
});
let relates_to_type = relates_to_field.first().map(|field| &field.inner.ty).expect(
"event content type without relation should have a `relates_to` field; \
this should have been checked during parsing",
);
let without_relation_fields_idents =
without_relation_fields.iter().flat_map(|field| &field.inner.ident).collect::<Vec<_>>();
let without_relation_struct_definition = if without_relation_fields.is_empty() {
quote! { ; }
} else {
quote! {
{ #( #without_relation_fields, )* }
}
};
let json_castable_impl = generate_json_castable_impl(&without_relation_ident, &[ident]);
Some(quote! {
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::std::convert::From<#ident> for #without_relation_ident {
fn from(c: #ident) -> Self {
Self {
#( #without_relation_fields_idents: c.#without_relation_fields_idents, )*
}
}
}
#[doc = #without_relation_doc]
#[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#vis struct #without_relation_ident #without_relation_struct_definition
impl #without_relation_ident {
#[doc = #with_relation_fn_doc]
#vis fn with_relation(self, relates_to: #relates_to_type) -> #ident {
#ident {
#( #without_relation_fields_idents: self.#without_relation_fields_idents, )*
relates_to,
}
}
}
#json_castable_impl
})
}
fn expand_event_content_impl<'a>(
&self,
variation: EventContentVariation,
ident: &syn::Ident,
fields: Option<impl IntoIterator<Item = &'a EventContentField>>,
) -> TokenStream {
let event_content_kind_trait_impl =
self.expand_event_content_kind_trait_impl(variation.into(), ident);
let static_state_event_content_impl =
self.expand_static_state_event_content_impl(variation, ident);
let event_content_from_type_impl = self.expand_event_content_from_type_impl(ident, fields);
quote! {
#event_content_from_type_impl
#event_content_kind_trait_impl
#static_state_event_content_impl
}
}
fn expand_event_content_kind_trait_impl(
&self,
variation: EventContentTraitVariation,
ident: &syn::Ident,
) -> TokenStream {
let ruma_events = &self.ruma_events;
let event_type = self.types.ev_type.without_wildcard();
let event_type_fn_impl = if let Some(field) = self.type_fragment_field() {
let format = event_type.to_owned() + "{}";
quote! {
::std::convert::From::from(::std::format!(#format, self.#field))
}
} else {
quote! { ::std::convert::From::from(#event_type) }
};
let state_key =
as_variant!(&self.kind, EventContentKind::State { state_key_type, .. } => state_key_type).map(|state_key_type| {
quote! {
type StateKey = #state_key_type;
}
});
self.kind
.as_event_type_enums_and_content_kind_traits(variation)
.into_iter()
.map(|(event_type_enum, event_content_kind_trait)| {
quote! {
#[automatically_derived]
impl #ruma_events::#event_content_kind_trait for #ident {
#state_key
fn event_type(&self) -> #ruma_events::#event_type_enum {
#event_type_fn_impl
}
}
}
})
.collect()
}
fn expand_static_state_event_content_impl(
&self,
variation: EventContentVariation,
ident: &syn::Ident,
) -> Option<TokenStream> {
let EventContentKind::State { unsigned_type, .. } = &self.kind else {
return None;
};
if variation != EventContentVariation::Original {
return None;
}
let ruma_events = &self.ruma_events;
let possibly_redacted_ident =
EventContentVariation::PossiblyRedacted.variation_ident(ident);
Some(quote! {
#[automatically_derived]
impl #ruma_events::StaticStateEventContent for #ident {
type PossiblyRedacted = #possibly_redacted_ident;
type Unsigned = #unsigned_type;
}
})
}
fn expand_static_event_content_impl(&self, ident: &syn::Ident) -> TokenStream {
let ruma_events = &self.ruma_events;
let static_event_type = self.types.ev_type.without_wildcard();
let is_prefix = if self.types.is_prefix() {
quote! { #ruma_events::True }
} else {
quote! { #ruma_events::False }
};
quote! {
impl #ruma_events::StaticEventContent for #ident {
const TYPE: &'static ::std::primitive::str = #static_event_type;
type IsPrefix = #is_prefix;
}
}
}
fn expand_event_content_from_type_impl<'a>(
&self,
ident: &syn::Ident,
fields: Option<impl IntoIterator<Item = &'a EventContentField>>,
) -> Option<TokenStream> {
let type_fragment_field = self.type_fragment_field()?;
let fields = fields.expect(
"event content with `.*` type suffix should be a struct; \
this should have been checked during parsing",
);
let ruma_events = &self.ruma_events;
let serde = ruma_events.reexported(RumaEventsReexport::Serde);
let serde_json = ruma_events.reexported(RumaEventsReexport::SerdeJson);
let type_prefixes = self.types.iter().map(EventType::without_wildcard);
let type_prefixes = quote! {
[#( #type_prefixes, )*]
};
let fields_without_type_fragment = fields
.into_iter()
.filter(|field| !field.is_type_fragment)
.map(|field| PrivateField(&field.inner))
.collect::<Vec<_>>();
let fields_ident_without_type_fragment =
fields_without_type_fragment.iter().filter_map(|f| f.0.ident.as_ref());
Some(quote! {
impl #ruma_events::EventContentFromType for #ident {
fn from_parts(
ev_type: &::std::primitive::str,
content: &#serde_json::value::RawValue,
) -> #serde_json::Result<Self> {
#[derive(#serde::Deserialize)]
struct WithoutTypeFragment {
#( #fields_without_type_fragment, )*
}
if let ::std::option::Option::Some(type_fragment) =
#type_prefixes.iter().find_map(|prefix| ev_type.strip_prefix(prefix))
{
let c: WithoutTypeFragment = #serde_json::from_str(content.get())?;
::std::result::Result::Ok(Self {
#(
#fields_ident_without_type_fragment:
c.#fields_ident_without_type_fragment,
)*
#type_fragment_field: type_fragment.to_owned(),
})
} else {
::std::result::Result::Err(#serde::de::Error::custom(
::std::format!(
"expected event type starting with one of `{:?}`, found `{}`",
#type_prefixes, ev_type,
)
))
}
}
}
})
}
fn expand_event_type_aliases(&self) -> Option<TokenStream> {
if self.ident == "RoomRedactionEventContent" {
return None;
}
let ruma_events = &self.ruma_events;
let event_type = &self.types.ev_type;
let ident = &self.ident;
let ident_s = ident.to_string();
let ev_type_s = ident_s.strip_suffix("Content").expect(
"event content struct name should end with `Content`; \
this should have been checked during parsing",
);
let vis = &self.vis;
Some(
self.kind
.event_variations()
.iter()
.flat_map(|&variation| {
std::iter::repeat(variation)
.zip(self.kind.as_event_idents(variation).into_iter().flatten())
})
.map(|(variation, (type_kind_prefix, event_ident))| {
let type_alias_ident =
format_ident!("{variation}{type_kind_prefix}{ev_type_s}");
let doc_suffix = match variation {
EventVariation::None | EventVariation::Original => "",
EventVariation::Sync | EventVariation::OriginalSync => {
" from a `sync_events` response"
}
EventVariation::Stripped => " from an invited room preview",
EventVariation::Redacted => " that has been redacted",
EventVariation::RedactedSync => {
" from a `sync_events` response that has been redacted"
}
EventVariation::Initial => " for creating a room",
};
let type_alias_doc = if type_kind_prefix.is_empty() {
format!("An `{event_type}` event{doc_suffix}.")
} else {
format!(
"A {} `{event_type}` event{doc_suffix}.",
type_kind_prefix.to_lowercase()
)
};
let content_ident = if variation.is_redacted() {
EventContentVariation::Redacted.variation_ident(ident)
} else if let EventVariation::Stripped = variation {
EventContentVariation::PossiblyRedacted.variation_ident(ident)
} else {
EventContentVariation::Original.variation_ident(ident)
};
quote! {
#[doc = #type_alias_doc]
#vis type #type_alias_ident = #ruma_events::#event_ident<#content_ident>;
}
})
.collect(),
)
}
}
#[derive(Clone)]
struct EventContentField {
inner: syn::Field,
skip_redaction: bool,
is_type_fragment: bool,
}
impl EventContentField {
fn keep_in_possibly_redacted(&self) -> bool {
self.skip_redaction
|| self.inner.ty.option_inner_type().is_some()
|| self.inner.has_serde_meta_item(SerdeMetaItem::Default)
}
}
impl ToTokens for EventContentField {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.inner.to_tokens(tokens);
}
}
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
enum EventContentKind {
GlobalAccountData,
RoomAccountData,
BothAccountData,
EphemeralRoom,
MessageLike {
has_custom_redacted: bool,
},
State {
state_key_type: syn::Type,
unsigned_type: syn::Type,
has_custom_redacted: bool,
has_custom_possibly_redacted: bool,
},
ToDevice,
}
impl EventContentKind {
fn event_kind(&self) -> Option<CommonEventKind> {
Some(match self {
Self::GlobalAccountData => CommonEventKind::GlobalAccountData,
Self::RoomAccountData => CommonEventKind::RoomAccountData,
Self::BothAccountData => return None,
Self::EphemeralRoom => CommonEventKind::EphemeralRoom,
Self::MessageLike { .. } => CommonEventKind::MessageLike,
Self::State { .. } => CommonEventKind::State,
Self::ToDevice => CommonEventKind::ToDevice,
})
}
fn is_account_data(&self) -> bool {
matches!(self, Self::BothAccountData)
|| self.event_kind().is_some_and(|event_kind| {
matches!(
event_kind,
CommonEventKind::GlobalAccountData | CommonEventKind::RoomAccountData
)
})
}
fn should_generate_redacted(&self) -> bool {
matches!(self, Self::MessageLike { has_custom_redacted, .. } | Self::State { has_custom_redacted, .. } if !*has_custom_redacted)
}
fn should_generate_possibly_redacted(&self) -> bool {
matches!(self, Self::State { has_custom_possibly_redacted, .. } if !*has_custom_possibly_redacted)
}
fn event_variations(&self) -> &'static [EventVariation] {
if let Some(event_kind) = self.event_kind() {
event_kind.event_variations()
} else {
CommonEventKind::GlobalAccountData.event_variations()
}
}
fn as_event_idents(
&self,
variation: EventVariation,
) -> Option<Vec<(&'static str, syn::Ident)>> {
if let Some(event_kind) = self.event_kind() {
event_kind.to_event_ident(variation).map(|event_ident| vec![("", event_ident)])
} else {
let first_event_ident = CommonEventKind::GlobalAccountData
.to_event_ident(variation)
.map(|event_ident| ("Global", event_ident));
let second_event_ident = CommonEventKind::RoomAccountData
.to_event_ident(variation)
.map(|event_ident| ("Room", event_ident));
if first_event_ident.is_none() && second_event_ident.is_none() {
None
} else {
Some(first_event_ident.into_iter().chain(second_event_ident).collect())
}
}
}
fn as_event_type_enums_and_content_kind_traits(
&self,
variation: EventContentTraitVariation,
) -> Vec<(syn::Ident, syn::Ident)> {
if let Some(event_kind) = self.event_kind() {
vec![(event_kind.to_event_type_enum(), event_kind.to_content_kind_trait(variation))]
} else {
[CommonEventKind::GlobalAccountData, CommonEventKind::RoomAccountData]
.iter()
.map(|event_kind| {
(event_kind.to_event_type_enum(), event_kind.to_content_kind_trait(variation))
})
.collect()
}
}
}
fn generate_json_castable_impl(ident: &syn::Ident, others: &[&syn::Ident]) -> TokenStream {
let ruma_common = RumaCommon::new();
let mut json_castable_impls = quote! {
#[automatically_derived]
impl #ruma_common::serde::JsonCastable<#ruma_common::serde::JsonObject> for #ident {}
};
json_castable_impls.extend(others.iter().map(|other| {
quote! {
#[automatically_derived]
impl #ruma_common::serde::JsonCastable<#ident> for #other {}
}
}));
json_castable_impls
}
#[derive(Clone, Copy, PartialEq)]
enum EventContentVariation {
Original,
Redacted,
PossiblyRedacted,
}
impl EventContentVariation {
fn variation_ident(self, ident: &syn::Ident) -> Cow<'_, syn::Ident> {
match self {
Self::Original => Cow::Borrowed(ident),
Self::Redacted => Cow::Owned(format_ident!("Redacted{ident}")),
Self::PossiblyRedacted => Cow::Owned(format_ident!("PossiblyRedacted{ident}")),
}
}
}
impl From<EventContentVariation> for EventContentTraitVariation {
fn from(value: EventContentVariation) -> Self {
match value {
EventContentVariation::Original => Self::Original,
EventContentVariation::Redacted => Self::Redacted,
EventContentVariation::PossiblyRedacted => Self::PossiblyRedacted,
}
}
}
impl CommonEventKind {
fn to_event_ident(self, variation: EventVariation) -> Option<syn::Ident> {
if !self.event_variations().contains(&variation) {
return None;
}
Some(format_ident!("{variation}{self}"))
}
fn to_content_kind_trait(self, variation: EventContentTraitVariation) -> syn::Ident {
format_ident!("{variation}{self}Content")
}
}