Jonas Platte 0c6dd0ba32
Simplify proc_macro tests
… by moving them from ruma-events-macros to ruma-events and updating the
macros to work in different contexts (without items like FromRaw being
available at the calling crate's root)
2020-03-17 13:10:40 +01:00

290 lines
10 KiB
Rust

//! 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("ruma_events", 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: Some(Default::default()),
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()?,
})
}
}