ruma-events-macros

This commit is contained in:
Jimmy Cuadra 2019-06-18 16:34:45 -07:00
commit 65bd8e86cc
7 changed files with 523 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

15
.travis.yml Normal file
View File

@ -0,0 +1,15 @@
language: "rust"
before_script:
- "rustup component add rustfmt"
- "rustup component add clippy"
script:
- "cargo fmt --all -- --check"
- "cargo clippy --all-targets --all-features -- -D warnings"
- "cargo build --verbose"
- "cargo test --verbose"
notifications:
email: false
irc:
channels:
- secure: "HvfXk7XMbm+iDeGoNNO886q4xMOUqJncfAxqklG6FJMCVxyrf8afyyXveCxnTH1F5oDvJXw33m6YetEj1oc7RQcB3+36XkxhjC/IzmupfD9KsikGiamL9YDrfQopvY4RXqodTR3YEof7SkFkEAzuobT0QStemX6TCkC9a7BX1QpMvEbo1pS5wlswy2G2WDbiicoiS93su73AKTQ2jOmzFdwUDZdhpNnPNJqVm5TM2Am8tj6hbX6A2y2AecRZISf8rv8LhmgpZi97NjeeK4CbsQO7G4KANGr8RA7oxlgzbW2q7FbDupB6+zLT4a4/R5GjtJoi8pvaJSL9r2GYpP4VLTYF3+tJVfLbvmQVtUjhHE4masGYfnZgpgRtiH6o+DiF/ErSE/SjJEy/S8ujqXS9mjLFtSg6nLM4k4JdCr7MLrX0buNUsv5mtmhyUvYgJtd9E+ZxLHV5TG5lF28JPMrpKrEE5UvQr/xHZh+70AwCTI5jMoSPqpBwsyQ1agxTIDmiyuo60FhVUoLyiXn25m0ZIf7v1sg4A8vFq0zA9xnhpxtZATXa7StZQn1BH2k82kuyO0hkbFhEHTv25sWJdtaFy/vmrGdchxVy7ogdOXOjXkeg+0oAnOHMsRyZlVusQ4mixM/PYet860XNcW4P6P9Nz0u5ZNmagggXSKCpCqs3smY="
use_notice: true

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
categories = ["api-bindings", "web-programming"]
description = "A procedural macro used by the ruma-events crate."
documentation = "https://docs.rs/ruma-events-macros"
edition = "2018"
homepage = "https://github.com/ruma/ruma-events-macros"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "MIT"
name = "ruma-events-macros"
readme = "README.md"
repository = "https://github.com/ruma/ruma-api-macros"
version = "0.1.0"
[dependencies]
syn = { version = "0.15.36", features = ["full"] }
quote = "0.6.12"
proc-macro2 = "0.4.30"
[lib]
proc-macro = true

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2019 Jimmy Cuadra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# ruma-events-macros
[![Build Status](https://travis-ci.org/ruma/ruma-events-macros.svg?branch=master)](https://travis-ci.org/ruma/ruma-events-macros)
**ruma-events-macros** provides a procedural macro for easily generating event types for [ruma-events](https://github.com/ruma/ruma-events).
## Documentation
ruma-events-macros has [comprehensive documentation](https://docs.rs/ruma-events-macros) available on docs.rs.
## License
[MIT](http://opensource.org/licenses/MIT)

318
src/event.rs Normal file
View File

@ -0,0 +1,318 @@
//! Details of the `ruma_event` procedural macro.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{
braced,
parse::{self, Parse, ParseStream},
punctuated::Punctuated,
token::Colon,
Attribute, Expr, Field, FieldValue, Ident, Member, Path, PathArguments, PathSegment, Token,
TypePath,
};
/// The result of processing the `ruma_event` macro, ready for output back to source code.
pub struct RumaEvent;
impl From<RumaEventInput> for RumaEvent {
// TODO: Provide an actual impl for this.
fn from(_input: RumaEventInput) -> Self {
Self
}
}
impl ToTokens for RumaEvent {
// TODO: Provide an actual impl for this.
fn to_tokens(&self, tokens: &mut TokenStream) {
let output = quote!(
pub struct Foo {}
);
output.to_tokens(tokens);
}
}
/// 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 field.
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(Ident::new("Event", Span::call_site()))
{
EventKind::Event
} else if expr_path
.path
.is_ident(Ident::new("RoomEvent", Span::call_site()))
{
EventKind::RoomEvent
} else if expr_path
.path
.is_ident(Ident::new("StateEvent", Span::call_site()))
{
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().into_value();
let mut punctuated = Punctuated::new();
punctuated.push(PathSegment {
ident: Ident::new("crate", 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: None,
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.
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.
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()?,
})
}
}

134
src/lib.rs Normal file
View File

@ -0,0 +1,134 @@
//! Crate `ruma-events-macros` provides a procedural macro for generating
//! [ruma-events](https://github.com/ruma/ruma-events) events.
//!
//! See the documentation for the `ruma_event!` macro for usage details.
//!
#![deny(
missing_copy_implementations,
missing_debug_implementations,
// missing_docs, # Uncomment when https://github.com/rust-lang/rust/pull/60562 is released.
warnings
)]
#![warn(
clippy::empty_line_after_outer_attr,
clippy::expl_impl_clone_on_copy,
clippy::if_not_else,
clippy::items_after_statements,
clippy::match_same_arms,
clippy::mem_forget,
clippy::missing_docs_in_private_items,
clippy::multiple_inherent_impl,
clippy::mut_mut,
clippy::needless_borrow,
clippy::needless_continue,
clippy::single_match_else,
clippy::unicode_not_nfc,
clippy::use_self,
clippy::used_underscore_binding,
clippy::wrong_pub_self_convention,
clippy::wrong_self_convention
)]
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::ToTokens;
use crate::event::{RumaEvent, RumaEventInput};
mod event;
// A note about the `example` modules that appears in doctests:
//
// This is necessary because otherwise the expanded code appears in function context, which makes
// the compiler interpret the output of the macro as a statement, and proc macros currently aren't
// allowed to expand to statements, resulting in a compiler error.
/// Generates a Rust type for a Matrix event.
///
/// # Examples
///
/// The most common form of event is a struct with all the standard fields for an event of its
/// kind and a struct for its `content` field:
///
/// ```rust,no_run
/// # pub mod example {
/// # use ruma_events_macros::ruma_event;
/// ruma_event! {
/// /// Informs the room about what room aliases it has been given.
/// AliasesEvent {
/// kind: StateEvent,
/// event_type: RoomAliases,
/// content: {
/// /// A list of room aliases.
/// pub aliases: Vec<RoomAliasId>,
/// }
/// }
/// }
/// # }
/// ```
///
/// Occasionally an event will have non-standard fields at its top level (outside the `content`
/// field). These extra fields are declared in block labeled with `fields`:
///
/// ```rust,no_run
/// # pub mod example {
/// # use ruma_events_macros::ruma_event;
/// ruma_event! {
/// /// A redaction of an event.
/// RedactionEvent {
/// kind: RoomEvent,
/// event_type: RoomRedaction,
/// fields: {
/// /// The ID of the event that was redacted.
/// pub redacts: EventId
/// },
/// content: {
/// /// The reason for the redaction, if any.
/// pub reason: Option<String>,
/// },
/// }
/// }
/// # }
/// ```
///
/// Sometimes the type of the `content` should be a type alias rather than a struct or enum. This
/// is designated with `content_type_alias`:
///
/// ```rust,no_run
/// # pub mod example {
/// # use ruma_events_macros::ruma_event;
/// ruma_event! {
/// /// Informs the client about the rooms that are considered direct by a user.
/// DirectEvent {
/// kind: Event,
/// event_type: Direct,
/// content_type_alias: {
/// /// The payload of a `DirectEvent`.
/// ///
/// /// A mapping of `UserId`'s to a collection of `RoomId`'s which are considered
/// /// *direct* for that particular user.
/// HashMap<UserId, Vec<RoomId>>
/// }
/// }
/// }
/// # }
/// ```
///
/// If `content` and `content_type_alias` are both supplied, the second one listed will overwrite
/// the first.
///
/// The event type and content type will have copies generated inside a private `raw` module. These
/// "raw" versions are the same, except they implement `serde::Deserialize`. An inherent method
/// called `from_str` will be generated for the event type that takes a `&str` of JSON and attempts
/// to deserialize the type using the "raw" version. If deserialization fails, an error is returned
/// to the user. If deserialization succeeds, a value of the public event type will be populated
/// from the raw version's fields and returned.
#[proc_macro]
pub fn ruma_event(input: TokenStream) -> TokenStream {
let ruma_event_input = syn::parse_macro_input!(input as RumaEventInput);
let ruma_event = RumaEvent::from(ruma_event_input);
ruma_event.into_token_stream().into()
}