1047 lines
37 KiB
Rust
1047 lines
37 KiB
Rust
//! Implementations of the EventContent derive macro.
|
|
#![allow(clippy::too_many_arguments)] // FIXME
|
|
|
|
use std::{borrow::Cow, fmt};
|
|
|
|
use proc_macro2::{Span, TokenStream};
|
|
use quote::{format_ident, quote, ToTokens};
|
|
use syn::{
|
|
parse::{Parse, ParseStream},
|
|
parse_quote,
|
|
punctuated::Punctuated,
|
|
DeriveInput, Field, Ident, LitStr, Meta, Token, Type,
|
|
};
|
|
|
|
use super::event_parse::{EventKind, EventKindVariation};
|
|
use crate::util::{m_prefix_name_to_type_name, PrivateField};
|
|
|
|
mod kw {
|
|
// This `content` field is kept when the event is redacted.
|
|
syn::custom_keyword!(skip_redaction);
|
|
// Do not emit any redacted event code.
|
|
syn::custom_keyword!(custom_redacted);
|
|
// Do not emit any possibly redacted event code.
|
|
syn::custom_keyword!(custom_possibly_redacted);
|
|
// The kind of event content this is.
|
|
syn::custom_keyword!(kind);
|
|
syn::custom_keyword!(type_fragment);
|
|
// The type to use for a state events' `state_key` field.
|
|
syn::custom_keyword!(state_key_type);
|
|
// The type to use for a state events' `unsigned` field.
|
|
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 field attributes for `*EventContent` derives.
|
|
///
|
|
/// `#[ruma_event(skip_redaction)]`
|
|
enum EventFieldMeta {
|
|
/// Fields marked with `#[ruma_event(skip_redaction)]` are kept when the event is
|
|
/// redacted.
|
|
SkipRedaction,
|
|
|
|
/// The given field holds a part of the event type (replaces the `*` in a `m.foo.*` event
|
|
/// type).
|
|
TypeFragment,
|
|
}
|
|
|
|
impl Parse for EventFieldMeta {
|
|
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
|
let lookahead = input.lookahead1();
|
|
if lookahead.peek(kw::skip_redaction) {
|
|
let _: kw::skip_redaction = input.parse()?;
|
|
Ok(EventFieldMeta::SkipRedaction)
|
|
} else if lookahead.peek(kw::type_fragment) {
|
|
let _: kw::type_fragment = input.parse()?;
|
|
Ok(EventFieldMeta::TypeFragment)
|
|
} else {
|
|
Err(lookahead.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ContentMeta {
|
|
event_type: Option<LitStr>,
|
|
event_kind: Option<EventKind>,
|
|
custom_redacted: Option<kw::custom_redacted>,
|
|
custom_possibly_redacted: Option<kw::custom_possibly_redacted>,
|
|
state_key_type: Option<Box<Type>>,
|
|
unsigned_type: Option<Box<Type>>,
|
|
aliases: Vec<LitStr>,
|
|
without_relation: Option<kw::without_relation>,
|
|
}
|
|
|
|
impl ContentMeta {
|
|
fn merge(self, other: ContentMeta) -> syn::Result<Self> {
|
|
fn either_spanned<T: ToTokens>(a: Option<T>, b: Option<T>) -> syn::Result<Option<T>> {
|
|
match (a, b) {
|
|
(None, None) => Ok(None),
|
|
(Some(val), None) | (None, Some(val)) => Ok(Some(val)),
|
|
(Some(a), Some(b)) => {
|
|
let mut error = syn::Error::new_spanned(a, "redundant attribute argument");
|
|
error.combine(syn::Error::new_spanned(b, "note: first one here"));
|
|
Err(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn either_named<T>(name: &str, a: Option<T>, b: Option<T>) -> syn::Result<Option<T>> {
|
|
match (a, b) {
|
|
(None, None) => Ok(None),
|
|
(Some(val), None) | (None, Some(val)) => Ok(Some(val)),
|
|
(Some(_), Some(_)) => Err(syn::Error::new(
|
|
Span::call_site(),
|
|
format!("multiple {name} attributes found, there can only be one"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
Ok(Self {
|
|
event_type: either_spanned(self.event_type, other.event_type)?,
|
|
event_kind: either_named("event_kind", self.event_kind, other.event_kind)?,
|
|
custom_redacted: either_spanned(self.custom_redacted, other.custom_redacted)?,
|
|
custom_possibly_redacted: either_spanned(
|
|
self.custom_possibly_redacted,
|
|
other.custom_possibly_redacted,
|
|
)?,
|
|
state_key_type: either_spanned(self.state_key_type, other.state_key_type)?,
|
|
unsigned_type: either_spanned(self.unsigned_type, other.unsigned_type)?,
|
|
aliases: [self.aliases, other.aliases].concat(),
|
|
without_relation: either_spanned(self.without_relation, other.without_relation)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Parse for ContentMeta {
|
|
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
|
let lookahead = input.lookahead1();
|
|
if lookahead.peek(Token![type]) {
|
|
let _: Token![type] = input.parse()?;
|
|
let _: Token![=] = input.parse()?;
|
|
let event_type = input.parse()?;
|
|
|
|
Ok(Self { event_type: Some(event_type), ..Default::default() })
|
|
} else if lookahead.peek(kw::kind) {
|
|
let _: kw::kind = input.parse()?;
|
|
let _: Token![=] = input.parse()?;
|
|
let event_kind = input.parse()?;
|
|
|
|
Ok(Self { event_kind: Some(event_kind), ..Default::default() })
|
|
} else if lookahead.peek(kw::custom_redacted) {
|
|
let custom_redacted: kw::custom_redacted = input.parse()?;
|
|
|
|
Ok(Self { custom_redacted: Some(custom_redacted), ..Default::default() })
|
|
} else if lookahead.peek(kw::custom_possibly_redacted) {
|
|
let custom_possibly_redacted: kw::custom_possibly_redacted = input.parse()?;
|
|
|
|
Ok(Self {
|
|
custom_possibly_redacted: Some(custom_possibly_redacted),
|
|
..Default::default()
|
|
})
|
|
} else if lookahead.peek(kw::state_key_type) {
|
|
let _: kw::state_key_type = input.parse()?;
|
|
let _: Token![=] = input.parse()?;
|
|
let state_key_type = input.parse()?;
|
|
|
|
Ok(Self { state_key_type: Some(state_key_type), ..Default::default() })
|
|
} else if lookahead.peek(kw::unsigned_type) {
|
|
let _: kw::unsigned_type = input.parse()?;
|
|
let _: Token![=] = input.parse()?;
|
|
let unsigned_type = input.parse()?;
|
|
|
|
Ok(Self { unsigned_type: Some(unsigned_type), ..Default::default() })
|
|
} else if lookahead.peek(kw::alias) {
|
|
let _: kw::alias = input.parse()?;
|
|
let _: Token![=] = input.parse()?;
|
|
let alias = input.parse()?;
|
|
|
|
Ok(Self { aliases: vec![alias], ..Default::default() })
|
|
} else if lookahead.peek(kw::without_relation) {
|
|
let without_relation: kw::without_relation = input.parse()?;
|
|
|
|
Ok(Self { without_relation: Some(without_relation), ..Default::default() })
|
|
} else {
|
|
Err(lookahead.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ContentAttrs {
|
|
event_type: LitStr,
|
|
event_kind: Option<EventKind>,
|
|
state_key_type: Option<TokenStream>,
|
|
unsigned_type: Option<TokenStream>,
|
|
aliases: Vec<LitStr>,
|
|
is_custom_redacted: bool,
|
|
is_custom_possibly_redacted: bool,
|
|
has_without_relation: bool,
|
|
}
|
|
|
|
impl TryFrom<ContentMeta> for ContentAttrs {
|
|
type Error = syn::Error;
|
|
|
|
fn try_from(value: ContentMeta) -> Result<Self, Self::Error> {
|
|
let ContentMeta {
|
|
event_type,
|
|
event_kind,
|
|
custom_redacted,
|
|
custom_possibly_redacted,
|
|
state_key_type,
|
|
unsigned_type,
|
|
aliases,
|
|
without_relation,
|
|
} = value;
|
|
|
|
let event_type = event_type.ok_or_else(|| {
|
|
syn::Error::new(
|
|
Span::call_site(),
|
|
"no event type attribute found, \
|
|
add `#[ruma_event(type = \"any.room.event\", kind = Kind)]` \
|
|
below the event content derive",
|
|
)
|
|
})?;
|
|
|
|
let state_key_type = match (event_kind, state_key_type) {
|
|
(Some(EventKind::State), None) => {
|
|
return Err(syn::Error::new(
|
|
Span::call_site(),
|
|
"no state_key_type attribute found, please specify one",
|
|
));
|
|
}
|
|
(Some(EventKind::State), Some(ty)) => Some(quote! { #ty }),
|
|
(_, None) => None,
|
|
(_, Some(ty)) => {
|
|
return Err(syn::Error::new_spanned(
|
|
ty,
|
|
"state_key_type attribute is not valid for non-state event kinds",
|
|
));
|
|
}
|
|
};
|
|
|
|
let is_custom_redacted = custom_redacted.is_some();
|
|
let is_custom_possibly_redacted = custom_possibly_redacted.is_some();
|
|
|
|
let unsigned_type = unsigned_type.map(|ty| quote! { #ty });
|
|
|
|
let event_type_s = event_type.value();
|
|
let prefix = event_type_s.strip_suffix(".*");
|
|
|
|
if prefix.unwrap_or(&event_type_s).contains('*') {
|
|
return Err(syn::Error::new_spanned(
|
|
event_type,
|
|
"event type may only contain `*` as part of a `.*` suffix",
|
|
));
|
|
}
|
|
|
|
if prefix.is_some() && !event_kind.is_some_and(|k| k.is_account_data()) {
|
|
return Err(syn::Error::new_spanned(
|
|
event_type,
|
|
"only account data events may contain a `.*` suffix",
|
|
));
|
|
}
|
|
|
|
for alias in &aliases {
|
|
if alias.value().ends_with(".*") != prefix.is_some() {
|
|
return Err(syn::Error::new_spanned(
|
|
alias,
|
|
"aliases should have the same `.*` suffix, or lack thereof, as the main event type",
|
|
));
|
|
}
|
|
}
|
|
|
|
let has_without_relation = without_relation.is_some();
|
|
|
|
Ok(Self {
|
|
event_type,
|
|
event_kind,
|
|
state_key_type,
|
|
unsigned_type,
|
|
aliases,
|
|
is_custom_redacted,
|
|
is_custom_possibly_redacted,
|
|
has_without_relation,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Create an `EventContent` implementation for a struct.
|
|
pub fn expand_event_content(
|
|
input: &DeriveInput,
|
|
ruma_events: &TokenStream,
|
|
) -> syn::Result<TokenStream> {
|
|
let content_meta = input
|
|
.attrs
|
|
.iter()
|
|
.filter(|attr| attr.path().is_ident("ruma_event"))
|
|
.try_fold(ContentMeta::default(), |meta, attr| {
|
|
let list: Punctuated<ContentMeta, Token![,]> =
|
|
attr.parse_args_with(Punctuated::parse_terminated)?;
|
|
|
|
list.into_iter().try_fold(meta, ContentMeta::merge)
|
|
})?;
|
|
|
|
let ContentAttrs {
|
|
event_type,
|
|
event_kind,
|
|
state_key_type,
|
|
unsigned_type,
|
|
aliases,
|
|
is_custom_redacted,
|
|
is_custom_possibly_redacted,
|
|
has_without_relation,
|
|
} = content_meta.try_into()?;
|
|
|
|
let ident = &input.ident;
|
|
let fields = match &input.data {
|
|
syn::Data::Struct(syn::DataStruct { fields, .. }) => Some(fields.iter()),
|
|
_ => {
|
|
if event_kind.is_some_and(|kind| needs_redacted(is_custom_redacted, kind)) {
|
|
return Err(syn::Error::new(
|
|
Span::call_site(),
|
|
"To generate a redacted event content, the event content type needs to be a struct. Disable this with the custom_redacted attribute",
|
|
));
|
|
}
|
|
|
|
if event_kind.is_some_and(|kind| needs_possibly_redacted(is_custom_redacted, kind)) {
|
|
return Err(syn::Error::new(
|
|
Span::call_site(),
|
|
"To generate a possibly redacted event content, the event content type needs to be a struct. Disable this with the custom_possibly_redacted attribute",
|
|
));
|
|
}
|
|
|
|
if has_without_relation {
|
|
return Err(syn::Error::new(
|
|
Span::call_site(),
|
|
"To generate an event content without relation, the event content type needs to be a struct. Disable this by removing the without_relation attribute",
|
|
));
|
|
}
|
|
|
|
None
|
|
}
|
|
};
|
|
|
|
// We only generate redacted content structs for state and message-like events
|
|
let redacted_event_content =
|
|
event_kind.filter(|kind| needs_redacted(is_custom_redacted, *kind)).map(|kind| {
|
|
generate_redacted_event_content(
|
|
ident,
|
|
&input.vis,
|
|
fields.clone().unwrap(),
|
|
&event_type,
|
|
kind,
|
|
state_key_type.as_ref(),
|
|
unsigned_type.clone(),
|
|
&aliases,
|
|
ruma_events,
|
|
)
|
|
.unwrap_or_else(syn::Error::into_compile_error)
|
|
});
|
|
|
|
// We only generate possibly redacted content structs for state events.
|
|
let possibly_redacted_event_content = event_kind
|
|
.filter(|kind| needs_possibly_redacted(is_custom_possibly_redacted, *kind))
|
|
.map(|_| {
|
|
generate_possibly_redacted_event_content(
|
|
ident,
|
|
&input.vis,
|
|
fields.clone().unwrap(),
|
|
&event_type,
|
|
state_key_type.as_ref(),
|
|
unsigned_type.clone(),
|
|
&aliases,
|
|
ruma_events,
|
|
)
|
|
.unwrap_or_else(syn::Error::into_compile_error)
|
|
});
|
|
|
|
let event_content_without_relation = has_without_relation.then(|| {
|
|
generate_event_content_without_relation(
|
|
ident,
|
|
&input.vis,
|
|
fields.clone().unwrap(),
|
|
ruma_events,
|
|
)
|
|
.unwrap_or_else(syn::Error::into_compile_error)
|
|
});
|
|
|
|
let event_content_impl = generate_event_content_impl(
|
|
ident,
|
|
&input.vis,
|
|
fields,
|
|
&event_type,
|
|
event_kind,
|
|
EventKindContentVariation::Original,
|
|
state_key_type.as_ref(),
|
|
unsigned_type,
|
|
&aliases,
|
|
ruma_events,
|
|
)
|
|
.unwrap_or_else(syn::Error::into_compile_error);
|
|
let static_event_content_impl =
|
|
generate_static_event_content_impl(ident, &event_type, ruma_events);
|
|
let type_aliases = event_kind.map(|k| {
|
|
generate_event_type_aliases(k, ident, &input.vis, &event_type.value(), ruma_events)
|
|
.unwrap_or_else(syn::Error::into_compile_error)
|
|
});
|
|
|
|
Ok(quote! {
|
|
#redacted_event_content
|
|
#possibly_redacted_event_content
|
|
#event_content_without_relation
|
|
#event_content_impl
|
|
#static_event_content_impl
|
|
#type_aliases
|
|
})
|
|
}
|
|
|
|
fn generate_redacted_event_content<'a>(
|
|
ident: &Ident,
|
|
vis: &syn::Visibility,
|
|
fields: impl Iterator<Item = &'a Field>,
|
|
event_type: &LitStr,
|
|
event_kind: EventKind,
|
|
state_key_type: Option<&TokenStream>,
|
|
unsigned_type: Option<TokenStream>,
|
|
aliases: &[LitStr],
|
|
ruma_events: &TokenStream,
|
|
) -> syn::Result<TokenStream> {
|
|
assert!(
|
|
!event_type.value().contains('*'),
|
|
"Event type shouldn't contain a `*`, this should have been checked previously"
|
|
);
|
|
|
|
let ruma_common = quote! { #ruma_events::exports::ruma_common };
|
|
let serde = quote! { #ruma_events::exports::serde };
|
|
|
|
let doc = format!("Redacted form of [`{ident}`]");
|
|
let redacted_ident = format_ident!("Redacted{ident}");
|
|
|
|
let kept_redacted_fields: Vec<_> = fields
|
|
.map(|f| {
|
|
let mut keep_field = false;
|
|
let attrs = f
|
|
.attrs
|
|
.iter()
|
|
.map(|a| -> syn::Result<_> {
|
|
if a.path().is_ident("ruma_event") {
|
|
if let EventFieldMeta::SkipRedaction = a.parse_args()? {
|
|
keep_field = true;
|
|
}
|
|
|
|
// don't re-emit our `ruma_event` attributes
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(a.clone()))
|
|
}
|
|
})
|
|
.filter_map(Result::transpose)
|
|
.collect::<syn::Result<_>>()?;
|
|
|
|
if keep_field {
|
|
Ok(Some(Field { attrs, ..f.clone() }))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
})
|
|
.filter_map(Result::transpose)
|
|
.collect::<syn::Result<_>>()?;
|
|
|
|
let redaction_struct_fields = kept_redacted_fields.iter().flat_map(|f| &f.ident);
|
|
|
|
let constructor = kept_redacted_fields.is_empty().then(|| {
|
|
let doc = format!("Creates an empty {redacted_ident}.");
|
|
quote! {
|
|
impl #redacted_ident {
|
|
#[doc = #doc]
|
|
#vis fn new() -> Self {
|
|
Self {}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let redacted_event_content = generate_event_content_impl(
|
|
&redacted_ident,
|
|
vis,
|
|
Some(kept_redacted_fields.iter()),
|
|
event_type,
|
|
Some(event_kind),
|
|
EventKindContentVariation::Redacted,
|
|
state_key_type,
|
|
unsigned_type,
|
|
aliases,
|
|
ruma_events,
|
|
)
|
|
.unwrap_or_else(syn::Error::into_compile_error);
|
|
|
|
let static_event_content_impl =
|
|
generate_static_event_content_impl(&redacted_ident, event_type, ruma_events);
|
|
|
|
Ok(quote! {
|
|
// this is the non redacted event content's impl
|
|
#[automatically_derived]
|
|
impl #ruma_events::RedactContent for #ident {
|
|
type Redacted = #redacted_ident;
|
|
|
|
fn redact(self, version: &#ruma_common::RoomVersionId) -> #redacted_ident {
|
|
#redacted_ident {
|
|
#( #redaction_struct_fields: self.#redaction_struct_fields, )*
|
|
}
|
|
}
|
|
}
|
|
|
|
#[doc = #doc]
|
|
#[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
#vis struct #redacted_ident {
|
|
#( #kept_redacted_fields, )*
|
|
}
|
|
|
|
#constructor
|
|
|
|
#redacted_event_content
|
|
|
|
#static_event_content_impl
|
|
})
|
|
}
|
|
|
|
fn generate_possibly_redacted_event_content<'a>(
|
|
ident: &Ident,
|
|
vis: &syn::Visibility,
|
|
fields: impl Iterator<Item = &'a Field>,
|
|
event_type: &LitStr,
|
|
state_key_type: Option<&TokenStream>,
|
|
unsigned_type: Option<TokenStream>,
|
|
aliases: &[LitStr],
|
|
ruma_events: &TokenStream,
|
|
) -> syn::Result<TokenStream> {
|
|
assert!(
|
|
!event_type.value().contains('*'),
|
|
"Event type shouldn't contain a `*`, this should have been checked previously"
|
|
);
|
|
|
|
let serde = quote! { #ruma_events::exports::serde };
|
|
|
|
let 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 = format_ident!("PossiblyRedacted{ident}");
|
|
|
|
let mut field_changed = false;
|
|
let possibly_redacted_fields: Vec<_> = fields
|
|
.map(|f| {
|
|
let mut keep_field = false;
|
|
let mut unsupported_serde_attribute = None;
|
|
|
|
if let Type::Path(type_path) = &f.ty {
|
|
if type_path.path.segments.first().filter(|s| s.ident == "Option").is_some() {
|
|
// Keep the field if it's an `Option`.
|
|
keep_field = true;
|
|
}
|
|
}
|
|
|
|
let mut attrs = f
|
|
.attrs
|
|
.iter()
|
|
.map(|a| -> syn::Result<_> {
|
|
if a.path().is_ident("ruma_event") {
|
|
// Keep the field if it is not redacted.
|
|
if let EventFieldMeta::SkipRedaction = a.parse_args()? {
|
|
keep_field = true;
|
|
}
|
|
|
|
// Don't re-emit our `ruma_event` attributes.
|
|
Ok(None)
|
|
} else {
|
|
if a.path().is_ident("serde") {
|
|
if let Meta::List(list) = &a.meta {
|
|
let nested: Punctuated<Meta, Token![,]> =
|
|
list.parse_args_with(Punctuated::parse_terminated)?;
|
|
for meta in &nested {
|
|
if meta.path().is_ident("default") {
|
|
// Keep the field if it deserializes to its default value.
|
|
keep_field = true;
|
|
} else if !meta.path().is_ident("rename")
|
|
&& !meta.path().is_ident("alias")
|
|
&& unsupported_serde_attribute.is_none()
|
|
{
|
|
// Error if the field is not kept and uses an unsupported
|
|
// serde attribute.
|
|
unsupported_serde_attribute =
|
|
Some(syn::Error::new_spanned(
|
|
meta,
|
|
"Can't generate PossiblyRedacted struct with \
|
|
unsupported serde attribute\n\
|
|
Expected one of `default`, `rename` or `alias`\n\
|
|
Use the `custom_possibly_redacted` attribute \
|
|
and create the struct manually",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Some(a.clone()))
|
|
}
|
|
})
|
|
.filter_map(Result::transpose)
|
|
.collect::<syn::Result<_>>()?;
|
|
|
|
if keep_field {
|
|
Ok(Field { attrs, ..f.clone() })
|
|
} else if let Some(err) = unsupported_serde_attribute {
|
|
Err(err)
|
|
} else if f.ident.is_none() {
|
|
// If the field has no `ident`, it's a tuple struct. Since `content` is an object,
|
|
// it will need a custom struct to deserialize from an empty object.
|
|
Err(syn::Error::new(
|
|
Span::call_site(),
|
|
"Can't generate PossiblyRedacted struct for tuple structs\n\
|
|
Use the `custom_possibly_redacted` attribute and create the struct manually",
|
|
))
|
|
} else {
|
|
// Change the field to an `Option`.
|
|
field_changed = true;
|
|
|
|
let old_type = &f.ty;
|
|
let ty = parse_quote! { Option<#old_type> };
|
|
attrs.push(parse_quote! { #[serde(skip_serializing_if = "Option::is_none")] });
|
|
|
|
Ok(Field { attrs, ty, ..f.clone() })
|
|
}
|
|
})
|
|
.collect::<syn::Result<_>>()?;
|
|
|
|
// If at least one field needs to change, generate a new struct, else use a type alias.
|
|
if field_changed {
|
|
let possibly_redacted_event_content = generate_event_content_impl(
|
|
&possibly_redacted_ident,
|
|
vis,
|
|
Some(possibly_redacted_fields.iter()),
|
|
event_type,
|
|
Some(EventKind::State),
|
|
EventKindContentVariation::PossiblyRedacted,
|
|
state_key_type,
|
|
unsigned_type,
|
|
aliases,
|
|
ruma_events,
|
|
)
|
|
.unwrap_or_else(syn::Error::into_compile_error);
|
|
|
|
let static_event_content_impl =
|
|
generate_static_event_content_impl(&possibly_redacted_ident, event_type, ruma_events);
|
|
|
|
Ok(quote! {
|
|
#[doc = #doc]
|
|
#[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
#vis struct #possibly_redacted_ident {
|
|
#( #possibly_redacted_fields, )*
|
|
}
|
|
|
|
#possibly_redacted_event_content
|
|
|
|
#static_event_content_impl
|
|
})
|
|
} else {
|
|
Ok(quote! {
|
|
#[doc = #doc]
|
|
#vis type #possibly_redacted_ident = #ident;
|
|
|
|
#[automatically_derived]
|
|
impl #ruma_events::PossiblyRedactedStateEventContent for #ident {
|
|
type StateKey = #state_key_type;
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
fn generate_event_content_without_relation<'a>(
|
|
ident: &Ident,
|
|
vis: &syn::Visibility,
|
|
fields: impl Iterator<Item = &'a Field>,
|
|
ruma_events: &TokenStream,
|
|
) -> syn::Result<TokenStream> {
|
|
let serde = quote! { #ruma_events::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)]
|
|
#vis struct #without_relation_ident #without_relation_struct
|
|
|
|
impl #without_relation_ident {
|
|
#[doc = #with_relation_fn_doc]
|
|
#vis 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,
|
|
vis: &syn::Visibility,
|
|
event_type: &str,
|
|
ruma_events: &TokenStream,
|
|
) -> syn::Result<TokenStream> {
|
|
// The redaction module has its own event types.
|
|
if ident == "RoomRedactionEventContent" {
|
|
return Ok(quote! {});
|
|
}
|
|
|
|
let ident_s = ident.to_string();
|
|
let ev_type_s = ident_s.strip_suffix("Content").ok_or_else(|| {
|
|
syn::Error::new_spanned(ident, "Expected content struct name ending in `Content`")
|
|
})?;
|
|
|
|
let type_aliases = [
|
|
EventKindVariation::None,
|
|
EventKindVariation::Sync,
|
|
EventKindVariation::Original,
|
|
EventKindVariation::OriginalSync,
|
|
EventKindVariation::Stripped,
|
|
EventKindVariation::Initial,
|
|
EventKindVariation::Redacted,
|
|
EventKindVariation::RedactedSync,
|
|
]
|
|
.iter()
|
|
.filter_map(|&var| Some((var, event_kind.to_event_ident(var).ok()?)))
|
|
.map(|(var, ev_struct)| {
|
|
let ev_type = format_ident!("{var}{ev_type_s}");
|
|
|
|
let doc_text = match var {
|
|
EventKindVariation::None | EventKindVariation::Original => "",
|
|
EventKindVariation::Sync | EventKindVariation::OriginalSync => {
|
|
" from a `sync_events` response"
|
|
}
|
|
EventKindVariation::Stripped => " from an invited room preview",
|
|
EventKindVariation::Redacted => " that has been redacted",
|
|
EventKindVariation::RedactedSync => {
|
|
" from a `sync_events` response that has been redacted"
|
|
}
|
|
EventKindVariation::Initial => " for creating a room",
|
|
};
|
|
let ev_type_doc = format!("An `{event_type}` event{doc_text}.");
|
|
|
|
let content_struct = if var.is_redacted() {
|
|
Cow::Owned(format_ident!("Redacted{ident}"))
|
|
} else if let EventKindVariation::Stripped = var {
|
|
Cow::Owned(format_ident!("PossiblyRedacted{ident}"))
|
|
} else {
|
|
Cow::Borrowed(ident)
|
|
};
|
|
|
|
quote! {
|
|
#[doc = #ev_type_doc]
|
|
#vis type #ev_type = #ruma_events::#ev_struct<#content_struct>;
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(type_aliases)
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
enum EventKindContentVariation {
|
|
Original,
|
|
Redacted,
|
|
PossiblyRedacted,
|
|
}
|
|
|
|
impl fmt::Display for EventKindContentVariation {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
EventKindContentVariation::Original => Ok(()),
|
|
EventKindContentVariation::Redacted => write!(f, "Redacted"),
|
|
EventKindContentVariation::PossiblyRedacted => write!(f, "PossiblyRedacted"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn generate_event_content_impl<'a>(
|
|
ident: &Ident,
|
|
vis: &syn::Visibility,
|
|
mut fields: Option<impl Iterator<Item = &'a Field>>,
|
|
event_type: &LitStr,
|
|
event_kind: Option<EventKind>,
|
|
variation: EventKindContentVariation,
|
|
state_key_type: Option<&TokenStream>,
|
|
unsigned_type: Option<TokenStream>,
|
|
aliases: &[LitStr],
|
|
ruma_events: &TokenStream,
|
|
) -> syn::Result<TokenStream> {
|
|
let serde = quote! { #ruma_events::exports::serde };
|
|
let serde_json = quote! { #ruma_events::exports::serde_json };
|
|
|
|
let (event_type_ty_decl, event_type_ty, event_type_fn_impl);
|
|
|
|
let type_suffix_data = event_type
|
|
.value()
|
|
.strip_suffix('*')
|
|
.map(|type_prefix| {
|
|
let Some(fields) = &mut fields else {
|
|
return Err(syn::Error::new_spanned(
|
|
event_type,
|
|
"event type with a `.*` suffix is required to be a struct",
|
|
));
|
|
};
|
|
|
|
let type_fragment_field = fields
|
|
.find_map(|f| {
|
|
f.attrs.iter().filter(|a| a.path().is_ident("ruma_event")).find_map(|attr| {
|
|
match attr.parse_args() {
|
|
Ok(EventFieldMeta::TypeFragment) => Some(Ok(f)),
|
|
Ok(_) => None,
|
|
Err(e) => Some(Err(e)),
|
|
}
|
|
})
|
|
})
|
|
.transpose()?
|
|
.ok_or_else(|| {
|
|
syn::Error::new_spanned(
|
|
event_type,
|
|
"event type with a `.*` suffix requires there to be a \
|
|
`#[ruma_event(type_fragment)]` field",
|
|
)
|
|
})?
|
|
.ident
|
|
.as_ref()
|
|
.expect("type fragment field needs to have a name");
|
|
|
|
<syn::Result<_>>::Ok((type_prefix.to_owned(), type_fragment_field))
|
|
})
|
|
.transpose()?;
|
|
|
|
match event_kind {
|
|
Some(kind) => {
|
|
let i = kind.to_event_type_enum();
|
|
event_type_ty_decl = None;
|
|
event_type_ty = quote! { #ruma_events::#i };
|
|
event_type_fn_impl = match &type_suffix_data {
|
|
Some((type_prefix, type_fragment_field)) => {
|
|
let format = type_prefix.to_owned() + "{}";
|
|
|
|
quote! {
|
|
::std::convert::From::from(::std::format!(#format, self.#type_fragment_field))
|
|
}
|
|
}
|
|
None => quote! { ::std::convert::From::from(#event_type) },
|
|
};
|
|
}
|
|
None => {
|
|
let camel_case_type_name = m_prefix_name_to_type_name(event_type)?;
|
|
let i = format_ident!("{}EventType", camel_case_type_name);
|
|
event_type_ty_decl = Some(quote! {
|
|
/// Implementation detail, you don't need to care about this.
|
|
#[doc(hidden)]
|
|
#vis struct #i {
|
|
// Set to None for intended type, Some for a different one
|
|
ty: ::std::option::Option<crate::PrivOwnedStr>,
|
|
}
|
|
|
|
impl #serde::Serialize for #i {
|
|
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
|
|
where
|
|
S: #serde::Serializer,
|
|
{
|
|
let s = self.ty.as_ref().map(|t| &t.0[..]).unwrap_or(#event_type);
|
|
serializer.serialize_str(s)
|
|
}
|
|
}
|
|
});
|
|
event_type_ty = quote! { #i };
|
|
event_type_fn_impl = quote! { #event_type_ty { ty: ::std::option::Option::None } };
|
|
}
|
|
}
|
|
|
|
let sub_trait_impl = event_kind.map(|kind| {
|
|
let trait_name = format_ident!("{variation}{kind}Content");
|
|
|
|
let state_key = (kind == EventKind::State).then(|| {
|
|
assert!(state_key_type.is_some());
|
|
|
|
quote! {
|
|
type StateKey = #state_key_type;
|
|
}
|
|
});
|
|
|
|
quote! {
|
|
#[automatically_derived]
|
|
impl #ruma_events::#trait_name for #ident {
|
|
#state_key
|
|
}
|
|
}
|
|
});
|
|
|
|
let static_state_event_content_impl = (event_kind == Some(EventKind::State)
|
|
&& variation == EventKindContentVariation::Original)
|
|
.then(|| {
|
|
let possibly_redacted_ident = format_ident!("PossiblyRedacted{ident}");
|
|
|
|
let unsigned_type = unsigned_type
|
|
.unwrap_or_else(|| quote! { #ruma_events::StateUnsigned<Self::PossiblyRedacted> });
|
|
|
|
quote! {
|
|
#[automatically_derived]
|
|
impl #ruma_events::StaticStateEventContent for #ident {
|
|
type PossiblyRedacted = #possibly_redacted_ident;
|
|
type Unsigned = #unsigned_type;
|
|
}
|
|
}
|
|
});
|
|
|
|
let event_types = aliases.iter().chain([event_type]);
|
|
|
|
let event_content_from_type_impl = type_suffix_data.map(|(_, type_fragment_field)| {
|
|
let type_prefixes = event_types.map(|ev_type| {
|
|
ev_type
|
|
.value()
|
|
.strip_suffix('*')
|
|
.expect("aliases have already been checked to have the same suffix")
|
|
.to_owned()
|
|
});
|
|
let type_prefixes = quote! {
|
|
[#(#type_prefixes,)*]
|
|
};
|
|
let fields_without_type_fragment = fields
|
|
.unwrap()
|
|
.filter(|f| {
|
|
!f.attrs.iter().any(|a| {
|
|
a.path().is_ident("ruma_event")
|
|
&& matches!(a.parse_args(), Ok(EventFieldMeta::TypeFragment))
|
|
})
|
|
})
|
|
.map(PrivateField)
|
|
.collect::<Vec<_>>();
|
|
let fields_ident_without_type_fragment =
|
|
fields_without_type_fragment.iter().filter_map(|f| f.0.ident.as_ref());
|
|
|
|
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,
|
|
)
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(quote! {
|
|
#event_type_ty_decl
|
|
|
|
#[automatically_derived]
|
|
impl #ruma_events::EventContent for #ident {
|
|
type EventType = #event_type_ty;
|
|
|
|
fn event_type(&self) -> Self::EventType {
|
|
#event_type_fn_impl
|
|
}
|
|
}
|
|
|
|
#event_content_from_type_impl
|
|
#sub_trait_impl
|
|
#static_state_event_content_impl
|
|
})
|
|
}
|
|
|
|
fn generate_static_event_content_impl(
|
|
ident: &Ident,
|
|
event_type: &LitStr,
|
|
ruma_events: &TokenStream,
|
|
) -> TokenStream {
|
|
quote! {
|
|
impl #ruma_events::StaticEventContent for #ident {
|
|
const TYPE: &'static ::std::primitive::str = #event_type;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn needs_redacted(is_custom_redacted: bool, event_kind: EventKind) -> bool {
|
|
// `is_custom` means that the content struct does not need a generated
|
|
// redacted struct also. If no `custom_redacted` attrs are found the content
|
|
// needs a redacted struct generated.
|
|
!is_custom_redacted && matches!(event_kind, EventKind::MessageLike | EventKind::State)
|
|
}
|
|
|
|
fn needs_possibly_redacted(is_custom_possibly_redacted: bool, event_kind: EventKind) -> bool {
|
|
// `is_custom_possibly_redacted` means that the content struct does not need
|
|
// a generated possibly redacted struct also. If no `custom_possibly_redacted`
|
|
// attrs are found the content needs a possibly redacted struct generated.
|
|
!is_custom_possibly_redacted && event_kind == EventKind::State
|
|
}
|