commit 65bd8e86cc4a486de74dc16b0f9addad0d256152 Author: Jimmy Cuadra Date: Tue Jun 18 16:34:45 2019 -0700 ruma-events-macros diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..69369904 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..26513f5f --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..547cf9a8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = ["Jimmy Cuadra "] +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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1ea21301 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..bc81d6df --- /dev/null +++ b/README.md @@ -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) diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 00000000..40980b30 --- /dev/null +++ b/src/event.rs @@ -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 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, + + /// 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>, + + /// 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 { + 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::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), + + /// 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 { + 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::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, + + /// 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, + + /// 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, +} + +/// 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, + + /// Path to the type. + pub path: TypePath, +} + +impl Parse for Typedef { + fn parse(input: ParseStream<'_>) -> parse::Result { + let body; + braced!(body in input); + + Ok(Self { + attrs: body.call(Attribute::parse_outer)?, + path: body.parse()?, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..40948117 --- /dev/null +++ b/src/lib.rs @@ -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, +/// } +/// } +/// } +/// # } +/// ``` +/// +/// 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, +/// }, +/// } +/// } +/// # } +/// ``` +/// +/// 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> +/// } +/// } +/// } +/// # } +/// ``` +/// +/// 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() +}