diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eff74b18..d01912f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -235,18 +235,15 @@ Structs also should not be borrowed, with the exception that if a struct: - has fields that should be borrowed according to the table above (strings, identifiers, `Vec`s), and -- is only used inside request blocks (i.e. not in response blocks or in -events), +- is only used inside request blocks (i.e. not in response blocks or in events), then the struct should be lifetime-parameterized and apply the same rules to their fields. So instead of ```rust -ruma_api! { - request: { - my_field: MyStruct, - } - // ... +#[request] +pub struct Request { + my_field: MyStruct, } pub struct MyStruct { @@ -257,11 +254,9 @@ pub struct MyStruct { use ```rust -ruma_api! { - request: { - my_field: MyStruct<'a>, - } - // ... +#[request] +pub struct Request<'a> { + my_field: MyStruct<'a>, } pub struct MyStruct<'a> { diff --git a/crates/ruma-common/src/api.rs b/crates/ruma-common/src/api.rs index ee0f16fa..a2505909 100644 --- a/crates/ruma-common/src/api.rs +++ b/crates/ruma-common/src/api.rs @@ -18,182 +18,6 @@ use bytes::BufMut; use crate::UserId; -/// Generates [`IncomingRequest`] and [`OutgoingRequest`] from a concise definition. -/// -/// The macro expects the following structure as input: -/// -/// ```text -/// ruma_api! { -/// metadata: { -/// description: &'static str, -/// method: http::Method, -/// name: &'static str, -/// path: &'static str, -/// rate_limited: bool, -/// authentication: ruma_common::api::AuthScheme, -/// } -/// -/// request: { -/// // Struct fields for each piece of data required -/// // to make a request to this API endpoint. -/// } -/// -/// response: { -/// // Struct fields for each piece of data expected -/// // in the response from this API endpoint. -/// } -/// -/// // The error returned when a response fails, defaults to `MatrixError`. -/// error: path::to::Error -/// } -/// ``` -/// -/// This will generate a [`Metadata`] value to be used for the associated constants of -/// [`IncomingRequest`] and [`OutgoingRequest`], single `Request` and `Response` structs, and -/// the necessary trait implementations to convert the request into a `http::Request` and to -/// create a response from a `http::Response` and vice versa. -/// -/// The details of each of the three sections of the macros are documented below. -/// -/// ## Metadata -/// -/// * `description`: A short description of what the endpoint does. -/// * `method`: The HTTP method used for requests to the endpoint. It's not necessary to import -/// `http::Method`'s associated constants. Just write the value as if it was imported, e.g. -/// `GET`. -/// * `name`: A unique name for the endpoint. Generally this will be the same as the containing -/// module. -/// * `path`: The path component of the URL for the endpoint, e.g. "/foo/bar". Components of -/// the path that are parameterized can indicate a variable by using a Rust identifier -/// prefixed with a colon, e.g. `/foo/:some_parameter`. A corresponding query string -/// parameter will be expected in the request struct (see below for details). -/// * `rate_limited`: Whether or not the endpoint enforces rate limiting on requests. -/// * `authentication`: What authentication scheme the endpoint uses. -/// -/// ## Request -/// -/// The request block contains normal struct field definitions. Doc comments and attributes are -/// allowed as normal. There are also a few special attributes available to control how the -/// struct is converted into an `http::Request`: -/// -/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP -/// headers on the request. The value must implement `AsRef`. Generally this is a -/// `String`. The attribute value shown above as `HEADER_NAME` must be a `const` expression -/// of the type `http::header::HeaderName`, like one of the constants from `http::header`, -/// e.g. `CONTENT_TYPE`. -/// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path -/// component of the request URL. -/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query -/// string. -/// * `#[ruma_api(query_map)]`: Instead of individual query fields, one query_map field, of any -/// type that implements `IntoIterator` (e.g. `HashMap`, can be used for cases where an endpoint supports arbitrary query parameters. -/// -/// Any field that does not include one of these attributes will be part of the request's JSON -/// body. -/// -/// ## Response -/// -/// Like the request block, the response block consists of normal struct field definitions. -/// Doc comments and attributes are allowed as normal. -/// There is also a special attribute available to control how the struct is created from a -/// `http::Request`: -/// -/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP -/// headers on the response. The value must implement `AsRef`. Generally this is a -/// `String`. The attribute value shown above as `HEADER_NAME` must be a header name constant -/// from `http::header`, e.g. `CONTENT_TYPE`. -/// -/// Any field that does not include the above attribute will be expected in the response's JSON -/// body. -/// -/// ## Newtype bodies -/// -/// Both the request and response block also support "newtype bodies" by using the -/// `#[ruma_api(body)]` attribute on a field. If present on a field, the entire request or -/// response body will be treated as the value of the field. This allows you to treat the -/// entire request or response body as a specific type, rather than a JSON object with named -/// fields. Only one field in each struct can be marked with this attribute. It is an error to -/// have a newtype body field and normal body fields within the same struct. -/// -/// There is another kind of newtype body that is enabled with `#[ruma_api(raw_body)]`. It is -/// used for endpoints in which the request or response body can be arbitrary bytes instead of -/// a JSON objects. A field with `#[ruma_api(raw_body)]` needs to have the type `Vec`. -/// -/// # Examples -/// -/// ``` -/// pub mod some_endpoint { -/// use http::header::CONTENT_TYPE; -/// use ruma_common::api::ruma_api; -/// -/// ruma_api! { -/// metadata: { -/// description: "Does something.", -/// method: POST, -/// name: "some_endpoint", -/// stable_path: "/_matrix/some/endpoint/:baz", -/// rate_limited: false, -/// authentication: None, -/// added: 1.1, -/// } -/// -/// request: { -/// pub foo: String, -/// -/// #[ruma_api(header = CONTENT_TYPE)] -/// pub content_type: String, -/// -/// #[ruma_api(query)] -/// pub bar: String, -/// -/// #[ruma_api(path)] -/// pub baz: String, -/// } -/// -/// response: { -/// #[ruma_api(header = CONTENT_TYPE)] -/// pub content_type: String, -/// -/// pub value: String, -/// } -/// } -/// } -/// -/// pub mod newtype_body_endpoint { -/// use ruma_common::api::ruma_api; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Clone, Debug, Deserialize, Serialize)] -/// pub struct MyCustomType { -/// pub foo: String, -/// } -/// -/// ruma_api! { -/// metadata: { -/// description: "Does something.", -/// method: PUT, -/// name: "newtype_body_endpoint", -/// stable_path: "/_matrix/some/newtype/body/endpoint", -/// rate_limited: false, -/// authentication: None, -/// added: 1.1, -/// } -/// -/// request: { -/// #[ruma_api(raw_body)] -/// pub file: &'a [u8], -/// } -/// -/// response: { -/// #[ruma_api(body)] -/// pub my_custom_type: MyCustomType, -/// } -/// } -/// } -/// ``` -pub use ruma_macros::ruma_api; - /// Generates [`OutgoingRequest`] and [`IncomingRequest`] implementations. /// /// The `OutgoingRequest` impl is on the `Request` type this attribute is used on. It is @@ -655,7 +479,6 @@ macro_rules! metadata { }; // Simple literal case: used for description, name, rate_limited - // Also used by ruma_api! while it still exists, for the history field ( @field $_field:ident: $rhs:expr ) => { $rhs }; ( @history_impl diff --git a/crates/ruma-common/tests/api/ui/deprecated-without-added.rs b/crates/ruma-common/tests/api/ui/deprecated-without-added.rs index 17ef471c..90c44478 100644 --- a/crates/ruma-common/tests/api/ui/deprecated-without-added.rs +++ b/crates/ruma-common/tests/api/ui/deprecated-without-added.rs @@ -4,10 +4,10 @@ const _: Metadata = metadata! { description: "This will fail.", method: GET, name: "invalid_versions", - unstable => "/a/path", rate_limited: false, authentication: None, history: { + unstable => "/a/path", 1.1 => deprecated, } }; diff --git a/crates/ruma-common/tests/api/ui/deprecated-without-added.stderr b/crates/ruma-common/tests/api/ui/deprecated-without-added.stderr index 01e582a8..e6cf7ba0 100644 --- a/crates/ruma-common/tests/api/ui/deprecated-without-added.stderr +++ b/crates/ruma-common/tests/api/ui/deprecated-without-added.stderr @@ -1,5 +1,14 @@ -error: no rules expected the token `=>` - --> tests/api/ui/deprecated-without-added.rs:7:14 - | -7 | unstable => "/a/path", - | ^^ no rules expected this token in macro call +error: no rules expected the token `history_impl` + --> tests/api/ui/deprecated-without-added.rs:3:21 + | +3 | const _: Metadata = metadata! { + | _____________________^ +4 | | description: "This will fail.", +5 | | method: GET, +6 | | name: "invalid_versions", +... | +12 | | } +13 | | }; + | |_^ no rules expected this token in macro call + | + = note: this error originates in the macro `$crate::metadata` which comes from the expansion of the macro `metadata` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ruma-common/tests/api/ui/removed-without-deprecated.rs b/crates/ruma-common/tests/api/ui/removed-without-deprecated.rs index 34c0e729..152af910 100644 --- a/crates/ruma-common/tests/api/ui/removed-without-deprecated.rs +++ b/crates/ruma-common/tests/api/ui/removed-without-deprecated.rs @@ -4,10 +4,10 @@ const METADATA: Metadata = metadata! { description: "This will fail.", method: GET, name: "invalid_versions", - unstable => "/a/path", rate_limited: false, authentication: None, history: { + unstable => "/a/path", 1.1 => removed, } }; diff --git a/crates/ruma-common/tests/api/ui/removed-without-deprecated.stderr b/crates/ruma-common/tests/api/ui/removed-without-deprecated.stderr index c9b2a6f7..d08614df 100644 --- a/crates/ruma-common/tests/api/ui/removed-without-deprecated.stderr +++ b/crates/ruma-common/tests/api/ui/removed-without-deprecated.stderr @@ -1,5 +1,14 @@ -error: no rules expected the token `=>` - --> tests/api/ui/removed-without-deprecated.rs:7:14 - | -7 | unstable => "/a/path", - | ^^ no rules expected this token in macro call +error: no rules expected the token `history_impl` + --> tests/api/ui/removed-without-deprecated.rs:3:28 + | +3 | const METADATA: Metadata = metadata! { + | ____________________________^ +4 | | description: "This will fail.", +5 | | method: GET, +6 | | name: "invalid_versions", +... | +12 | | } +13 | | }; + | |_^ no rules expected this token in macro call + | + = note: this error originates in the macro `$crate::metadata` which comes from the expansion of the macro `metadata` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ruma-macros/src/api.rs b/crates/ruma-macros/src/api.rs index 999e5256..a6f00a6b 100644 --- a/crates/ruma-macros/src/api.rs +++ b/crates/ruma-macros/src/api.rs @@ -3,21 +3,10 @@ use std::{env, fs, path::Path}; use once_cell::sync::Lazy; -use proc_macro2::{Span, TokenStream}; -use quote::quote; +use proc_macro2::Span; use serde::{de::IgnoredAny, Deserialize}; -use syn::{ - braced, - parse::{Parse, ParseStream}, - Attribute, Field, Token, Type, -}; - -use self::{api_metadata::Metadata, api_request::Request, api_response::Response}; -use crate::util::import_ruma_common; mod api_metadata; -mod api_request; -mod api_response; mod attribute; mod auth_scheme; pub mod request; @@ -26,137 +15,11 @@ mod util; mod version; mod kw { - use syn::custom_keyword; - - custom_keyword!(error); - custom_keyword!(request); - custom_keyword!(response); + syn::custom_keyword!(error); } -/// The result of processing the `ruma_api` macro, ready for output back to source code. -pub struct Api { - /// The `metadata` section of the macro. - metadata: Metadata, - - /// The `request` section of the macro. - request: Option, - - /// The `response` section of the macro. - response: Option, - - /// The `error` section of the macro. - error_ty: Option, -} - -impl Api { - pub fn expand_all(self) -> TokenStream { - let ruma_common = import_ruma_common(); - - let metadata = &self.metadata; - let description = &metadata.description; - let method = &metadata.method; - let name = &metadata.name; - let rate_limited = &self.metadata.rate_limited; - let authentication = &self.metadata.authentication; - let history = &self.metadata.history; - - let error_ty = self.error_ty.map_or_else( - || quote! { #ruma_common::api::error::MatrixError }, - |err_ty| quote! { #err_ty }, - ); - - let request = self.request.map(|req| req.expand(metadata, &error_ty, &ruma_common)); - let response = self.response.map(|res| res.expand(metadata, &error_ty, &ruma_common)); - - let metadata_doc = format!("Metadata for the `{}` API endpoint.", name.value()); - - quote! { - // For some reason inlining the expression causes issues with macro parsing - const _RUMA_API_VERSION_HISTORY: #ruma_common::api::VersionHistory = #history; - - #[doc = #metadata_doc] - pub const METADATA: #ruma_common::api::Metadata = #ruma_common::metadata! { - description: #description, - method: #method, - name: #name, - rate_limited: #rate_limited, - authentication: #authentication, - history: _RUMA_API_VERSION_HISTORY, - }; - - #request - #response - - #[cfg(not(any(feature = "client", feature = "server")))] - type _SilenceUnusedError = #error_ty; - } - } -} - -impl Parse for Api { - fn parse(input: ParseStream<'_>) -> syn::Result { - let metadata: Metadata = input.parse()?; - - let req_attrs = input.call(Attribute::parse_outer)?; - let (request, attributes) = if input.peek(kw::request) { - let request = parse_request(input, req_attrs)?; - let after_req_attrs = input.call(Attribute::parse_outer)?; - - (Some(request), after_req_attrs) - } else { - // There was no `request` field so the attributes are for `response` - (None, req_attrs) - }; - - let response = if input.peek(kw::response) { - Some(parse_response(input, attributes)?) - } else if !attributes.is_empty() { - return Err(syn::Error::new_spanned( - &attributes[0], - "attributes are not supported on the error type", - )); - } else { - None - }; - - let error_ty = input - .peek(kw::error) - .then(|| { - let _: kw::error = input.parse()?; - let _: Token![:] = input.parse()?; - - input.parse() - }) - .transpose()?; - - Ok(Self { metadata, request, response, error_ty }) - } -} - -fn parse_request(input: ParseStream<'_>, attributes: Vec) -> syn::Result { - let request_kw: kw::request = input.parse()?; - let _: Token![:] = input.parse()?; - let fields; - braced!(fields in input); - - let fields = fields.parse_terminated::<_, Token![,]>(Field::parse_named)?; - - Ok(Request { request_kw, attributes, fields }) -} - -fn parse_response(input: ParseStream<'_>, attributes: Vec) -> syn::Result { - let response_kw: kw::response = input.parse()?; - let _: Token![:] = input.parse()?; - let fields; - braced!(fields in input); - - let fields = fields.parse_terminated::<_, Token![,]>(Field::parse_named)?; - - Ok(Response { attributes, fields, response_kw }) -} - -// Returns an error with a helpful error if the crate `ruma_api!` is used from doesn't declare both -// a `client` and a `server` feature. +// Returns an error with a helpful error if the crate the request or response macro is used from +// doesn't declare both a `client` and a `server` feature. fn ensure_feature_presence() -> Option<&'static syn::Error> { #[derive(Deserialize)] struct CargoToml { diff --git a/crates/ruma-macros/src/api/api_request.rs b/crates/ruma-macros/src/api/api_request.rs deleted file mode 100644 index 614c94d9..00000000 --- a/crates/ruma-macros/src/api/api_request.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Details of the `request` section of the procedural macro. - -use std::collections::btree_map::{BTreeMap, Entry}; - -use proc_macro2::TokenStream; -use quote::quote; -use syn::{ - parse_quote, punctuated::Punctuated, spanned::Spanned, visit::Visit, Attribute, Field, Ident, - Lifetime, Token, -}; - -use super::{ - api_metadata::Metadata, - kw, - util::{all_cfgs, extract_cfg}, -}; - -/// The result of processing the `request` section of the macro. -pub(crate) struct Request { - /// The `request` keyword - pub(super) request_kw: kw::request, - - /// The attributes that will be applied to the struct definition. - pub(super) attributes: Vec, - - /// The fields of the request. - pub(super) fields: Punctuated, -} - -impl Request { - /// The combination of every fields unique lifetime annotation. - fn all_lifetimes(&self) -> BTreeMap> { - let mut lifetimes = BTreeMap::new(); - - struct Visitor<'lt> { - field_cfg: Option, - lifetimes: &'lt mut BTreeMap>, - } - - impl<'ast> Visit<'ast> for Visitor<'_> { - fn visit_lifetime(&mut self, lt: &'ast Lifetime) { - match self.lifetimes.entry(lt.clone()) { - Entry::Vacant(v) => { - v.insert(self.field_cfg.clone()); - } - Entry::Occupied(mut o) => { - let lifetime_cfg = o.get_mut(); - - // If at least one field uses this lifetime and has no cfg attribute, we - // don't need a cfg attribute for the lifetime either. - *lifetime_cfg = Option::zip(lifetime_cfg.as_ref(), self.field_cfg.as_ref()) - .map(|(a, b)| { - let expr_a = extract_cfg(a); - let expr_b = extract_cfg(b); - parse_quote! { #[cfg( any( #expr_a, #expr_b ) )] } - }); - } - } - } - } - - for field in &self.fields { - let field_cfg = if field.attrs.is_empty() { None } else { all_cfgs(&field.attrs) }; - Visitor { lifetimes: &mut lifetimes, field_cfg }.visit_type(&field.ty); - } - - lifetimes - } - - pub(super) fn expand( - &self, - metadata: &Metadata, - error_ty: &TokenStream, - ruma_common: &TokenStream, - ) -> TokenStream { - let ruma_macros = quote! { #ruma_common::exports::ruma_macros }; - - let docs = format!( - "Data for a request to the `{}` API endpoint.\n\n{}", - metadata.name.value(), - metadata.description.value(), - ); - let struct_attributes = &self.attributes; - - let request_ident = Ident::new("Request", self.request_kw.span()); - let lifetimes = self.all_lifetimes(); - let lifetimes = lifetimes.iter().map(|(lt, attr)| quote! { #attr #lt }); - let fields = &self.fields; - - quote! { - #[doc = #docs] - #[#ruma_macros::request(error = #error_ty)] - #( #struct_attributes )* - pub struct #request_ident < #(#lifetimes),* > { - #fields - } - } - } -} diff --git a/crates/ruma-macros/src/api/api_response.rs b/crates/ruma-macros/src/api/api_response.rs deleted file mode 100644 index 0113fa7c..00000000 --- a/crates/ruma-macros/src/api/api_response.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Details of the `response` section of the procedural macro. - -use proc_macro2::TokenStream; -use quote::quote; -use syn::{punctuated::Punctuated, spanned::Spanned, Attribute, Field, Ident, Token}; - -use super::{api_metadata::Metadata, kw}; - -/// The result of processing the `response` section of the macro. -pub(crate) struct Response { - /// The `response` keyword - pub(super) response_kw: kw::response, - - /// The attributes that will be applied to the struct definition. - pub attributes: Vec, - - /// The fields of the response. - pub fields: Punctuated, -} - -impl Response { - pub(super) fn expand( - &self, - metadata: &Metadata, - error_ty: &TokenStream, - ruma_common: &TokenStream, - ) -> TokenStream { - let ruma_macros = quote! { #ruma_common::exports::ruma_macros }; - - let docs = - format!("Data in the response from the `{}` API endpoint.", metadata.name.value()); - let struct_attributes = &self.attributes; - - let response_ident = Ident::new("Response", self.response_kw.span()); - let fields = &self.fields; - quote! { - #[doc = #docs] - #[#ruma_macros::response(error = #error_ty)] - #( #struct_attributes )* - pub struct #response_ident { - #fields - } - } - } -} diff --git a/crates/ruma-macros/src/api/util.rs b/crates/ruma-macros/src/api/util.rs index dfc001a1..284e8ca3 100644 --- a/crates/ruma-macros/src/api/util.rs +++ b/crates/ruma-macros/src/api/util.rs @@ -4,7 +4,7 @@ use std::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use syn::{parse_quote, visit::Visit, Attribute, Lifetime, NestedMeta, Type}; +use syn::{visit::Visit, Lifetime, Type}; pub fn map_option_literal(ver: &Option) -> TokenStream { match ver { @@ -27,30 +27,3 @@ pub fn collect_lifetime_idents(lifetimes: &mut BTreeSet, ty: &Type) { Visitor(lifetimes).visit_type(ty); } - -pub fn all_cfgs_expr(cfgs: &[Attribute]) -> Option { - let sub_cfgs: Vec<_> = cfgs.iter().filter_map(extract_cfg).collect(); - (!sub_cfgs.is_empty()).then(|| quote! { all( #(#sub_cfgs),* ) }) -} - -pub fn all_cfgs(cfgs: &[Attribute]) -> Option { - let cfg_expr = all_cfgs_expr(cfgs)?; - Some(parse_quote! { #[cfg( #cfg_expr )] }) -} - -pub fn extract_cfg(attr: &Attribute) -> Option { - if !attr.path.is_ident("cfg") { - return None; - } - - let meta = attr.parse_meta().expect("cfg attribute can be parsed to syn::Meta"); - let mut list = match meta { - syn::Meta::List(l) => l, - _ => panic!("unexpected cfg syntax"), - }; - - assert!(list.path.is_ident("cfg"), "expected cfg attributes only"); - assert_eq!(list.nested.len(), 1, "expected one item inside cfg()"); - - Some(list.nested.pop().unwrap().into_value()) -} diff --git a/crates/ruma-macros/src/lib.rs b/crates/ruma-macros/src/lib.rs index 388bc9d3..6da62f5c 100644 --- a/crates/ruma-macros/src/lib.rs +++ b/crates/ruma-macros/src/lib.rs @@ -28,7 +28,6 @@ use self::{ api::{ request::{expand_derive_request, expand_request}, response::{expand_derive_response, expand_response}, - Api, }, events::{ event::expand_event, @@ -380,14 +379,6 @@ pub fn fake_derive_serde(_input: TokenStream) -> TokenStream { TokenStream::new() } -/// > ⚠ If this is the only documentation you see, please navigate to the docs for -/// > `ruma_common::api::ruma_api`, where actual documentation can be found. -#[proc_macro] -pub fn ruma_api(input: TokenStream) -> TokenStream { - let api = parse_macro_input!(input as Api); - api.expand_all().into() -} - /// > ⚠ If this is the only documentation you see, please navigate to the docs for /// > `ruma_common::api::request`, where actual documentation can be found. #[proc_macro_attribute] @@ -406,14 +397,14 @@ pub fn response(attr: TokenStream, item: TokenStream) -> TokenStream { expand_response(attr, item).into() } -/// Internal helper taking care of the request-specific parts of `ruma_api!`. +/// Internal helper that the request macro delegates most of its work to. #[proc_macro_derive(Request, attributes(ruma_api))] pub fn derive_request(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); expand_derive_request(input).unwrap_or_else(syn::Error::into_compile_error).into() } -/// Internal helper taking care of the response-specific parts of `ruma_api!`. +/// Internal helper that the response macro delegates most of its work to. #[proc_macro_derive(Response, attributes(ruma_api))] pub fn derive_response(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput);