diff --git a/ruma-api-macros/src/api.rs b/ruma-api-macros/src/api.rs index dba3140f..c776b126 100644 --- a/ruma-api-macros/src/api.rs +++ b/ruma-api-macros/src/api.rs @@ -102,13 +102,13 @@ impl ToTokens for Api { let request_type = &self.request; let response_type = &self.response; - let request_try_from_type = if self.request.uses_wrap_incoming() { + let request_try_from_type = if self.request.contains_lifetimes() { quote!(IncomingRequest) } else { quote!(Request) }; - let response_try_from_type = if self.response.uses_wrap_incoming() { + let response_try_from_type = if self.response.contains_lifetimes() { quote!(IncomingResponse) } else { quote!(Response) @@ -222,6 +222,21 @@ impl ToTokens for Api { let error = &self.error; + let res_life = self.response.lifetimes().collect::>(); + let req_life = self.request.lifetimes().collect::>(); + + let response_lifetimes = util::generics_to_tokens(res_life.iter().cloned()); + let request_lifetimes = util::generics_to_tokens(req_life.iter().cloned()); + + let endpoint_impl_lifetimes = if res_life != req_life { + let diff = + res_life.into_iter().filter(|resp| !req_life.contains(resp)).collect::>(); + + util::generics_to_tokens(req_life.iter().cloned().chain(diff)) + } else { + request_lifetimes.clone() + }; + let api = quote! { // FIXME: These can't conflict with other imports, but it would still be nice not to // bring anything into scope that code outside the macro could then rely on. @@ -260,13 +275,13 @@ impl ToTokens for Api { #[doc = #response_doc] #response_type - impl ::std::convert::TryFrom + impl #response_lifetimes ::std::convert::TryFrom for ::ruma_api::exports::http::Response> { type Error = ::ruma_api::error::IntoHttpError; #[allow(unused_variables)] - fn try_from(response: Response) -> ::std::result::Result { + fn try_from(response: Response #response_lifetimes) -> ::std::result::Result { let response = ::ruma_api::exports::http::Response::builder() .header(::ruma_api::exports::http::header::CONTENT_TYPE, "application/json") #serialize_response_headers @@ -304,8 +319,8 @@ impl ToTokens for Api { } } - impl ::ruma_api::Endpoint for Request { - type Response = Response; + impl #endpoint_impl_lifetimes ::ruma_api::Endpoint for Request #request_lifetimes { + type Response = Response #response_lifetimes; type ResponseError = #error; /// Metadata for the `#name` endpoint. diff --git a/ruma-api-macros/src/api/request.rs b/ruma-api-macros/src/api/request.rs index f694a865..4b7d7a77 100644 --- a/ruma-api-macros/src/api/request.rs +++ b/ruma-api-macros/src/api/request.rs @@ -1,10 +1,10 @@ //! Details of the `request` section of the procedural macro. -use std::{convert::TryFrom, mem}; +use std::{collections::BTreeSet, convert::TryFrom, mem}; use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; -use syn::{spanned::Spanned, Field, Ident}; +use syn::{spanned::Spanned, Field, Ident, Lifetime}; use crate::{ api::{ @@ -18,6 +18,9 @@ use crate::{ pub struct Request { /// The fields of the request. fields: Vec, + + /// The collected lifetime identifiers from the declared fields. + lifetimes: Vec, } impl Request { @@ -98,9 +101,13 @@ impl Request { self.fields.iter().filter_map(|field| field.as_body_field()) } - /// Whether any field has a #[wrap_incoming] attribute. - pub fn uses_wrap_incoming(&self) -> bool { - self.fields.iter().any(|f| f.has_wrap_incoming_attr()) + /// Whether any field has a lifetime. + pub fn contains_lifetimes(&self) -> bool { + self.fields.iter().any(|f| util::has_lifetime(&f.field().ty)) + } + + pub fn lifetimes(&self) -> impl Iterator { + self.lifetimes.iter() } /// Produces an iterator over all the header fields. @@ -194,6 +201,7 @@ impl TryFrom for Request { fn try_from(raw: RawRequest) -> syn::Result { let mut newtype_body_field = None; let mut query_map_field = None; + let mut lifetimes = BTreeSet::new(); let fields = raw .fields @@ -202,6 +210,8 @@ impl TryFrom for Request { let mut field_kind = None; let mut header = None; + util::copy_lifetime_ident(&mut lifetimes, &field.ty); + for attr in mem::replace(&mut field.attrs, Vec::new()) { let meta = match Meta::from_attribute(&attr)? { Some(m) => m, @@ -287,7 +297,7 @@ impl TryFrom for Request { )); } - Ok(Self { fields }) + Ok(Self { fields, lifetimes: lifetimes.into_iter().collect() }) } } @@ -301,10 +311,12 @@ impl ToTokens for Request { quote! { { #(#fields),* } } }; + let request_generics = util::generics_to_tokens(self.lifetimes.iter()); + let request_body_struct = if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) { let field = Field { ident: None, colon_token: None, ..body_field.field().clone() }; - let derive_deserialize = if body_field.has_wrap_incoming_attr() { + let derive_deserialize = if util::has_lifetime(&body_field.field().ty) { TokenStream::new() } else { quote!(::ruma_api::exports::serde::Deserialize) @@ -313,14 +325,18 @@ impl ToTokens for Request { Some((derive_deserialize, quote! { (#field); })) } else if self.has_body_fields() { let fields = self.fields.iter().filter(|f| f.is_body()); - let derive_deserialize = if fields.clone().any(|f| f.has_wrap_incoming_attr()) { - TokenStream::new() - } else { - quote!(::ruma_api::exports::serde::Deserialize) - }; + let (derive_deserialize, lifetimes) = + if fields.clone().any(|f| util::has_lifetime(&f.field().ty)) { + ( + TokenStream::new(), + util::collect_generic_idents(fields.clone().map(|f| &f.field().ty)), + ) + } else { + (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new()) + }; let fields = fields.map(RequestField::field); - Some((derive_deserialize, quote! { { #(#fields),* } })) + Some((derive_deserialize, quote! { #lifetimes { #(#fields),* } })) } else { None } @@ -339,6 +355,7 @@ impl ToTokens for Request { let request_query_struct = if let Some(f) = self.query_map_field() { let field = Field { ident: None, colon_token: None, ..f.clone() }; + let lifetime = util::collect_generic_idents(Some(&field.ty).into_iter()); quote! { /// Data in the request's query string. @@ -347,10 +364,11 @@ impl ToTokens for Request { ::ruma_api::exports::serde::Deserialize, ::ruma_api::exports::serde::Serialize, )] - struct RequestQuery(#field); + struct RequestQuery #lifetime (#field); } } else if self.has_query_fields() { let fields = self.fields.iter().filter_map(RequestField::as_query_field); + let lifetime = util::collect_generic_idents(fields.clone().map(|f| &f.ty)); quote! { /// Data in the request's query string. @@ -359,7 +377,7 @@ impl ToTokens for Request { ::ruma_api::exports::serde::Deserialize, ::ruma_api::exports::serde::Serialize, )] - struct RequestQuery { + struct RequestQuery #lifetime { #(#fields),* } } @@ -370,7 +388,7 @@ impl ToTokens for Request { let request = quote! { #[derive(Debug, Clone, ::ruma_api::Outgoing)] #[incoming_no_deserialize] - pub struct Request #request_def + pub struct Request #request_generics #request_def #request_body_struct #request_query_struct @@ -498,13 +516,6 @@ impl RequestField { None } } - - /// Whether or not the request field has a #[wrap_incoming] attribute. - fn has_wrap_incoming_attr(&self) -> bool { - self.field().attrs.iter().any(|attr| { - attr.path.segments.len() == 1 && attr.path.segments[0].ident == "wrap_incoming" - }) - } } /// The types of fields that a request can have, without their values. diff --git a/ruma-api-macros/src/api/response.rs b/ruma-api-macros/src/api/response.rs index 866789ee..51a34434 100644 --- a/ruma-api-macros/src/api/response.rs +++ b/ruma-api-macros/src/api/response.rs @@ -1,10 +1,10 @@ //! Details of the `response` section of the procedural macro. -use std::{convert::TryFrom, mem}; +use std::{collections::BTreeSet, convert::TryFrom, mem}; use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; -use syn::{spanned::Spanned, Field, Ident}; +use syn::{spanned::Spanned, Field, Ident, Lifetime}; use crate::{ api::{ @@ -18,6 +18,9 @@ use crate::{ pub struct Response { /// The fields of the response. fields: Vec, + + /// The collected lifetime identifiers from the declared fields. + lifetimes: Vec, } impl Response { @@ -31,9 +34,13 @@ impl Response { self.fields.iter().any(|field| field.is_header()) } - /// Whether any field has a #[wrap_incoming] attribute. - pub fn uses_wrap_incoming(&self) -> bool { - self.fields.iter().any(|f| f.has_wrap_incoming_attr()) + /// Whether any field has a lifetime. + pub fn contains_lifetimes(&self) -> bool { + self.fields.iter().any(|f| util::has_lifetime(&f.field().ty)) + } + + pub fn lifetimes(&self) -> impl Iterator { + self.lifetimes.iter() } /// Produces code for a response struct initializer. @@ -162,6 +169,7 @@ impl TryFrom for Response { fn try_from(raw: RawResponse) -> syn::Result { let mut newtype_body_field = None; + let mut lifetimes = BTreeSet::new(); let fields = raw .fields @@ -170,6 +178,8 @@ impl TryFrom for Response { let mut field_kind = None; let mut header = None; + util::copy_lifetime_ident(&mut lifetimes, &field.ty); + for attr in mem::replace(&mut field.attrs, Vec::new()) { let meta = match Meta::from_attribute(&attr)? { Some(m) => m, @@ -230,7 +240,7 @@ impl TryFrom for Response { )); } - Ok(Self { fields }) + Ok(Self { fields, lifetimes: lifetimes.into_iter().collect() }) } } @@ -245,30 +255,40 @@ impl ToTokens for Response { quote! { { #(#fields),* } } }; - let (derive_deserialize, def) = - if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) { - let field = Field { ident: None, colon_token: None, ..body_field.field().clone() }; - let derive_deserialize = if body_field.has_wrap_incoming_attr() { - TokenStream::new() - } else { - quote!(::ruma_api::exports::serde::Deserialize) - }; + let (derive_deserialize, def) = if let Some(body_field) = + self.fields.iter().find(|f| f.is_newtype_body()) + { + let field = Field { ident: None, colon_token: None, ..body_field.field().clone() }; - (derive_deserialize, quote! { (#field); }) - } else if self.has_body_fields() { - let fields = self.fields.iter().filter(|f| f.is_body()); - let derive_deserialize = if fields.clone().any(|f| f.has_wrap_incoming_attr()) { - TokenStream::new() - } else { - quote!(::ruma_api::exports::serde::Deserialize) - }; - let fields = fields.map(ResponseField::field); - - (derive_deserialize, quote!({ #(#fields),* })) + let (derive_deserialize, lifetimes) = if util::has_lifetime(&body_field.field().ty) { + ( + TokenStream::new(), + util::collect_generic_idents(Some(&body_field.field().ty).into_iter()), + ) } else { - (TokenStream::new(), quote!({})) + (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new()) }; + (derive_deserialize, quote! { #lifetimes (#field); }) + } else if self.has_body_fields() { + let fields = self.fields.iter().filter(|f| f.is_body()); + let (derive_deserialize, lifetimes) = + if fields.clone().any(|f| util::has_lifetime(&f.field().ty)) { + ( + TokenStream::new(), + util::collect_generic_idents(fields.clone().map(|f| &f.field().ty)), + ) + } else { + (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new()) + }; + + let fields = fields.map(ResponseField::field); + + (derive_deserialize, quote!( #lifetimes { #(#fields),* })) + } else { + (TokenStream::new(), quote!({})) + }; + let response_body_struct = quote! { /// Data in the response body. #[derive( @@ -280,10 +300,11 @@ impl ToTokens for Response { struct ResponseBody #def }; + let response_generics = util::generics_to_tokens(self.lifetimes.iter()); let response = quote! { #[derive(Debug, Clone, ::ruma_api::Outgoing)] #[incoming_no_deserialize] - pub struct Response #response_def + pub struct Response #response_generics #response_def #response_body_struct }; @@ -353,13 +374,6 @@ impl ResponseField { _ => None, } } - - /// Whether or not the response field has a #[wrap_incoming] attribute. - fn has_wrap_incoming_attr(&self) -> bool { - self.field().attrs.iter().any(|attr| { - attr.path.segments.len() == 1 && attr.path.segments[0].ident == "wrap_incoming" - }) - } } /// The types of fields that a response can have, without their values. diff --git a/ruma-api/tests/outgoing.rs b/ruma-api/tests/outgoing.rs index b1c101e1..d467f78c 100644 --- a/ruma-api/tests/outgoing.rs +++ b/ruma-api/tests/outgoing.rs @@ -54,24 +54,24 @@ ruma_api! { } } -// #[derive(Outgoing)] -// #[incoming_no_deserialize] -// pub struct Request<'a, T> { -// pub abc: &'a str, -// pub thing: Thing<'a, T>, -// pub device_id: &'a ::ruma_identifiers::DeviceId, -// pub user_id: &'a UserId, -// pub bytes: &'a [u8], -// pub recursive: &'a [Thing<'a, T>], -// pub option: Option<&'a [u8]>, -// } +#[derive(Outgoing)] +#[incoming_no_deserialize] +pub struct FakeRequest<'a, T> { + pub abc: &'a str, + pub thing: Thing<'a, T>, + pub device_id: &'a ::ruma_identifiers::DeviceId, + pub user_id: &'a UserId, + pub bytes: &'a [u8], + pub recursive: &'a [Thing<'a, T>], + pub option: Option<&'a [u8]>, +} -// #[derive(Outgoing)] -// #[incoming_no_deserialize] -// pub enum EnumThing<'a, T> { -// Abc(&'a str), -// Stuff(Thing<'a, T>), -// Boxy(&'a ::ruma_identifiers::DeviceId), -// Other(Option<&'a str>), -// StructVar { stuff: &'a str, more: &'a ::ruma_identifiers::ServerName }, -// } +#[derive(Outgoing)] +#[incoming_no_deserialize] +pub enum EnumThing<'a, T> { + Abc(&'a str), + Stuff(Thing<'a, T>), + Boxy(&'a ::ruma_identifiers::DeviceId), + Other(Option<&'a str>), + StructVar { stuff: &'a str, more: &'a ::ruma_identifiers::ServerName }, +}