From 65bd8e86cc4a486de74dc16b0f9addad0d256152 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 18 Jun 2019 16:34:45 -0700 Subject: [PATCH 01/26] ruma-events-macros --- .gitignore | 3 + .travis.yml | 15 +++ Cargo.toml | 21 ++++ LICENSE | 19 +++ README.md | 13 +++ src/event.rs | 318 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 134 ++++++++++++++++++++++ 7 files changed, 523 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/event.rs create mode 100644 src/lib.rs 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() +} From 48d6ef7eadd7e2b46fa7ddbc35ad3c4349cfe56e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 18 Jun 2019 16:59:48 -0700 Subject: [PATCH 02/26] Split parsing and generation into separate modules. --- src/gen.rs | 27 +++++++++++++++++++++++++++ src/lib.rs | 5 +++-- src/{event.rs => parse.rs} | 26 ++------------------------ 3 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 src/gen.rs rename src/{event.rs => parse.rs} (94%) diff --git a/src/gen.rs b/src/gen.rs new file mode 100644 index 00000000..30fdf61e --- /dev/null +++ b/src/gen.rs @@ -0,0 +1,27 @@ +//! Details of generating code for the `ruma_event` procedural macro. + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::parse::RumaEventInput; + +/// 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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 40948117..537e7198 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,9 +34,10 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::ToTokens; -use crate::event::{RumaEvent, RumaEventInput}; +use crate::{gen::RumaEvent, parse::RumaEventInput}; -mod event; +mod gen; +mod parse; // A note about the `example` modules that appears in doctests: // diff --git a/src/event.rs b/src/parse.rs similarity index 94% rename from src/event.rs rename to src/parse.rs index 40980b30..d4812147 100644 --- a/src/event.rs +++ b/src/parse.rs @@ -1,8 +1,7 @@ -//! Details of the `ruma_event` procedural macro. +//! Details of parsing input for the `ruma_event` procedural macro. -use proc_macro2::{Span, TokenStream}; +use proc_macro2::Span; -use quote::{quote, ToTokens}; use syn::{ braced, parse::{self, Parse, ParseStream}, @@ -12,27 +11,6 @@ use syn::{ 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. From 2f54ee3e3253a61c258ab9960397246acc2c51ec Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 18 Jun 2019 17:54:50 -0700 Subject: [PATCH 03/26] Implement generation of structs. --- Cargo.toml | 6 ++ src/gen.rs | 117 +++++++++++++++++++++++++++++++++--- src/lib.rs | 6 +- src/parse.rs | 2 +- tests/ruma_events_macros.rs | 47 +++++++++++++++ 5 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 tests/ruma_events_macros.rs diff --git a/Cargo.toml b/Cargo.toml index 547cf9a8..809f555b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,9 @@ proc-macro2 = "0.4.30" [lib] proc-macro = true + +[dev-dependencies] +ruma-identifiers = "0.13.1" +serde_json = "1.0.39" +js_int = "0.1.0" +serde = "1.0.92" diff --git a/src/gen.rs b/src/gen.rs index 30fdf61e..c84cb45f 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -1,25 +1,126 @@ //! Details of generating code for the `ruma_event` procedural macro. -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; +use syn::{Attribute, Field, Ident}; -use crate::parse::RumaEventInput; +use crate::parse::{EventKind, RumaEventInput}; /// The result of processing the `ruma_event` macro, ready for output back to source code. -pub struct RumaEvent; +pub struct RumaEvent { + /// Outer attributes on the field, such as a docstring. + attrs: Vec, + + /// The name of the type of the event's `content` field. + content_name: Ident, + + /// Additional named struct fields in the top level event struct. + fields: Option>, + + /// The kind of event. + kind: EventKind, + + /// The name of the event. + name: Ident, +} + +impl RumaEvent { + /// Fills in the event's struct definition with fields common to all basic events. + fn common_basic_event_fields(&self) -> TokenStream { + let content_name = &self.content_name; + + quote! { + /// The event's content. + pub content: #content_name, + } + } + + /// Fills in the event's struct definition with fields common to all room events. + fn common_room_event_fields(&self) -> TokenStream { + let common_basic_event_fields = self.common_basic_event_fields(); + + quote! { + #common_basic_event_fields + + /// The unique identifier for the event. + pub event_id: ruma_identifiers::EventId, + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this + /// event was sent. + pub origin_server_ts: js_int::UInt, + + /// The unique identifier for the room associated with this event. + pub room_id: Option, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: Option, + + /// The unique identifier for the user who sent this event. + pub sender: ruma_identifiers::UserId, + } + } + + /// Fills in the event's struct definition with fields common to all state events. + fn common_state_event_fields(&self) -> TokenStream { + let content_name = &self.content_name; + let common_room_event_fields = self.common_room_event_fields(); + + quote! { + #common_room_event_fields + + /// The previous content for this state key, if any. + pub prev_content: Option<#content_name>, + + /// A key that determines which piece of room state the event represents. + pub state_key: String, + } + } +} impl From for RumaEvent { - // TODO: Provide an actual impl for this. - fn from(_input: RumaEventInput) -> Self { - Self + fn from(input: RumaEventInput) -> Self { + Self { + attrs: input.attrs, + content_name: Ident::new(&format!("{}Content", input.name), Span::call_site()), + fields: input.fields, + kind: input.kind, + name: input.name, + } } } impl ToTokens for RumaEvent { - // TODO: Provide an actual impl for this. fn to_tokens(&self, tokens: &mut TokenStream) { + let attrs = &self.attrs; + let content_name = &self.content_name; + + let common_event_fields = match self.kind { + EventKind::Event => self.common_basic_event_fields(), + EventKind::RoomEvent => self.common_room_event_fields(), + EventKind::StateEvent => self.common_state_event_fields(), + }; + + let event_fields = match &self.fields { + Some(fields) => fields.clone(), + None => vec![], + }; + + let name = &self.name; + + let content_docstring = format!("The payload for `{}`.", name); + let output = quote!( - pub struct Foo {} + #(#attrs),* + #[derive(Clone, Debug)] + pub struct #name { + #common_event_fields + #(#event_fields),* + } + + #[doc = #content_docstring] + #[derive(Clone, Debug)] + pub struct #content_name { + } ); output.to_tokens(tokens); diff --git a/src/lib.rs b/src/lib.rs index 537e7198..ca782e54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ mod parse; /// event_type: RoomAliases, /// content: { /// /// A list of room aliases. -/// pub aliases: Vec, +/// pub aliases: Vec, /// } /// } /// } @@ -82,7 +82,7 @@ mod parse; /// event_type: RoomRedaction, /// fields: { /// /// The ID of the event that was redacted. -/// pub redacts: EventId +/// pub redacts: ruma_identifiers::EventId /// }, /// content: { /// /// The reason for the redaction, if any. @@ -109,7 +109,7 @@ mod parse; /// /// /// /// A mapping of `UserId`'s to a collection of `RoomId`'s which are considered /// /// *direct* for that particular user. -/// HashMap> +/// HashMap> /// } /// } /// } diff --git a/src/parse.rs b/src/parse.rs index d4812147..5077191c 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -16,7 +16,7 @@ pub struct RumaEventInput { /// Outer attributes on the field, such as a docstring. pub attrs: Vec, - /// The name of the field. + /// The name of the event. pub name: Ident, /// The kind of event, determiend by the `kind` field. diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs new file mode 100644 index 00000000..bb2e1dce --- /dev/null +++ b/tests/ruma_events_macros.rs @@ -0,0 +1,47 @@ +// See note about wrapping macro expansion in a module from `src/lib.rs` +pub mod tests { + 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, + } + } + } + + ruma_event! { + /// A redaction of an event. + RedactionEvent { + kind: RoomEvent, + event_type: RoomRedaction, + fields: { + /// The ID of the event that was redacted. + pub redacts: ruma_identifiers::EventId + }, + content: { + /// The reason for the redaction, if any. + pub reason: Option, + }, + } + } + + 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. + std::collections::HashMap> + } + } + } +} From a5834ea192dc9e6107767c8112f301c06c2998cb Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 15:40:29 -0700 Subject: [PATCH 04/26] Combine and sort common and custom fields using a Vec. --- src/gen.rs | 195 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 124 insertions(+), 71 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index c84cb45f..42f807ef 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -2,7 +2,12 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; -use syn::{Attribute, Field, Ident}; +use syn::{ + parse::{self, Parse, ParseStream}, + parse_quote, + punctuated::Punctuated, + Attribute, Field, Ident, Token, +}; use crate::parse::{EventKind, RumaEventInput}; @@ -14,77 +19,44 @@ pub struct RumaEvent { /// The name of the type of the event's `content` field. content_name: Ident, - /// Additional named struct fields in the top level event struct. - fields: Option>, + /// Struct fields of the event. + fields: Vec, /// The kind of event. + #[allow(dead_code)] kind: EventKind, /// The name of the event. name: Ident, } -impl RumaEvent { - /// Fills in the event's struct definition with fields common to all basic events. - fn common_basic_event_fields(&self) -> TokenStream { - let content_name = &self.content_name; - - quote! { - /// The event's content. - pub content: #content_name, - } - } - - /// Fills in the event's struct definition with fields common to all room events. - fn common_room_event_fields(&self) -> TokenStream { - let common_basic_event_fields = self.common_basic_event_fields(); - - quote! { - #common_basic_event_fields - - /// The unique identifier for the event. - pub event_id: ruma_identifiers::EventId, - - /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this - /// event was sent. - pub origin_server_ts: js_int::UInt, - - /// The unique identifier for the room associated with this event. - pub room_id: Option, - - /// Additional key-value pairs not signed by the homeserver. - pub unsigned: Option, - - /// The unique identifier for the user who sent this event. - pub sender: ruma_identifiers::UserId, - } - } - - /// Fills in the event's struct definition with fields common to all state events. - fn common_state_event_fields(&self) -> TokenStream { - let content_name = &self.content_name; - let common_room_event_fields = self.common_room_event_fields(); - - quote! { - #common_room_event_fields - - /// The previous content for this state key, if any. - pub prev_content: Option<#content_name>, - - /// A key that determines which piece of room state the event represents. - pub state_key: String, - } - } -} - impl From for RumaEvent { fn from(input: RumaEventInput) -> Self { + let kind = input.kind; + let name = input.name; + let content_name = Ident::new(&format!("{}Content", &name), Span::call_site()); + + let mut fields = match kind { + EventKind::Event => { + populate_event_fields(content_name.clone(), input.fields.unwrap_or_else(Vec::new)) + } + EventKind::RoomEvent => populate_room_event_fields( + content_name.clone(), + input.fields.unwrap_or_else(Vec::new), + ), + EventKind::StateEvent => { + populate_state_fields(content_name.clone(), input.fields.unwrap_or_else(Vec::new)) + } + }; + + fields.sort_unstable_by_key(|field| field.ident.clone().unwrap()); + Self { attrs: input.attrs, - content_name: Ident::new(&format!("{}Content", input.name), Span::call_site()), - fields: input.fields, - kind: input.kind, - name: input.name, + content_name, + fields, + kind, + name, } } } @@ -94,16 +66,7 @@ impl ToTokens for RumaEvent { let attrs = &self.attrs; let content_name = &self.content_name; - let common_event_fields = match self.kind { - EventKind::Event => self.common_basic_event_fields(), - EventKind::RoomEvent => self.common_room_event_fields(), - EventKind::StateEvent => self.common_state_event_fields(), - }; - - let event_fields = match &self.fields { - Some(fields) => fields.clone(), - None => vec![], - }; + let event_fields = &self.fields; let name = &self.name; @@ -113,7 +76,6 @@ impl ToTokens for RumaEvent { #(#attrs),* #[derive(Clone, Debug)] pub struct #name { - #common_event_fields #(#event_fields),* } @@ -126,3 +88,94 @@ impl ToTokens for RumaEvent { output.to_tokens(tokens); } } + +/// Fills in the event's struct definition with fields common to all basic events. +fn populate_event_fields(content_name: Ident, mut fields: Vec) -> Vec { + let punctuated_fields: Punctuated = parse_quote! { + /// The event's content. + pub content: #content_name, + }; + + let mut additional_fields = Vec::with_capacity(punctuated_fields.len()); + + for punctuated_field in punctuated_fields { + additional_fields.push(punctuated_field.field); + } + + fields.extend(additional_fields); + + fields +} + +/// Fills in the event's struct definition with fields common to all room events. +fn populate_room_event_fields(content_name: Ident, fields: Vec) -> Vec { + let mut fields = populate_event_fields(content_name, fields); + + let punctuated_fields: Punctuated = parse_quote! { + /// The unique identifier for the event. + pub event_id: ruma_identifiers::EventId, + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this + /// event was sent. + pub origin_server_ts: js_int::UInt, + + /// The unique identifier for the room associated with this event. + pub room_id: Option, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: Option, + + /// The unique identifier for the user who sent this event. + pub sender: ruma_identifiers::UserId, + }; + + let mut additional_fields = Vec::with_capacity(punctuated_fields.len()); + + for punctuated_field in punctuated_fields { + additional_fields.push(punctuated_field.field); + } + + fields.extend(additional_fields); + + fields +} + +/// Fills in the event's struct definition with fields common to all state events. +fn populate_state_fields(content_name: Ident, fields: Vec) -> Vec { + let mut fields = populate_room_event_fields(content_name.clone(), fields); + + let punctuated_fields: Punctuated = parse_quote! { + /// The previous content for this state key, if any. + pub prev_content: Option<#content_name>, + + /// A key that determines which piece of room state the event represents. + pub state_key: String, + }; + + let mut additional_fields = Vec::with_capacity(punctuated_fields.len()); + + for punctuated_field in punctuated_fields { + additional_fields.push(punctuated_field.field); + } + + fields.extend(additional_fields); + + fields +} + +/// A wrapper around `syn::Field` that makes it possible to parse `Punctuated` +/// from a `TokenStream`. +/// +/// See https://github.com/dtolnay/syn/issues/651 for more context. +struct ParsableNamedField { + /// The wrapped `Field`. + pub field: Field, +} + +impl Parse for ParsableNamedField { + fn parse(input: ParseStream<'_>) -> parse::Result { + let field = Field::parse_named(input)?; + + Ok(Self { field }) + } +} From 3b65905784732965437e0eaea989dc9da6f676de Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 16:08:43 -0700 Subject: [PATCH 05/26] Generate content types and raw module. --- Cargo.toml | 4 +-- src/gen.rs | 58 +++++++++++++++++++++++++++++++++---- src/lib.rs | 2 +- tests/ruma_events_macros.rs | 10 ++++++- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 809f555b..c1300d28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,5 +23,5 @@ proc-macro = true [dev-dependencies] ruma-identifiers = "0.13.1" serde_json = "1.0.39" -js_int = "0.1.0" -serde = "1.0.92" +js_int = { version = "0.1.0", features = ["serde"] } +serde = { version = "1.0.92", features = ["derive"] } diff --git a/src/gen.rs b/src/gen.rs index 42f807ef..5ed8defc 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -9,13 +9,16 @@ use syn::{ Attribute, Field, Ident, Token, }; -use crate::parse::{EventKind, RumaEventInput}; +use crate::parse::{Content, EventKind, RumaEventInput}; /// The result of processing the `ruma_event` macro, ready for output back to source code. pub struct RumaEvent { /// Outer attributes on the field, such as a docstring. attrs: Vec, + /// Information for generating the type used for the event's `content` field. + content: Content, + /// The name of the type of the event's `content` field. content_name: Ident, @@ -53,6 +56,7 @@ impl From for RumaEvent { Self { attrs: input.attrs, + content: input.content, content_name, fields, kind, @@ -72,16 +76,60 @@ impl ToTokens for RumaEvent { let content_docstring = format!("The payload for `{}`.", name); + let content = match &self.content { + Content::Struct(fields) => { + quote! { + #[doc = #content_docstring] + #[derive(Clone, Debug)] + pub struct #content_name { + #(#fields),* + } + } + } + Content::Typedef(typedef) => { + let content_attrs = &typedef.attrs; + let path = &typedef.path; + + quote! { + #(#content_attrs)* + pub type #content_name = #path; + } + } + }; + + let raw_content = match &self.content { + Content::Struct(fields) => { + quote! { + #[doc = #content_docstring] + #[derive(Clone, Debug, serde::Deserialize)] + pub struct #content_name { + #(#fields),* + } + } + } + Content::Typedef(_) => TokenStream::new(), + }; + let output = quote!( - #(#attrs),* + #(#attrs)* #[derive(Clone, Debug)] pub struct #name { #(#event_fields),* } - #[doc = #content_docstring] - #[derive(Clone, Debug)] - pub struct #content_name { + #content + + /// "Raw" versions of the event and its content which implement `serde::Deserialize`. + mod raw { + use super::*; + + #(#attrs)* + #[derive(Clone, Debug, serde::Deserialize)] + pub struct #name { + #(#event_fields),* + } + + #raw_content } ); diff --git a/src/lib.rs b/src/lib.rs index ca782e54..91ae9450 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,7 +109,7 @@ mod parse; /// /// /// /// A mapping of `UserId`'s to a collection of `RoomId`'s which are considered /// /// *direct* for that particular user. -/// HashMap> +/// std::collections::HashMap> /// } /// } /// } diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index bb2e1dce..8716e0e7 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -1,5 +1,5 @@ // See note about wrapping macro expansion in a module from `src/lib.rs` -pub mod tests { +pub mod common_case { use ruma_events_macros::ruma_event; ruma_event! { @@ -13,6 +13,10 @@ pub mod tests { } } } +} + +pub mod extra_fields { + use ruma_events_macros::ruma_event; ruma_event! { /// A redaction of an event. @@ -29,6 +33,10 @@ pub mod tests { }, } } +} + +pub mod type_alias { + use ruma_events_macros::ruma_event; ruma_event! { /// Informs the client about the rooms that are considered direct by a user. From 78e25552fbdcd551478e21d88657fc2fc206ff80 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 16:52:49 -0700 Subject: [PATCH 06/26] Implement Serialize and Event. --- src/gen.rs | 58 +++++++++++++++++++++++++++++++++---- src/lib.rs | 7 +++-- tests/ruma_events_macros.rs | 43 +++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 5ed8defc..6971fccf 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -6,7 +6,7 @@ use syn::{ parse::{self, Parse, ParseStream}, parse_quote, punctuated::Punctuated, - Attribute, Field, Ident, Token, + Attribute, Field, Ident, Path, Token, }; use crate::parse::{Content, EventKind, RumaEventInput}; @@ -22,6 +22,10 @@ pub struct RumaEvent { /// The name of the type of the event's `content` field. content_name: Ident, + /// The variant of `ruma_events::EventType` for this event, determined by the `event_type` + /// field. + event_type: Path, + /// Struct fields of the event. fields: Vec, @@ -58,6 +62,7 @@ impl From for RumaEvent { attrs: input.attrs, content: input.content, content_name, + event_type: input.event_type, fields, kind, name, @@ -69,18 +74,17 @@ impl ToTokens for RumaEvent { fn to_tokens(&self, tokens: &mut TokenStream) { let attrs = &self.attrs; let content_name = &self.content_name; - let event_fields = &self.fields; - + let event_type = &self.event_type; let name = &self.name; - + let name_str = format!("{}", name); let content_docstring = format!("The payload for `{}`.", name); let content = match &self.content { Content::Struct(fields) => { quote! { #[doc = #content_docstring] - #[derive(Clone, Debug)] + #[derive(Clone, Debug, serde::Serialize)] pub struct #content_name { #(#fields),* } @@ -110,6 +114,21 @@ impl ToTokens for RumaEvent { Content::Typedef(_) => TokenStream::new(), }; + let field_count = event_fields.len() + 1; // + 1 because of manually adding `event_type` + + let mut serialize_field_calls: Vec = Vec::with_capacity(event_fields.len()); + + for field in event_fields { + let ident = field.ident.clone().unwrap(); + let ident_str = format!("{}", ident); + + let token_stream = quote! { + state.serialize_field(#ident_str, &self.#ident)?; + }; + + serialize_field_calls.push(token_stream); + } + let output = quote!( #(#attrs)* #[derive(Clone, Debug)] @@ -119,6 +138,35 @@ impl ToTokens for RumaEvent { #content + use serde::ser::SerializeStruct as _; + + impl serde::Serialize for #name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer + { + let mut state = serializer.serialize_struct(#name_str, #field_count)?; + + #(#serialize_field_calls)* + state.serialize_field("type", &self.event_type())?; + + state.end() + } + } + + impl crate::Event for #name { + /// The type of the event. + const EVENT_TYPE: crate::EventType = #event_type; + + /// The type of this event's `content` field. + type Content = #content_name; + + /// The event's content. + fn content(&self) -> &Self::Content { + &self.content + } + } + /// "Raw" versions of the event and its content which implement `serde::Deserialize`. mod raw { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 91ae9450..c0faab46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ clippy::wrong_pub_self_convention, clippy::wrong_self_convention )] +#![recursion_limit = "128"] extern crate proc_macro; @@ -52,7 +53,7 @@ mod parse; /// 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 +/// ```ignore /// # pub mod example { /// # use ruma_events_macros::ruma_event; /// ruma_event! { @@ -72,7 +73,7 @@ mod parse; /// 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 +/// ```ignore /// # pub mod example { /// # use ruma_events_macros::ruma_event; /// ruma_event! { @@ -96,7 +97,7 @@ mod parse; /// 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 +/// ```ignore /// # pub mod example { /// # use ruma_events_macros::ruma_event; /// ruma_event! { diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index 8716e0e7..fa077be8 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -1,5 +1,44 @@ +use std::fmt::Debug; + +use serde::{Deserialize, Serialize}; + +/// The type of an event. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum EventType { + /// m.direct + Direct, + + /// m.room.aliases + RoomAliases, + + /// m.room.redaction + RoomRedaction, +} + +/// A basic event. +pub trait Event +where + Self: Debug + Serialize, +{ + /// The type of the event. + const EVENT_TYPE: EventType; + + /// The type of this event's `content` field. + type Content: Debug + Serialize; + + /// The event's content. + fn content(&self) -> &Self::Content; + + /// The type of the event. + fn event_type(&self) -> EventType { + Self::EVENT_TYPE + } +} + // See note about wrapping macro expansion in a module from `src/lib.rs` pub mod common_case { + use super::Event; + use ruma_events_macros::ruma_event; ruma_event! { @@ -16,6 +55,8 @@ pub mod common_case { } pub mod extra_fields { + use super::Event; + use ruma_events_macros::ruma_event; ruma_event! { @@ -36,6 +77,8 @@ pub mod extra_fields { } pub mod type_alias { + use super::Event; + use ruma_events_macros::ruma_event; ruma_event! { From be0f1d036327bf60fb13108782364508a55bb54c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 21:14:49 -0700 Subject: [PATCH 07/26] Add inherent impl with from_str method. --- src/gen.rs | 85 ++++++++++++++++++++++++++++++++++++- tests/ruma_events_macros.rs | 8 ++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 6971fccf..894d96db 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -71,6 +71,9 @@ impl From for RumaEvent { } impl ToTokens for RumaEvent { + // TODO: Maybe break this off into functions so it's not so large. Then remove the clippy + // allowance. + #[allow(clippy::cognitive_complexity)] fn to_tokens(&self, tokens: &mut TokenStream) { let attrs = &self.attrs; let content_name = &self.content_name; @@ -116,17 +119,84 @@ impl ToTokens for RumaEvent { let field_count = event_fields.len() + 1; // + 1 because of manually adding `event_type` + let mut from_str_field_values: Vec = Vec::with_capacity(event_fields.len()); let mut serialize_field_calls: Vec = Vec::with_capacity(event_fields.len()); for field in event_fields { let ident = field.ident.clone().unwrap(); let ident_str = format!("{}", ident); - let token_stream = quote! { + let from_str_field_value = if ident == "content" { + match &self.content { + Content::Struct(content_fields) => { + let mut content_field_values: Vec = + Vec::with_capacity(content_fields.len()); + + for content_field in content_fields { + let content_field_ident = content_field.ident.clone().unwrap(); + + let token_stream = quote! { + #content_field_ident: raw.content.#content_field_ident, + }; + + content_field_values.push(token_stream); + } + + quote! { + content: #content_name { + #(#content_field_values),* + }, + } + } + Content::Typedef(_) => { + quote! { + content: raw.content, + } + } + } + } else if ident == "prev_content" { + match &self.content { + Content::Struct(content_fields) => { + let mut content_field_values: Vec = + Vec::with_capacity(content_fields.len()); + + for content_field in content_fields { + let content_field_ident = content_field.ident.clone().unwrap(); + + let token_stream = quote! { + #content_field_ident: prev.#content_field_ident, + }; + + content_field_values.push(token_stream); + } + + quote! { + prev_content: raw.prev_content.map(|prev| { + #content_name { + #(#content_field_values),* + } + }), + } + } + Content::Typedef(_) => { + quote! { + content: raw.content, + } + } + } + } else { + quote! { + #ident: raw.#ident, + } + }; + + from_str_field_values.push(from_str_field_value); + + let serialize_field_call = quote! { state.serialize_field(#ident_str, &self.#ident)?; }; - serialize_field_calls.push(token_stream); + serialize_field_calls.push(serialize_field_call); } let output = quote!( @@ -138,6 +208,17 @@ impl ToTokens for RumaEvent { #content + impl #name { + /// Attempt to create `Self` from parsing a string of JSON data. + pub fn from_str(json: &str) -> Result { + let raw = serde_json::from_str::(json)?; + + Ok(Self { + #(#from_str_field_values)* + }) + } + } + use serde::ser::SerializeStruct as _; impl serde::Serialize for #name { diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index fa077be8..e8257c6d 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -35,6 +35,14 @@ where } } +pub struct InvalidEvent; + +impl From for InvalidEvent { + fn from(_: serde_json::Error) -> Self { + Self + } +} + // See note about wrapping macro expansion in a module from `src/lib.rs` pub mod common_case { use super::Event; From 4423275ce27b7d26b5418d4fda5bd74c39bd9697 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 22:40:58 -0700 Subject: [PATCH 08/26] Implement RoomEvent and StateEvent when applicable. --- src/gen.rs | 61 ++++++++++++++++++++++++++++++++++++- src/parse.rs | 1 + tests/ruma_events_macros.rs | 31 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/gen.rs b/src/gen.rs index 894d96db..93c858e3 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -30,7 +30,6 @@ pub struct RumaEvent { fields: Vec, /// The kind of event. - #[allow(dead_code)] kind: EventKind, /// The name of the event. @@ -199,6 +198,62 @@ impl ToTokens for RumaEvent { serialize_field_calls.push(serialize_field_call); } + let impl_room_event = match self.kind { + EventKind::RoomEvent | EventKind::StateEvent => { + quote! { + impl crate::RoomEvent for #name { + /// The unique identifier for the event. + fn event_id(&self) -> &ruma_identifiers::EventId { + &self.event_id + } + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this event was + /// sent. + fn origin_server_ts(&self) -> js_int::UInt { + self.origin_server_ts + } + + /// The unique identifier for the room associated with this event. + /// + /// This can be `None` if the event came from a context where there is + /// no ambiguity which room it belongs to, like a `/sync` response for example. + fn room_id(&self) -> Option<&ruma_identifiers::RoomId> { + self.room_id.as_ref() + } + + /// The unique identifier for the user who sent this event. + fn sender(&self) -> &ruma_identifiers::UserId { + &self.sender + } + + /// Additional key-value pairs not signed by the homeserver. + fn unsigned(&self) -> Option<&serde_json::Value> { + self.unsigned.as_ref() + } + } + } + } + _ => TokenStream::new(), + }; + + let impl_state_event = if self.kind == EventKind::StateEvent { + quote! { + impl crate::StateEvent for #name { + /// The previous content for this state key, if any. + fn prev_content(&self) -> Option<&Self::Content> { + self.prev_content.as_ref() + } + + /// A key that determines which piece of room state the event represents. + fn state_key(&self) -> &str { + &self.state_key + } + } + } + } else { + TokenStream::new() + }; + let output = quote!( #(#attrs)* #[derive(Clone, Debug)] @@ -248,6 +303,10 @@ impl ToTokens for RumaEvent { } } + #impl_room_event + + #impl_state_event + /// "Raw" versions of the event and its content which implement `serde::Deserialize`. mod raw { use super::*; diff --git a/src/parse.rs b/src/parse.rs index 5077191c..266f8756 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -167,6 +167,7 @@ impl Parse for RumaEventInput { /// 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, diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index e8257c6d..c3be78f7 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -35,6 +35,37 @@ where } } +/// An event within the context of a room. +pub trait RoomEvent: Event { + /// The unique identifier for the event. + fn event_id(&self) -> &ruma_identifiers::EventId; + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this event was + /// sent. + fn origin_server_ts(&self) -> js_int::UInt; + + /// The unique identifier for the room associated with this event. + /// + /// This can be `None` if the event came from a context where there is + /// no ambiguity which room it belongs to, like a `/sync` response for example. + fn room_id(&self) -> Option<&ruma_identifiers::RoomId>; + + /// The unique identifier for the user who sent this event. + fn sender(&self) -> &ruma_identifiers::UserId; + + /// Additional key-value pairs not signed by the homeserver. + fn unsigned(&self) -> Option<&serde_json::Value>; +} + +/// An event that describes persistent state about a room. +pub trait StateEvent: RoomEvent { + /// The previous content for this state key, if any. + fn prev_content(&self) -> Option<&Self::Content>; + + /// A key that determines which piece of room state the event represents. + fn state_key(&self) -> &str; +} + pub struct InvalidEvent; impl From for InvalidEvent { From f663c792504852192658a0e9487a328b923413c9 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 23:36:01 -0700 Subject: [PATCH 09/26] Remove extra commas from generated code. --- src/gen.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 93c858e3..204bda9e 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -143,7 +143,7 @@ impl ToTokens for RumaEvent { quote! { content: #content_name { - #(#content_field_values),* + #(#content_field_values)* }, } } @@ -172,7 +172,7 @@ impl ToTokens for RumaEvent { quote! { prev_content: raw.prev_content.map(|prev| { #content_name { - #(#content_field_values),* + #(#content_field_values)* } }), } From 52754f617c21de1b1c4b5d2ac292da0889852ccd Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 23:36:16 -0700 Subject: [PATCH 10/26] Import Event trait in Serialize impl so event_type can be called. --- src/gen.rs | 2 ++ tests/ruma_events_macros.rs | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 204bda9e..8f93e4b6 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -281,6 +281,8 @@ impl ToTokens for RumaEvent { where S: serde::Serializer { + use crate::Event as _; + let mut state = serializer.serialize_struct(#name_str, #field_count)?; #(#serialize_field_calls)* diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index c3be78f7..14b74815 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -76,8 +76,6 @@ impl From for InvalidEvent { // See note about wrapping macro expansion in a module from `src/lib.rs` pub mod common_case { - use super::Event; - use ruma_events_macros::ruma_event; ruma_event! { @@ -94,8 +92,6 @@ pub mod common_case { } pub mod extra_fields { - use super::Event; - use ruma_events_macros::ruma_event; ruma_event! { @@ -116,8 +112,6 @@ pub mod extra_fields { } pub mod type_alias { - use super::Event; - use ruma_events_macros::ruma_event; ruma_event! { From 44a13e6515eeebb0f452c283c37d42b8aa0b3a66 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 19 Jun 2019 23:50:06 -0700 Subject: [PATCH 11/26] Add span information to token streams when possible. --- src/gen.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 8f93e4b6..ac153b98 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -1,11 +1,12 @@ //! Details of generating code for the `ruma_event` procedural macro. use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; use syn::{ parse::{self, Parse, ParseStream}, parse_quote, punctuated::Punctuated, + spanned::Spanned, Attribute, Field, Ident, Path, Token, }; @@ -124,6 +125,7 @@ impl ToTokens for RumaEvent { for field in event_fields { let ident = field.ident.clone().unwrap(); let ident_str = format!("{}", ident); + let span = field.span(); let from_str_field_value = if ident == "content" { match &self.content { @@ -133,22 +135,23 @@ impl ToTokens for RumaEvent { for content_field in content_fields { let content_field_ident = content_field.ident.clone().unwrap(); + let span = content_field.span(); - let token_stream = quote! { + let token_stream = quote_spanned! {span=> #content_field_ident: raw.content.#content_field_ident, }; content_field_values.push(token_stream); } - quote! { + quote_spanned! {span=> content: #content_name { #(#content_field_values)* }, } } Content::Typedef(_) => { - quote! { + quote_spanned! {span=> content: raw.content, } } @@ -161,15 +164,16 @@ impl ToTokens for RumaEvent { for content_field in content_fields { let content_field_ident = content_field.ident.clone().unwrap(); + let span = content_field.span(); - let token_stream = quote! { + let token_stream = quote_spanned! {span=> #content_field_ident: prev.#content_field_ident, }; content_field_values.push(token_stream); } - quote! { + quote_spanned! {span=> prev_content: raw.prev_content.map(|prev| { #content_name { #(#content_field_values)* @@ -178,20 +182,20 @@ impl ToTokens for RumaEvent { } } Content::Typedef(_) => { - quote! { + quote_spanned! {span=> content: raw.content, } } } } else { - quote! { + quote_spanned! {span=> #ident: raw.#ident, } }; from_str_field_values.push(from_str_field_value); - let serialize_field_call = quote! { + let serialize_field_call = quote_spanned! {span=> state.serialize_field(#ident_str, &self.#ident)?; }; From 553d9c05cdf89df78a11fbef5cc24634f518f213 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 20 Jun 2019 16:53:35 -0700 Subject: [PATCH 12/26] Add support for events with custom types. --- src/gen.rs | 130 ++++++++++++++++++++++++++++-------- tests/ruma_events_macros.rs | 29 ++++++-- 2 files changed, 124 insertions(+), 35 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index ac153b98..5a9dd640 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -30,6 +30,9 @@ pub struct RumaEvent { /// Struct fields of the event. fields: Vec, + /// Whether or not the event type is `EventType::Custom`. + is_custom: bool, + /// The kind of event. kind: EventKind, @@ -42,18 +45,25 @@ impl From for RumaEvent { let kind = input.kind; let name = input.name; let content_name = Ident::new(&format!("{}Content", &name), Span::call_site()); + let event_type = input.event_type; + let is_custom = is_custom_event_type(&event_type); let mut fields = match kind { - EventKind::Event => { - populate_event_fields(content_name.clone(), input.fields.unwrap_or_else(Vec::new)) - } - EventKind::RoomEvent => populate_room_event_fields( + EventKind::Event => populate_event_fields( + is_custom, + content_name.clone(), + input.fields.unwrap_or_else(Vec::new), + ), + EventKind::RoomEvent => populate_room_event_fields( + is_custom, + content_name.clone(), + input.fields.unwrap_or_else(Vec::new), + ), + EventKind::StateEvent => populate_state_fields( + is_custom, content_name.clone(), input.fields.unwrap_or_else(Vec::new), ), - EventKind::StateEvent => { - populate_state_fields(content_name.clone(), input.fields.unwrap_or_else(Vec::new)) - } }; fields.sort_unstable_by_key(|field| field.ident.clone().unwrap()); @@ -62,8 +72,9 @@ impl From for RumaEvent { attrs: input.attrs, content: input.content, content_name, - event_type: input.event_type, + event_type, fields, + is_custom, kind, name, } @@ -78,7 +89,19 @@ impl ToTokens for RumaEvent { let attrs = &self.attrs; let content_name = &self.content_name; let event_fields = &self.fields; - let event_type = &self.event_type; + + let event_type = if self.is_custom { + quote! { + crate::EventType::Custom(self.event_type.clone()) + } + } else { + let event_type = &self.event_type; + + quote! { + #event_type + } + }; + let name = &self.name; let name_str = format!("{}", name); let content_docstring = format!("The payload for `{}`.", name); @@ -87,7 +110,7 @@ impl ToTokens for RumaEvent { Content::Struct(fields) => { quote! { #[doc = #content_docstring] - #[derive(Clone, Debug, serde::Serialize)] + #[derive(Clone, Debug, PartialEq, serde::Serialize)] pub struct #content_name { #(#fields),* } @@ -108,7 +131,7 @@ impl ToTokens for RumaEvent { Content::Struct(fields) => { quote! { #[doc = #content_docstring] - #[derive(Clone, Debug, serde::Deserialize)] + #[derive(Clone, Debug, PartialEq, serde::Deserialize)] pub struct #content_name { #(#fields),* } @@ -117,14 +140,23 @@ impl ToTokens for RumaEvent { Content::Typedef(_) => TokenStream::new(), }; - let field_count = event_fields.len() + 1; // + 1 because of manually adding `event_type` + // Custom events will already have an event_type field. All other events need to account + // for this field being manually inserted in `Serialize` impls. + let event_type_field_count = if self.is_custom { 0 } else { 1 }; + let field_count = event_fields.len() + event_type_field_count; let mut from_str_field_values: Vec = Vec::with_capacity(event_fields.len()); let mut serialize_field_calls: Vec = Vec::with_capacity(event_fields.len()); for field in event_fields { let ident = field.ident.clone().unwrap(); - let ident_str = format!("{}", ident); + + let ident_str = if ident == "event_type" { + "type".to_string() + } else { + format!("{}", ident) + }; + let span = field.span(); let from_str_field_value = if ident == "content" { @@ -202,6 +234,23 @@ impl ToTokens for RumaEvent { serialize_field_calls.push(serialize_field_call); } + let (manually_serialize_type_field, import_event_in_serialize_impl) = if self.is_custom { + (TokenStream::new(), TokenStream::new()) + } else { + let manually_serialize_type_field = quote! { + state.serialize_field("type", &self.event_type())?; + }; + + let import_event_in_serialize_impl = quote! { + use crate::Event as _; + }; + + ( + manually_serialize_type_field, + import_event_in_serialize_impl, + ) + }; + let impl_room_event = match self.kind { EventKind::RoomEvent | EventKind::StateEvent => { quote! { @@ -260,7 +309,7 @@ impl ToTokens for RumaEvent { let output = quote!( #(#attrs)* - #[derive(Clone, Debug)] + #[derive(Clone, PartialEq, Debug)] pub struct #name { #(#event_fields),* } @@ -285,21 +334,18 @@ impl ToTokens for RumaEvent { where S: serde::Serializer { - use crate::Event as _; + #import_event_in_serialize_impl let mut state = serializer.serialize_struct(#name_str, #field_count)?; #(#serialize_field_calls)* - state.serialize_field("type", &self.event_type())?; + #manually_serialize_type_field state.end() } } impl crate::Event for #name { - /// The type of the event. - const EVENT_TYPE: crate::EventType = #event_type; - /// The type of this event's `content` field. type Content = #content_name; @@ -307,6 +353,11 @@ impl ToTokens for RumaEvent { fn content(&self) -> &Self::Content { &self.content } + + /// The type of the event. + fn event_type(&self) -> crate::EventType { + #event_type + } } #impl_room_event @@ -318,7 +369,7 @@ impl ToTokens for RumaEvent { use super::*; #(#attrs)* - #[derive(Clone, Debug, serde::Deserialize)] + #[derive(Clone, Debug, PartialEq, serde::Deserialize)] pub struct #name { #(#event_fields),* } @@ -332,10 +383,24 @@ impl ToTokens for RumaEvent { } /// Fills in the event's struct definition with fields common to all basic events. -fn populate_event_fields(content_name: Ident, mut fields: Vec) -> Vec { - let punctuated_fields: Punctuated = parse_quote! { - /// The event's content. - pub content: #content_name, +fn populate_event_fields( + is_custom: bool, + content_name: Ident, + mut fields: Vec, +) -> Vec { + let punctuated_fields: Punctuated = if is_custom { + parse_quote! { + /// The event's content. + pub content: #content_name, + + /// The custom type of the event. + pub event_type: String, + } + } else { + parse_quote! { + /// The event's content. + pub content: #content_name, + } }; let mut additional_fields = Vec::with_capacity(punctuated_fields.len()); @@ -350,8 +415,12 @@ fn populate_event_fields(content_name: Ident, mut fields: Vec) -> Vec) -> Vec { - let mut fields = populate_event_fields(content_name, fields); +fn populate_room_event_fields( + is_custom: bool, + content_name: Ident, + fields: Vec, +) -> Vec { + let mut fields = populate_event_fields(is_custom, content_name, fields); let punctuated_fields: Punctuated = parse_quote! { /// The unique identifier for the event. @@ -383,8 +452,8 @@ fn populate_room_event_fields(content_name: Ident, fields: Vec) -> Vec) -> Vec { - let mut fields = populate_room_event_fields(content_name.clone(), fields); +fn populate_state_fields(is_custom: bool, content_name: Ident, fields: Vec) -> Vec { + let mut fields = populate_room_event_fields(is_custom, content_name.clone(), fields); let punctuated_fields: Punctuated = parse_quote! { /// The previous content for this state key, if any. @@ -405,6 +474,11 @@ fn populate_state_fields(content_name: Ident, fields: Vec) -> Vec fields } +/// Checks if the given `Path` refers to `EventType::Custom`. +fn is_custom_event_type(event_type: &Path) -> bool { + event_type.segments.last().unwrap().value().ident == "Custom" +} + /// A wrapper around `syn::Field` that makes it possible to parse `Punctuated` /// from a `TokenStream`. /// diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index 14b74815..ffc0c87d 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use serde::{Deserialize, Serialize}; /// The type of an event. -#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum EventType { /// m.direct Direct, @@ -13,6 +13,9 @@ pub enum EventType { /// m.room.redaction RoomRedaction, + + /// Any event that is not part of the specification. + Custom(String), } /// A basic event. @@ -20,9 +23,6 @@ pub trait Event where Self: Debug + Serialize, { - /// The type of the event. - const EVENT_TYPE: EventType; - /// The type of this event's `content` field. type Content: Debug + Serialize; @@ -30,9 +30,7 @@ where fn content(&self) -> &Self::Content; /// The type of the event. - fn event_type(&self) -> EventType { - Self::EVENT_TYPE - } + fn event_type(&self) -> EventType; } /// An event within the context of a room. @@ -91,6 +89,23 @@ pub mod common_case { } } +pub mod custom_event_type { + use ruma_events_macros::ruma_event; + use serde_json::Value; + + ruma_event! { + /// A custom event. + CustomEvent { + kind: Event, + event_type: Custom, + content_type_alias: { + /// The payload for `CustomEvent`. + Value + }, + } + } +} + pub mod extra_fields { use ruma_events_macros::ruma_event; From e13acb4d7dacfaedb9e59e48efb3646670b1e6f4 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 20 Jun 2019 17:08:38 -0700 Subject: [PATCH 13/26] Fix copy/paste error. --- src/gen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gen.rs b/src/gen.rs index 5a9dd640..a41cc585 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -215,7 +215,7 @@ impl ToTokens for RumaEvent { } Content::Typedef(_) => { quote_spanned! {span=> - content: raw.content, + prev_content: raw.prev_content, } } } From d9039db8df71388e72b73650ccedf04fb7e1ada0 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 20 Jun 2019 22:10:04 -0700 Subject: [PATCH 14/26] Convert from JSON using impl FromStr and impl TryFrom<&'a str> rather than an inherent from_str method. --- src/gen.rs | 23 +++++++++++++++++------ src/lib.rs | 13 ++++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index a41cc585..0b205d0f 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -145,7 +145,7 @@ impl ToTokens for RumaEvent { let event_type_field_count = if self.is_custom { 0 } else { 1 }; let field_count = event_fields.len() + event_type_field_count; - let mut from_str_field_values: Vec = Vec::with_capacity(event_fields.len()); + let mut try_from_field_values: Vec = Vec::with_capacity(event_fields.len()); let mut serialize_field_calls: Vec = Vec::with_capacity(event_fields.len()); for field in event_fields { @@ -159,7 +159,7 @@ impl ToTokens for RumaEvent { let span = field.span(); - let from_str_field_value = if ident == "content" { + let try_from_field_value = if ident == "content" { match &self.content { Content::Struct(content_fields) => { let mut content_field_values: Vec = @@ -225,7 +225,7 @@ impl ToTokens for RumaEvent { } }; - from_str_field_values.push(from_str_field_value); + try_from_field_values.push(try_from_field_value); let serialize_field_call = quote_spanned! {span=> state.serialize_field(#ident_str, &self.#ident)?; @@ -316,17 +316,28 @@ impl ToTokens for RumaEvent { #content - impl #name { + impl std::str::FromStr for #name { + type Err = crate::InvalidEvent; + /// Attempt to create `Self` from parsing a string of JSON data. - pub fn from_str(json: &str) -> Result { + fn from_str(json: &str) -> Result { let raw = serde_json::from_str::(json)?; Ok(Self { - #(#from_str_field_values)* + #(#try_from_field_values)* }) } } + impl<'a> std::convert::TryFrom<&'a str> for #name { + type Error = crate::InvalidEvent; + + /// Attempt to create `Self` from parsing a string of JSON data. + fn try_from(json: &'a str) -> Result { + std::str::FromStr::from_str(json) + } + } + use serde::ser::SerializeStruct as _; impl serde::Serialize for #name { diff --git a/src/lib.rs b/src/lib.rs index c0faab46..32617891 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,11 +121,14 @@ mod parse; /// 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. +/// "raw" versions are the same, except they implement `serde::Deserialize`. An implementation of +/// `TryFrom<&str>` will be provided, which (through a blanket implementation in the standard +/// library for `FromStr` will allow the user to call `parse` on a string slice of JSON data in +/// attempt to convert into the event type. `TryFrom<&str>` 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. If any semantic error is found after deserialization, a +/// `serde_json::Value` of the deserialized data will be returned in an `InvalidEvent`. #[proc_macro] pub fn ruma_event(input: TokenStream) -> TokenStream { let ruma_event_input = syn::parse_macro_input!(input as RumaEventInput); From e34ea0544521e97ec96aa5a4f5a2c4e5e985653a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 21 Jun 2019 16:33:53 -0700 Subject: [PATCH 15/26] Skip serializing options. --- src/gen.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 0b205d0f..21b8e6e5 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -7,7 +7,7 @@ use syn::{ parse_quote, punctuated::Punctuated, spanned::Spanned, - Attribute, Field, Ident, Path, Token, + Attribute, Field, Ident, Path, Token, Type, }; use crate::parse::{Content, EventKind, RumaEventInput}; @@ -142,8 +142,11 @@ impl ToTokens for RumaEvent { // Custom events will already have an event_type field. All other events need to account // for this field being manually inserted in `Serialize` impls. - let event_type_field_count = if self.is_custom { 0 } else { 1 }; - let field_count = event_fields.len() + event_type_field_count; + let mut base_field_count: usize = if self.is_custom { 0 } else { 1 }; + + // Keep track of all the optional fields, because we'll need to check at runtime if they + // are `Some` in order to increase the number of fields we tell serde to serialize. + let mut optional_field_idents = Vec::with_capacity(event_fields.len()); let mut try_from_field_values: Vec = Vec::with_capacity(event_fields.len()); let mut serialize_field_calls: Vec = Vec::with_capacity(event_fields.len()); @@ -227,8 +230,21 @@ impl ToTokens for RumaEvent { try_from_field_values.push(try_from_field_value); - let serialize_field_call = quote_spanned! {span=> - state.serialize_field(#ident_str, &self.#ident)?; + // Does the same thing as #[serde(skip_serializing_if = "Option::is_none")] + let serialize_field_call = if is_option(&field.ty) { + optional_field_idents.push(ident.clone()); + + quote_spanned! {span=> + if self.#ident.is_some() { + state.serialize_field(#ident_str, &self.#ident)?; + } + } + } else { + base_field_count += 1; + + quote_spanned! {span=> + state.serialize_field(#ident_str, &self.#ident)?; + } }; serialize_field_calls.push(serialize_field_call); @@ -251,6 +267,27 @@ impl ToTokens for RumaEvent { ) }; + let increment_struct_len_statements: Vec = optional_field_idents + .iter() + .map(|ident| { + let span = ident.span(); + + quote_spanned! {span=> + if self.#ident.is_some() { + len += 1; + } + } + }) + .collect(); + + let set_up_struct_serializer = quote! { + let mut len = #base_field_count; + + #(#increment_struct_len_statements)* + + let mut state = serializer.serialize_struct(#name_str, len)?; + }; + let impl_room_event = match self.kind { EventKind::RoomEvent | EventKind::StateEvent => { quote! { @@ -347,7 +384,7 @@ impl ToTokens for RumaEvent { { #import_event_in_serialize_impl - let mut state = serializer.serialize_struct(#name_str, #field_count)?; + #set_up_struct_serializer #(#serialize_field_calls)* #manually_serialize_type_field @@ -490,6 +527,15 @@ fn is_custom_event_type(event_type: &Path) -> bool { event_type.segments.last().unwrap().value().ident == "Custom" } +/// Checks if a type is an `Option`. +fn is_option(ty: &Type) -> bool { + if let Type::Path(ref type_path) = ty { + type_path.path.segments.first().unwrap().value().ident == "Option" + } else { + panic!("struct field had unexpected non-path type"); + } +} + /// A wrapper around `syn::Field` that makes it possible to parse `Punctuated` /// from a `TokenStream`. /// From 3bad559980be99ff289dc78cd35916cf53067548 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 21 Jun 2019 17:00:28 -0700 Subject: [PATCH 16/26] Add serialization and deserialization tests. --- tests/ruma_events_macros.rs | 162 +++++++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 3 deletions(-) diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index ffc0c87d..807aa133 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -1,9 +1,9 @@ -use std::fmt::Debug; +use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use serde::{Deserialize, Serialize}; +use serde::{de::{Error as SerdeError, Visitor}, Deserialize, Deserializer, Serialize, Serializer}; /// The type of an event. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq)] pub enum EventType { /// m.direct Direct, @@ -18,6 +18,65 @@ pub enum EventType { Custom(String), } +impl Display for EventType { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let event_type_str = match *self { + EventType::Direct => "m.direct", + EventType::RoomAliases => "m.room.aliases", + EventType::RoomRedaction => "m.room.redaction", + EventType::Custom(ref event_type) => event_type, + }; + + write!(f, "{}", event_type_str) + } +} + +impl<'a> From<&'a str> for EventType { + fn from(s: &'a str) -> EventType { + match s { + "m.direct" => EventType::Direct, + "m.room.aliases" => EventType::RoomAliases, + "m.room.redaction" => EventType::RoomRedaction, + event_type => EventType::Custom(event_type.to_string()), + } + } +} + +impl Serialize for EventType { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for EventType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct EventTypeVisitor; + + impl<'de> Visitor<'de> for EventTypeVisitor { + type Value = EventType; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix event type as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + Ok(EventType::from(v)) + } + } + + deserializer.deserialize_str(EventTypeVisitor) + } +} + /// A basic event. pub trait Event where @@ -64,6 +123,7 @@ pub trait StateEvent: RoomEvent { fn state_key(&self) -> &str; } +#[derive(Debug)] pub struct InvalidEvent; impl From for InvalidEvent { @@ -74,7 +134,12 @@ impl From for InvalidEvent { // See note about wrapping macro expansion in a module from `src/lib.rs` pub mod common_case { + use std::convert::TryFrom; + + use js_int::UInt; use ruma_events_macros::ruma_event; + use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId}; + use serde_json::Value; ruma_event! { /// Informs the room about what room aliases it has been given. @@ -87,6 +152,97 @@ pub mod common_case { } } } + + #[test] + fn serialization_with_optional_fields_as_none() { + let event = AliasesEvent { + content: AliasesEventContent { + aliases: Vec::with_capacity(0), + }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UInt::try_from(1).unwrap(), + prev_content: None, + room_id: None, + sender: UserId::try_from("@carl:example.com").unwrap(), + state_key: "example.com".to_string(), + unsigned: None, + }; + + let actual = serde_json::to_string(&event).unwrap(); + let expected = r#"{"content":{"aliases":[]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"sender":"@carl:example.com","state_key":"example.com","type":"m.room.aliases"}"#; + + assert_eq!(actual, expected); + } + + #[test] + fn serialization_with_some_optional_fields_as_some() { + let event = AliasesEvent { + content: AliasesEventContent { + aliases: vec![RoomAliasId::try_from("#room:example.org").unwrap()], + }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UInt::try_from(1).unwrap(), + prev_content: Some(AliasesEventContent { + aliases: Vec::with_capacity(0), + }), + room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()), + sender: UserId::try_from("@carl:example.com").unwrap(), + state_key: "example.com".to_string(), + unsigned: None, + }; + + let actual = serde_json::to_string(&event).unwrap(); + let expected = r##"{"content":{"aliases":["#room:example.org"]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"prev_content":{"aliases":[]},"room_id":"!n8f893n9:example.com","sender":"@carl:example.com","state_key":"example.com","type":"m.room.aliases"}"##; + + assert_eq!(actual, expected); + } + + #[test] + fn serialization_with_all_optional_fields_as_some() { + let event = AliasesEvent { + content: AliasesEventContent { + aliases: vec![RoomAliasId::try_from("#room:example.org").unwrap()], + }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UInt::try_from(1).unwrap(), + prev_content: Some(AliasesEventContent { + aliases: Vec::with_capacity(0), + }), + room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()), + sender: UserId::try_from("@carl:example.com").unwrap(), + state_key: "example.com".to_string(), + unsigned: Some(serde_json::from_str::(r#"{"foo":"bar"}"#).unwrap()), + }; + + let actual = serde_json::to_string(&event).unwrap(); + let expected = r##"{"content":{"aliases":["#room:example.org"]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"prev_content":{"aliases":[]},"room_id":"!n8f893n9:example.com","sender":"@carl:example.com","state_key":"example.com","unsigned":{"foo":"bar"},"type":"m.room.aliases"}"##; + + assert_eq!(actual, expected); + } + + #[test] + fn deserialization() { + let json = r##"{"content":{"aliases":["#room:example.org"]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"prev_content":{"aliases":[]},"room_id":"!n8f893n9:example.com","sender":"@carl:example.com","state_key":"example.com","unsigned":{"foo":"bar"},"type":"m.room.aliases"}"##; + + let actual: AliasesEvent = json.parse().unwrap(); + + let expected = AliasesEvent { + content: AliasesEventContent { + aliases: vec![RoomAliasId::try_from("#room:example.org").unwrap()], + }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UInt::try_from(1).unwrap(), + prev_content: Some(AliasesEventContent { + aliases: Vec::with_capacity(0), + }), + room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()), + sender: UserId::try_from("@carl:example.com").unwrap(), + state_key: "example.com".to_string(), + unsigned: Some(serde_json::from_str::(r#"{"foo":"bar"}"#).unwrap()), + }; + + assert_eq!(actual, expected); + } } pub mod custom_event_type { From db70792f03ebde379e4cd649b5e40bd1ab25db92 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 21 Jun 2019 17:14:08 -0700 Subject: [PATCH 17/26] Alphabetize struct fields. --- src/gen.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 21b8e6e5..fd29fda0 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -481,11 +481,12 @@ fn populate_room_event_fields( /// The unique identifier for the room associated with this event. pub room_id: Option, + /// The unique identifier for the user who sent this event. + pub sender: ruma_identifiers::UserId, + /// Additional key-value pairs not signed by the homeserver. pub unsigned: Option, - /// The unique identifier for the user who sent this event. - pub sender: ruma_identifiers::UserId, }; let mut additional_fields = Vec::with_capacity(punctuated_fields.len()); From a65a6189302f784d3136441cf3d27af65d32707d Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 21 Jun 2019 17:18:06 -0700 Subject: [PATCH 18/26] Run rustfmt. --- tests/ruma_events_macros.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index 807aa133..98a5c9ce 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -1,6 +1,9 @@ use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; -use serde::{de::{Error as SerdeError, Visitor}, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{ + de::{Error as SerdeError, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; /// The type of an event. #[derive(Clone, Debug, PartialEq)] From 065cb770af2ac99fb2af42ffe2e7c39ce003cadf Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 22 Jun 2019 00:17:50 -0700 Subject: [PATCH 19/26] Remove unnecessary for loops. --- src/gen.rs | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index fd29fda0..0222e811 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -451,13 +451,7 @@ fn populate_event_fields( } }; - let mut additional_fields = Vec::with_capacity(punctuated_fields.len()); - - for punctuated_field in punctuated_fields { - additional_fields.push(punctuated_field.field); - } - - fields.extend(additional_fields); + fields.extend(punctuated_fields.into_iter().map(|p| p.field)); fields } @@ -486,16 +480,9 @@ fn populate_room_event_fields( /// Additional key-value pairs not signed by the homeserver. pub unsigned: Option, - }; - let mut additional_fields = Vec::with_capacity(punctuated_fields.len()); - - for punctuated_field in punctuated_fields { - additional_fields.push(punctuated_field.field); - } - - fields.extend(additional_fields); + fields.extend(punctuated_fields.into_iter().map(|p| p.field)); fields } @@ -512,13 +499,7 @@ fn populate_state_fields(is_custom: bool, content_name: Ident, fields: Vec Date: Fri, 28 Jun 2019 12:47:41 -0700 Subject: [PATCH 20/26] Implement FromStr and TryFrom<&str> for content types. --- src/gen.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/gen.rs b/src/gen.rs index 0222e811..328eba42 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -344,6 +344,48 @@ impl ToTokens for RumaEvent { TokenStream::new() }; + let impl_conversions_for_content = if let Content::Struct(content_fields) = &self.content { + let mut content_field_values: Vec = + Vec::with_capacity(content_fields.len()); + + for content_field in content_fields { + let content_field_ident = content_field.ident.clone().unwrap(); + let span = content_field.span(); + + let token_stream = quote_spanned! {span=> + #content_field_ident: raw.#content_field_ident, + }; + + content_field_values.push(token_stream); + } + + quote! { + impl std::str::FromStr for #content_name { + type Err = crate::InvalidEvent; + + /// Attempt to create `Self` from parsing a string of JSON data. + fn from_str(json: &str) -> Result { + let raw = serde_json::from_str::(json)?; + + Ok(Self { + #(#content_field_values)* + }) + } + } + + impl<'a> std::convert::TryFrom<&'a str> for #content_name { + type Error = crate::InvalidEvent; + + /// Attempt to create `Self` from parsing a string of JSON data. + fn try_from(json: &'a str) -> Result { + std::str::FromStr::from_str(json) + } + } + } + } else { + TokenStream::new() + }; + let output = quote!( #(#attrs)* #[derive(Clone, PartialEq, Debug)] @@ -375,6 +417,8 @@ impl ToTokens for RumaEvent { } } + #impl_conversions_for_content + use serde::ser::SerializeStruct as _; impl serde::Serialize for #name { From c25d76e20f5414dd63531f2ac553c9fe6823a2c1 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 03:29:47 -0700 Subject: [PATCH 21/26] Treat deserialization failure as a validation failure. If a ruma-events type fails deserialization, but the input was valid JSON, this should be treated as a validation failure instead of a deserialization failure. In this case, attempt to deserialize the json into a `serde_json::Value`, and only return a deserialization error if that fails. --- src/gen.rs | 30 ++++++++++++++++++++++++++++-- tests/ruma_events_macros.rs | 31 ++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index 328eba42..c6b9af1f 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -365,7 +365,20 @@ impl ToTokens for RumaEvent { /// Attempt to create `Self` from parsing a string of JSON data. fn from_str(json: &str) -> Result { - let raw = serde_json::from_str::(json)?; + let raw = match serde_json::from_str::(json) { + Ok(raw) => raw, + Err(error) => match serde_json::from_str::(json) { + Ok(value) => { + return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Validation { + json: value, + message: error.to_string(), + })); + } + Err(error) => { + return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Deserialization { error })); + } + }, + }; Ok(Self { #(#content_field_values)* @@ -400,7 +413,20 @@ impl ToTokens for RumaEvent { /// Attempt to create `Self` from parsing a string of JSON data. fn from_str(json: &str) -> Result { - let raw = serde_json::from_str::(json)?; + let raw = match serde_json::from_str::(json) { + Ok(raw) => raw, + Err(error) => match serde_json::from_str::(json) { + Ok(value) => { + return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Validation { + json: value, + message: error.to_string(), + })); + } + Err(error) => { + return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Deserialization { error })); + } + }, + }; Ok(Self { #(#try_from_field_values)* diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index 98a5c9ce..21b2b5ce 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -126,13 +126,34 @@ pub trait StateEvent: RoomEvent { fn state_key(&self) -> &str; } +/// An event that is malformed or otherwise invalid. +/// +/// When attempting to create an event from a string of JSON data, an error in the input data may +/// cause deserialization to fail, or the JSON structure may not corresponded to ruma-events's +/// strict definition of the event's schema. If deserialization completely fails, this type will +/// provide a message with details about the deserialization error. If deserialization succeeds but +/// the event is otherwise invalid, a similar message will be provided, as well as a +/// `serde_json::Value` containing the raw JSON data as it was deserialized. #[derive(Debug)] -pub struct InvalidEvent; +pub struct InvalidEvent(InnerInvalidEvent); -impl From for InvalidEvent { - fn from(_: serde_json::Error) -> Self { - Self - } +/// An event that is malformed or otherwise invalid. +#[derive(Debug)] +enum InnerInvalidEvent { + /// An event that failed to deserialize from JSON. + Deserialization { + /// The deserialization error returned by serde. + error: serde_json::Error, + }, + + /// An event that deserialized but failed validation. + Validation { + /// The raw `serde_json::Value` representation of the invalid event. + json: serde_json::Value, + + /// An message describing why the event was invalid. + message: String, + }, } // See note about wrapping macro expansion in a module from `src/lib.rs` From cea0421523ae79934071f2d8fff623a8d9c6d88f Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 19 Jul 2019 15:06:47 -0700 Subject: [PATCH 22/26] Reword some documentation. --- src/lib.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 32617891..6b7ea701 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! Crate `ruma-events-macros` provides a procedural macro for generating +//! 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. @@ -122,13 +122,13 @@ mod parse; /// /// 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 implementation of -/// `TryFrom<&str>` will be provided, which (through a blanket implementation in the standard -/// library for `FromStr` will allow the user to call `parse` on a string slice of JSON data in -/// attempt to convert into the event type. `TryFrom<&str>` 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. If any semantic error is found after deserialization, a -/// `serde_json::Value` of the deserialized data will be returned in an `InvalidEvent`. +/// `std::str::FromStr` (and for completeness, `std::convert::TryFrom<&str>`) will be provided, +/// which will allow the user to call `parse` on a string slice of JSON data in attempt to convert +/// into the event type. `FromStr` 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. If any +/// semantic error is found after deserialization, a `serde_json::Value` of the deserialized data +/// will be returned in an `InvalidEvent`. #[proc_macro] pub fn ruma_event(input: TokenStream) -> TokenStream { let ruma_event_input = syn::parse_macro_input!(input as RumaEventInput); From 8b926897501a294d5e3eb013879beb7a0911deea Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 23 Jul 2019 09:57:42 -0700 Subject: [PATCH 23/26] Run cargo-audit on CI. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 26513f5f..d403272f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,12 @@ language: "rust" +cache: "cargo" before_script: - "rustup component add rustfmt" - "rustup component add clippy" + - "cargo install --force cargo-audit" + - "cargo generate-lockfile" script: + - "cargo audit" - "cargo fmt --all -- --check" - "cargo clippy --all-targets --all-features -- -D warnings" - "cargo build --verbose" From e11ac61bef05d82f559da4ecb51d9ca8c65208ad Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 3 Aug 2019 14:01:50 -0700 Subject: [PATCH 24/26] Only build PRs and the master branch on CI. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d403272f..681414c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ script: - "cargo clippy --all-targets --all-features -- -D warnings" - "cargo build --verbose" - "cargo test --verbose" +if: "type != push OR (tag IS blank AND branch = master)" notifications: email: false irc: From f1927b2e5b7e1dd72e81c96ccd85fa8cffce79b2 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 22 Aug 2019 17:31:13 -0700 Subject: [PATCH 25/26] Replace FromStr/TryFrom impls with Deserialize using EventResult. --- src/gen.rs | 83 ++++++++++++++----------------------- tests/ruma_events_macros.rs | 33 +++++++++++---- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/gen.rs b/src/gen.rs index c6b9af1f..96529e43 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -360,40 +360,31 @@ impl ToTokens for RumaEvent { } quote! { - impl std::str::FromStr for #content_name { - type Err = crate::InvalidEvent; + impl<'de> serde::Deserialize<'de> for crate::EventResult<#content_name> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let json = serde_json::Value::deserialize(deserializer)?; - /// Attempt to create `Self` from parsing a string of JSON data. - fn from_str(json: &str) -> Result { - let raw = match serde_json::from_str::(json) { + let raw: raw::#content_name = match serde_json::from_value(json.clone()) { Ok(raw) => raw, - Err(error) => match serde_json::from_str::(json) { - Ok(value) => { - return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Validation { - json: value, + Err(error) => { + return Ok(crate::EventResult::Err(crate::InvalidEvent( + crate::InnerInvalidEvent::Validation { + json, message: error.to_string(), - })); - } - Err(error) => { - return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Deserialization { error })); - } - }, + }, + ))); + } }; - Ok(Self { + Ok(crate::EventResult::Ok(#content_name { #(#content_field_values)* - }) + })) } } - impl<'a> std::convert::TryFrom<&'a str> for #content_name { - type Error = crate::InvalidEvent; - - /// Attempt to create `Self` from parsing a string of JSON data. - fn try_from(json: &'a str) -> Result { - std::str::FromStr::from_str(json) - } - } } } else { TokenStream::new() @@ -408,38 +399,28 @@ impl ToTokens for RumaEvent { #content - impl std::str::FromStr for #name { - type Err = crate::InvalidEvent; + impl<'de> serde::Deserialize<'de> for crate::EventResult<#name> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let json = serde_json::Value::deserialize(deserializer)?; - /// Attempt to create `Self` from parsing a string of JSON data. - fn from_str(json: &str) -> Result { - let raw = match serde_json::from_str::(json) { + let raw: raw::#name = match serde_json::from_value(json.clone()) { Ok(raw) => raw, - Err(error) => match serde_json::from_str::(json) { - Ok(value) => { - return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Validation { - json: value, + Err(error) => { + return Ok(crate::EventResult::Err(crate::InvalidEvent( + crate::InnerInvalidEvent::Validation { + json, message: error.to_string(), - })); - } - Err(error) => { - return Err(crate::InvalidEvent(crate::InnerInvalidEvent::Deserialization { error })); - } - }, + }, + ))); + } }; - Ok(Self { + Ok(crate::EventResult::Ok(#name { #(#try_from_field_values)* - }) - } - } - - impl<'a> std::convert::TryFrom<&'a str> for #name { - type Error = crate::InvalidEvent; - - /// Attempt to create `Self` from parsing a string of JSON data. - fn try_from(json: &'a str) -> Result { - std::str::FromStr::from_str(json) + })) } } diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index 21b2b5ce..dd47dc7b 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -80,6 +80,28 @@ impl<'de> Deserialize<'de> for EventType { } } +/// The result of deserializing an event, which may or may not be valid. +#[derive(Debug)] +pub enum EventResult { + /// `T` deserialized and validated successfully. + Ok(T), + + /// `T` deserialized but was invalid. + /// + /// `InvalidEvent` contains the original input. + Err(InvalidEvent), +} + +impl EventResult { + /// Convert `EventResult` into the equivalent `std::result::Result`. + pub fn into_result(self) -> Result { + match self { + EventResult::Ok(t) => Ok(t), + EventResult::Err(invalid_event) => Err(invalid_event), + } + } +} + /// A basic event. pub trait Event where @@ -140,12 +162,6 @@ pub struct InvalidEvent(InnerInvalidEvent); /// An event that is malformed or otherwise invalid. #[derive(Debug)] enum InnerInvalidEvent { - /// An event that failed to deserialize from JSON. - Deserialization { - /// The deserialization error returned by serde. - error: serde_json::Error, - }, - /// An event that deserialized but failed validation. Validation { /// The raw `serde_json::Value` representation of the invalid event. @@ -165,6 +181,8 @@ pub mod common_case { use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId}; use serde_json::Value; + use super::EventResult; + ruma_event! { /// Informs the room about what room aliases it has been given. AliasesEvent { @@ -248,7 +266,8 @@ pub mod common_case { fn deserialization() { let json = r##"{"content":{"aliases":["#room:example.org"]},"event_id":"$h29iv0s8:example.com","origin_server_ts":1,"prev_content":{"aliases":[]},"room_id":"!n8f893n9:example.com","sender":"@carl:example.com","state_key":"example.com","unsigned":{"foo":"bar"},"type":"m.room.aliases"}"##; - let actual: AliasesEvent = json.parse().unwrap(); + let event_result: EventResult = serde_json::from_str(json).unwrap(); + let actual: AliasesEvent = event_result.into_result().unwrap(); let expected = AliasesEvent { content: AliasesEventContent { From b08f1a964d2ccef3d29eef3dcb9d877cfda81849 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sun, 1 Sep 2019 10:26:57 +0000 Subject: [PATCH 26/26] Update dependencies. --- Cargo.toml | 14 +++++++------- src/gen.rs | 4 ++-- src/parse.rs | 18 +++++------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1300d28..2d808a5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,15 @@ 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" +syn = { version = "1.0.4", features = ["full"] } +quote = "1.0.2" +proc-macro2 = "1.0.1" [lib] proc-macro = true [dev-dependencies] -ruma-identifiers = "0.13.1" -serde_json = "1.0.39" -js_int = { version = "0.1.0", features = ["serde"] } -serde = { version = "1.0.92", features = ["derive"] } +ruma-identifiers = "0.14.0" +serde_json = "1.0.40" +js_int = { version = "0.1.2", features = ["serde"] } +serde = { version = "1.0.99", features = ["derive"] } diff --git a/src/gen.rs b/src/gen.rs index 96529e43..c3fc9b96 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -557,13 +557,13 @@ fn populate_state_fields(is_custom: bool, content_name: Ident, fields: Vec bool { - event_type.segments.last().unwrap().value().ident == "Custom" + event_type.segments.last().unwrap().ident == "Custom" } /// Checks if a type is an `Option`. fn is_option(ty: &Type) -> bool { if let Type::Path(ref type_path) = ty { - type_path.path.segments.first().unwrap().value().ident == "Option" + type_path.path.segments.first().unwrap().ident == "Option" } else { panic!("struct field had unexpected non-path type"); } diff --git a/src/parse.rs b/src/parse.rs index 266f8756..e2fb6ba5 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -81,20 +81,11 @@ impl Parse for RumaEventInput { 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())) - { + if expr_path.path.is_ident("Event") { EventKind::Event - } else if expr_path - .path - .is_ident(Ident::new("RoomEvent", Span::call_site())) - { + } else if expr_path.path.is_ident("RoomEvent") { EventKind::RoomEvent - } else if expr_path - .path - .is_ident(Ident::new("StateEvent", Span::call_site())) - { + } else if expr_path.path.is_ident("StateEvent") { EventKind::StateEvent } else { panic!("value of field `kind` must be one of `Event`, `RoomEvent`, or `StateEvent`"); @@ -114,7 +105,7 @@ impl Parse for RumaEventInput { } let path = expr_path.path; - let variant = path.segments.first().unwrap().into_value(); + let variant = path.segments.first().unwrap(); let mut punctuated = Punctuated::new(); punctuated.push(PathSegment { @@ -189,6 +180,7 @@ pub enum Content { } /// 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. ///