Repository: ruma/ruma
Pull request #179 (2020-08-05 00:21:17)

Lifetime parameters in the ruma_api! macro
My last commit mentions about conversion between the fields of a `Response/Request` that are `&str` or some other type that needs to be converted, will we use `From/Into` or is there another way you were thinking for converting `Request<'a>/Response<'a>` into `IncomingRequest/Response`?

DevinR528: The bulk of the work is in https://github.com/ruma/ruma/pull/179/commits/285570885c6dcd7ca54cf779591c531ec550261c. I ran into a problem with deserializing a query_map field of anything other than a `Vec<...>` serde doesn't like `&[any type that isn't u8]`, is there any way around this or should the macro enforce this limitation (or am I just doing something dumb)?
jplatte: Let's leave out `query_map` support for later, it's only relevant for one or two endpoints anyway.

Patch:
diff --git a/ruma-api-macros/src/api.rs b/ruma-api-macros/src/api.rs
index dba3140f..1973fa41 100644
--- a/ruma-api-macros/src/api.rs
+++ b/ruma-api-macros/src/api.rs
@@ -91,24 +91,16 @@ impl ToTokens for Api {
         let rate_limited = &self.metadata.rate_limited;
         let requires_authentication = &self.metadata.requires_authentication;
 
-        let non_auth_endpoint_impl = if requires_authentication.value {
-            quote! {
-                impl ::ruma_api::NonAuthEndpoint for Request {}
-            }
-        } else {
-            TokenStream::new()
-        };
-
         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)
@@ -134,7 +126,7 @@ impl ToTokens for Api {
             let field_name = field.ident.as_ref().expect("expected field to have an identifier");
 
             quote! {
-                #field_name: request_query
+                #field_name: request_query,
             }
         } else {
             self.request.request_init_query_fields()
@@ -163,18 +155,27 @@ impl ToTokens for Api {
             TokenStream::new()
         };
 
-        let extract_request_body =
-            if self.request.has_body_fields() || self.request.newtype_body_field().is_some() {
-                quote! {
-                    let request_body: <RequestBody as ::ruma_api::Outgoing>::Incoming =
-                        ::ruma_api::try_deserialize!(
-                            request,
-                            ::ruma_api::exports::serde_json::from_slice(request.body().as_slice())
-                        );
-                }
+        let extract_request_body = if self.request.has_body_fields()
+            || self.request.newtype_body_field().is_some()
+        {
+            let body_lifetimes = if self.request.has_body_lifetimes() {
+                // duplicate the anonymous lifetime as many times as needed
+                let lifetimes =
+                    std::iter::repeat(quote! { '_ }).take(self.request.body_lifetime_count());
+                quote! { < #( #lifetimes, )* >}
             } else {
                 TokenStream::new()
             };
+            quote! {
+                let request_body: <RequestBody #body_lifetimes as ::ruma_api::Outgoing>::Incoming =
+                    ::ruma_api::try_deserialize!(
+                        request,
+                        ::ruma_api::exports::serde_json::from_slice(request.body().as_slice())
+                    );
+            }
+        } else {
+            TokenStream::new()
+        };
 
         let parse_request_headers = if self.request.has_header_fields() {
             self.request.parse_headers_from_request()
@@ -194,19 +195,29 @@ impl ToTokens for Api {
             TokenStream::new()
         };
 
-        let typed_response_body_decl =
-            if self.response.has_body_fields() || self.response.newtype_body_field().is_some() {
-                quote! {
-                    let response_body: <ResponseBody as ::ruma_api::Outgoing>::Incoming =
-                        ::ruma_api::try_deserialize!(
-                            response,
-                            ::ruma_api::exports::serde_json::from_slice(response.body().as_slice()),
-                        );
-                }
+        let typed_response_body_decl = if self.response.has_body_fields()
+            || self.response.newtype_body_field().is_some()
+        {
+            let body_lifetimes = if self.response.has_body_lifetimes() {
+                // duplicate the anonymous lifetime as many times as needed
+                let lifetimes =
+                    std::iter::repeat(quote! { '_ }).take(self.response.body_lifetime_count());
+                quote! { < #( #lifetimes, )* >}
             } else {
                 TokenStream::new()
             };
 
+            quote! {
+                let response_body: <ResponseBody #body_lifetimes as ::ruma_api::Outgoing>::Incoming =
+                    ::ruma_api::try_deserialize!(
+                        response,
+                        ::ruma_api::exports::serde_json::from_slice(response.body().as_slice()),
+                    );
+            }
+        } else {
+            TokenStream::new()
+        };
+
         let response_init_fields = self.response.init_fields();
 
         let serialize_response_headers = self.response.apply_header_fields();
@@ -222,6 +233,17 @@ impl ToTokens for Api {
 
         let error = &self.error;
 
+        let response_lifetimes = self.response.combine_lifetimes();
+        let request_lifetimes = self.request.combine_lifetimes();
+
+        let non_auth_endpoint_impl = if requires_authentication.value {
+            quote! {
+                impl #request_lifetimes  ::ruma_api::NonAuthEndpoint for Request #request_lifetimes {}
+            }
+        } else {
+            TokenStream::new()
+        };
+
         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 +282,13 @@ impl ToTokens for Api {
             #[doc = #response_doc]
             #response_type
 
-            impl ::std::convert::TryFrom<Response>
+            impl #response_lifetimes ::std::convert::TryFrom<Response #response_lifetimes>
                 for ::ruma_api::exports::http::Response<Vec<u8>>
             {
                 type Error = ::ruma_api::error::IntoHttpError;
 
                 #[allow(unused_variables)]
-                fn try_from(response: Response) -> ::std::result::Result<Self, Self::Error> {
+                fn try_from(response: Response #response_lifetimes) -> ::std::result::Result<Self, Self::Error> {
                     let response = ::ruma_api::exports::http::Response::builder()
                         .header(::ruma_api::exports::http::header::CONTENT_TYPE, "application/json")
                         #serialize_response_headers
@@ -304,8 +326,8 @@ impl ToTokens for Api {
                 }
             }
 
-            impl ::ruma_api::Endpoint for Request {
-                type Response = Response;
+            impl #request_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..fd53bca4 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::{
@@ -14,10 +14,21 @@ use crate::{
     util,
 };
 
+#[derive(Debug, Default)]
+pub struct RequestLifetimes {
+    body: BTreeSet<Lifetime>,
+    path: BTreeSet<Lifetime>,
+    query: BTreeSet<Lifetime>,
+    header: BTreeSet<Lifetime>,
+}
+
 /// The result of processing the `request` section of the macro.
 pub struct Request {
     /// The fields of the request.
     fields: Vec<RequestField>,
+
+    /// The collected lifetime identifiers from the declared fields.
+    lifetimes: RequestLifetimes,
 }
 
 impl Request {
@@ -98,11 +109,58 @@ 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())
+    /// The number of unique lifetime annotations for `body` fields.
+    pub fn body_lifetime_count(&self) -> usize {
+        self.lifetimes.body.len()
+    }
+
+    /// Whether any `body` field has a lifetime annotation.
+    pub fn has_body_lifetimes(&self) -> bool {
+        !self.lifetimes.body.is_empty()
+    }
+
+    /// Whether any `query` field has a lifetime annotation.
+    pub fn has_query_lifetimes(&self) -> bool {
+        !self.lifetimes.query.is_empty()
+    }
+
+    /// Whether any field has a lifetime.
+    pub fn contains_lifetimes(&self) -> bool {
+        !(self.lifetimes.body.is_empty()
+            && self.lifetimes.path.is_empty()
+            && self.lifetimes.query.is_empty()
+            && self.lifetimes.header.is_empty())
+    }
+
+    /// The combination of every fields unique lifetime annotation.
+    pub fn combine_lifetimes(&self) -> TokenStream {
+        util::generics_to_tokens(
+            self.lifetimes
+                .body
+                .iter()
+                .chain(self.lifetimes.path.iter())
+                .chain(self.lifetimes.query.iter())
+                .chain(self.lifetimes.header.iter())
+                .collect::<BTreeSet<_>>()
+                .into_iter(),
+        )
+    }
+
+    /// The lifetimes on fields with the `query` attribute.
+    pub fn query_lifetimes(&self) -> TokenStream {
+        util::generics_to_tokens(self.lifetimes.query.iter())
+    }
+
+    /// The lifetimes on fields with the `body` attribute.
+    pub fn body_lifetimes(&self) -> TokenStream {
+        util::generics_to_tokens(self.lifetimes.body.iter())
     }
 
+    // /// The lifetimes on fields with the `header` attribute.
+    // pub fn header_lifetimes(&self) -> TokenStream {
+    //     util::generics_to_tokens(self.lifetimes.header.iter())
+    // }
+
     /// Produces an iterator over all the header fields.
     pub fn header_fields(&self) -> impl Iterator<Item = &RequestField> {
         self.fields.iter().filter(|field| field.is_header())
@@ -194,6 +252,7 @@ impl TryFrom<RawRequest> for Request {
     fn try_from(raw: RawRequest) -> syn::Result<Self> {
         let mut newtype_body_field = None;
         let mut query_map_field = None;
+        let mut lifetimes = RequestLifetimes::default();
 
         let fields = raw
             .fields
@@ -263,6 +322,16 @@ impl TryFrom<RawRequest> for Request {
                     });
                 }
 
+                match field_kind.unwrap_or(RequestFieldKind::Body) {
+                    RequestFieldKind::Header => util::collect_lifetime_ident(&mut lifetimes.header, &field.ty),
+                    RequestFieldKind::Body => util::collect_lifetime_ident(&mut lifetimes.body, &field.ty),
+                    RequestFieldKind::NewtypeBody => util::collect_lifetime_ident(&mut lifetimes.body, &field.ty),
+                    RequestFieldKind::NewtypeRawBody => util::collect_lifetime_ident(&mut lifetimes.body, &field.ty),
+                    RequestFieldKind::Path => util::collect_lifetime_ident(&mut lifetimes.path, &field.ty),
+                    RequestFieldKind::Query => util::collect_lifetime_ident(&mut lifetimes.query, &field.ty),
+                    RequestFieldKind::QueryMap => util::collect_lifetime_ident(&mut lifetimes.query, &field.ty),
+                }
+
                 Ok(RequestField::new(
                     field_kind.unwrap_or(RequestFieldKind::Body),
                     field,
@@ -287,7 +356,15 @@ impl TryFrom<RawRequest> for Request {
             ));
         }
 
-        Ok(Self { fields })
+        // TODO when/if `&[(&str, &str)]` is supported remove this
+        if query_map_field.is_some() && !lifetimes.query.is_empty() {
+            return Err(syn::Error::new_spanned(
+                raw.request_kw,
+                "Lifetimes are not allowed for query_map fields",
+            ));
+        }
+
+        Ok(Self { fields, lifetimes })
     }
 }
 
@@ -301,26 +378,32 @@ impl ToTokens for Request {
             quote! { { #(#fields),* } }
         };
 
+        let request_generics = self.combine_lifetimes();
+
         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() {
-                    TokenStream::new()
+                // Though we don't track the difference between new type body and body
+                // for lifetimes, the outer check and the macro failing if it encounters
+                // an illegal combination of field attributes, is enough to guarantee `body_lifetimes`
+                // correctness.
+                let (derive_deserialize, lifetimes) = if self.has_body_lifetimes() {
+                    (TokenStream::new(), self.body_lifetimes())
                 } else {
-                    quote!(::ruma_api::exports::serde::Deserialize)
+                    (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new())
                 };
 
-                Some((derive_deserialize, quote! { (#field); }))
+                Some((derive_deserialize, quote! { #lifetimes (#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()
+                let (derive_deserialize, lifetimes) = if self.has_body_lifetimes() {
+                    (TokenStream::new(), self.body_lifetimes())
                 } else {
-                    quote!(::ruma_api::exports::serde::Deserialize)
+                    (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,27 +422,39 @@ 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 (derive_deserialize, lifetime) = if self.has_query_lifetimes() {
+                (TokenStream::new(), self.query_lifetimes())
+            } else {
+                (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new())
+            };
 
             quote! {
                 /// Data in the request's query string.
                 #[derive(
                     Debug,
-                    ::ruma_api::exports::serde::Deserialize,
+                    ::ruma_api::Outgoing,
                     ::ruma_api::exports::serde::Serialize,
+                    #derive_deserialize
                 )]
-                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 (derive_deserialize, lifetime) = if self.has_query_lifetimes() {
+                (TokenStream::new(), self.query_lifetimes())
+            } else {
+                (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new())
+            };
 
             quote! {
                 /// Data in the request's query string.
                 #[derive(
                     Debug,
-                    ::ruma_api::exports::serde::Deserialize,
+                    ::ruma_api::Outgoing,
                     ::ruma_api::exports::serde::Serialize,
+                    #derive_deserialize
                 )]
-                struct RequestQuery {
+                struct RequestQuery #lifetime {
                     #(#fields),*
                 }
             }
@@ -370,9 +465,10 @@ 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 +594,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..839d6b86 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::{
@@ -14,10 +14,19 @@ use crate::{
     util,
 };
 
+#[derive(Debug, Default)]
+pub struct ResponseLifetimes {
+    body: BTreeSet<Lifetime>,
+    header: BTreeSet<Lifetime>,
+}
+
 /// The result of processing the `response` section of the macro.
 pub struct Response {
     /// The fields of the response.
     fields: Vec<ResponseField>,
+
+    /// The collected lifetime identifiers from the declared fields.
+    lifetimes: ResponseLifetimes,
 }
 
 impl Response {
@@ -31,9 +40,34 @@ 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 `body` field has a lifetime annotation.
+    pub fn has_body_lifetimes(&self) -> bool {
+        !self.lifetimes.body.is_empty()
+    }
+
+    /// The number of unique lifetime annotations for `body` fields.
+    pub fn body_lifetime_count(&self) -> usize {
+        self.lifetimes.body.len()
+    }
+
+    /// Whether any field has a lifetime annotation.
+    pub fn contains_lifetimes(&self) -> bool {
+        !(self.lifetimes.body.is_empty() && self.lifetimes.header.is_empty())
+    }
+
+    pub fn combine_lifetimes(&self) -> TokenStream {
+        util::generics_to_tokens(
+            self.lifetimes
+                .body
+                .iter()
+                .chain(self.lifetimes.header.iter())
+                .collect::<BTreeSet<_>>()
+                .into_iter(),
+        )
+    }
+
+    pub fn body_lifetimes(&self) -> TokenStream {
+        util::generics_to_tokens(self.lifetimes.body.iter())
     }
 
     /// Produces code for a response struct initializer.
@@ -162,6 +196,7 @@ impl TryFrom<RawResponse> for Response {
 
     fn try_from(raw: RawResponse) -> syn::Result<Self> {
         let mut newtype_body_field = None;
+        let mut lifetimes = ResponseLifetimes::default();
 
         let fields = raw
             .fields
@@ -212,12 +247,22 @@ impl TryFrom<RawResponse> for Response {
                 }
 
                 Ok(match field_kind.unwrap_or(ResponseFieldKind::Body) {
-                    ResponseFieldKind::Body => ResponseField::Body(field),
+                    ResponseFieldKind::Body => {
+                        util::collect_lifetime_ident(&mut lifetimes.body, &field.ty);
+                        ResponseField::Body(field)
+                    }
                     ResponseFieldKind::Header => {
+                        util::collect_lifetime_ident(&mut lifetimes.header, &field.ty);
                         ResponseField::Header(field, header.expect("missing header name"))
                     }
-                    ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field),
-                    ResponseFieldKind::NewtypeRawBody => ResponseField::NewtypeRawBody(field),
+                    ResponseFieldKind::NewtypeBody => {
+                        util::collect_lifetime_ident(&mut lifetimes.body, &field.ty);
+                        ResponseField::NewtypeBody(field)
+                    }
+                    ResponseFieldKind::NewtypeRawBody => {
+                        util::collect_lifetime_ident(&mut lifetimes.body, &field.ty);
+                        ResponseField::NewtypeRawBody(field)
+                    }
                 })
             })
             .collect::<syn::Result<Vec<_>>>()?;
@@ -230,7 +275,7 @@ impl TryFrom<RawResponse> for Response {
             ));
         }
 
-        Ok(Self { fields })
+        Ok(Self { fields, lifetimes })
     }
 }
 
@@ -248,23 +293,29 @@ impl ToTokens for Response {
         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()
+
+                // Though we don't track the difference between new type body and body
+                // for lifetimes, the outer check and the macro failing if it encounters
+                // an illegal combination of field attributes, is enough to guarantee `body_lifetimes`
+                // correctness.
+                let (derive_deserialize, lifetimes) = if self.has_body_lifetimes() {
+                    (TokenStream::new(), self.body_lifetimes())
                 } else {
-                    quote!(::ruma_api::exports::serde::Deserialize)
+                    (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new())
                 };
 
-                (derive_deserialize, quote! { (#field); })
+                (derive_deserialize, quote! { #lifetimes (#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()
+                let (derive_deserialize, lifetimes) = if self.has_body_lifetimes() {
+                    (TokenStream::new(), self.body_lifetimes())
                 } else {
-                    quote!(::ruma_api::exports::serde::Deserialize)
+                    (quote!(::ruma_api::exports::serde::Deserialize), TokenStream::new())
                 };
+
                 let fields = fields.map(ResponseField::field);
 
-                (derive_deserialize, quote!({ #(#fields),* }))
+                (derive_deserialize, quote!( #lifetimes { #(#fields),* }))
             } else {
                 (TokenStream::new(), quote!({}))
             };
@@ -280,10 +331,11 @@ impl ToTokens for Response {
             struct ResponseBody #def
         };
 
+        let response_generics = self.combine_lifetimes();
         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 +405,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-macros/src/derive_outgoing.rs b/ruma-api-macros/src/derive_outgoing.rs
index 5f914372..adac1938 100644
--- a/ruma-api-macros/src/derive_outgoing.rs
+++ b/ruma-api-macros/src/derive_outgoing.rs
@@ -41,11 +41,11 @@ pub fn expand_derive_outgoing(input: DeriveInput) -> syn::Result<TokenStream> {
     match data {
         DataKind::Unit => Ok(impl_outgoing_with_incoming_self(&input)),
         DataKind::Enum(mut vars) => {
-            let mut any_attribute = false;
+            let mut found_lifetime = false;
             for var in &mut vars {
                 for field in &mut var.fields {
                     if strip_lifetimes(&mut field.ty) {
-                        any_attribute = true;
+                        found_lifetime = true;
                     }
                 }
             }
@@ -53,7 +53,7 @@ pub fn expand_derive_outgoing(input: DeriveInput) -> syn::Result<TokenStream> {
             let original_ident = &input.ident;
             let (original_impl_gen, original_ty_gen, _) = input.generics.split_for_impl();
 
-            if !any_attribute {
+            if !found_lifetime {
                 return Ok(impl_outgoing_with_incoming_self(&input));
             }
 
@@ -75,17 +75,17 @@ pub fn expand_derive_outgoing(input: DeriveInput) -> syn::Result<TokenStream> {
             })
         }
         DataKind::Struct(mut fields, struct_kind) => {
-            let mut any_attribute = false;
+            let mut found_lifetime = false;
             for field in &mut fields {
                 if strip_lifetimes(&mut field.ty) {
-                    any_attribute = true;
+                    found_lifetime = true;
                 }
             }
 
             let original_ident = &input.ident;
             let (original_impl_gen, original_ty_gen, _) = input.generics.split_for_impl();
 
-            if !any_attribute {
+            if !found_lifetime {
                 return Ok(impl_outgoing_with_incoming_self(&input));
             }
 
@@ -121,6 +121,7 @@ fn no_deserialize_in_attrs(attrs: &[Attribute]) -> bool {
 fn impl_outgoing_with_incoming_self(input: &DeriveInput) -> TokenStream {
     let ident = &input.ident;
     let (impl_gen, ty_gen, _) = input.generics.split_for_impl();
+
     quote! {
         impl #impl_gen ::ruma_api::Outgoing for #ident #ty_gen {
             type Incoming = Self;
@@ -146,6 +147,8 @@ fn strip_lifetimes(field_type: &mut Type) -> bool {
         // The IncomingT has to be declared by the user of this derive macro.
         Type::Path(TypePath { path, .. }) => {
             let mut has_lifetimes = false;
+            let mut is_lifetime_generic = false;
+
             for seg in &mut path.segments {
                 // strip generic lifetimes
                 match &mut seg.arguments {
@@ -157,13 +160,15 @@ fn strip_lifetimes(field_type: &mut Type) -> bool {
                             .into_iter()
                             .map(|mut ty| {
                                 if let GenericArgument::Type(ty) = &mut ty {
-                                    strip_lifetimes(ty);
+                                    if strip_lifetimes(ty) {
+                                        has_lifetimes = true;
+                                    };
                                 }
                                 ty
                             })
                             .filter(|arg| {
                                 if let GenericArgument::Lifetime(_) = arg {
-                                    has_lifetimes = true;
+                                    is_lifetime_generic = true;
                                     false
                                 } else {
                                     true
@@ -178,7 +183,9 @@ fn strip_lifetimes(field_type: &mut Type) -> bool {
                             .clone()
                             .into_iter()
                             .map(|mut ty| {
-                                strip_lifetimes(&mut ty);
+                                if strip_lifetimes(&mut ty) {
+                                    has_lifetimes = true;
+                                };
                                 ty
                             })
                             .collect();
@@ -187,14 +194,15 @@ fn strip_lifetimes(field_type: &mut Type) -> bool {
                 }
             }
 
-            if has_lifetimes {
+            // If a type has a generic lifetime parameter there must be an `Incoming` variant of that type.
+            if is_lifetime_generic {
                 if let Some(name) = path.segments.last_mut() {
                     let incoming_ty_ident = format_ident!("Incoming{}", name.ident);
                     name.ident = incoming_ty_ident;
                 }
             }
 
-            has_lifetimes
+            has_lifetimes || is_lifetime_generic
         }
         Type::Reference(TypeReference { elem, .. }) => match &mut **elem {
             Type::Path(ty_path) => {
@@ -227,6 +235,15 @@ fn strip_lifetimes(field_type: &mut Type) -> bool {
             }
             _ => false,
         },
+        Type::Tuple(syn::TypeTuple { elems, .. }) => {
+            let mut has_lifetime = false;
+            for elem in elems {
+                if strip_lifetimes(elem) {
+                    has_lifetime = true;
+                }
+            }
+            has_lifetime
+        }
         _ => false,
     }
 }
diff --git a/ruma-api-macros/src/util.rs b/ruma-api-macros/src/util.rs
index 20d6c7ea..a9c38819 100644
--- a/ruma-api-macros/src/util.rs
+++ b/ruma-api-macros/src/util.rs
@@ -2,10 +2,82 @@
 
 use proc_macro2::{Span, TokenStream};
 use quote::quote;
-use syn::Ident;
+use std::collections::BTreeSet;
+use syn::{
+    AngleBracketedGenericArguments, GenericArgument, Ident, Lifetime,
+    ParenthesizedGenericArguments, PathArguments, Type, TypeArray, TypeBareFn, TypeGroup,
+    TypeParen, TypePath, TypePtr, TypeReference, TypeSlice, TypeTuple,
+};
 
 use crate::api::{metadata::Metadata, request::Request};
 
+pub fn collect_lifetime_ident(lifetimes: &mut BTreeSet<Lifetime>, ty: &Type) {
+    match ty {
+        Type::Path(TypePath { path, .. }) => {
+            for seg in &path.segments {
+                match &seg.arguments {
+                    PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+                        args, ..
+                    }) => {
+                        for gen in args {
+                            if let GenericArgument::Type(ty) = gen {
+                                collect_lifetime_ident(lifetimes, &ty);
+                            } else if let GenericArgument::Lifetime(lt) = gen {
+                                lifetimes.insert(lt.clone());
+                            }
+                        }
+                    }
+                    PathArguments::Parenthesized(ParenthesizedGenericArguments {
+                        inputs, ..
+                    }) => {
+                        for ty in inputs {
+                            collect_lifetime_ident(lifetimes, ty);
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+        Type::Reference(TypeReference { elem, lifetime, .. }) => {
+            collect_lifetime_ident(lifetimes, &*elem);
+            if let Some(lt) = lifetime {
+                lifetimes.insert(lt.clone());
+            }
+        }
+        Type::Tuple(TypeTuple { elems, .. }) => {
+            for ty in elems {
+                collect_lifetime_ident(lifetimes, ty);
+            }
+        }
+        Type::Paren(TypeParen { elem, .. }) => collect_lifetime_ident(lifetimes, &*elem),
+        Type::Group(TypeGroup { elem, .. }) => collect_lifetime_ident(lifetimes, &*elem),
+        Type::Ptr(TypePtr { elem, .. }) => collect_lifetime_ident(lifetimes, &*elem),
+        Type::Slice(TypeSlice { elem, .. }) => collect_lifetime_ident(lifetimes, &*elem),
+        Type::Array(TypeArray { elem, .. }) => collect_lifetime_ident(lifetimes, &*elem),
+        Type::BareFn(TypeBareFn {
+            lifetimes: Some(syn::BoundLifetimes { lifetimes: fn_lifetimes, .. }),
+            ..
+        }) => {
+            for lt in fn_lifetimes {
+                let syn::LifetimeDef { lifetime, .. } = lt;
+                lifetimes.insert(lifetime.clone());
+            }
+        }
+        _ => {}
+    }
+}
+
+/// Generates a `TokenStream` of lifetime identifiers `<'lifetime>`.
+pub fn generics_to_tokens<'a, I: Iterator<Item = &'a Lifetime>>(lifetimes: I) -> TokenStream {
+    let lifetimes = lifetimes.collect::<Vec<_>>();
+    if lifetimes.is_empty() {
+        TokenStream::new()
+    } else {
+        let lifetimes = quote! { #( #lifetimes ),* };
+        quote! { < #lifetimes > }
+    }
+}
+
 /// The first item in the tuple generates code for the request path from
 /// the `Metadata` and `Request` structs. The second item in the returned tuple
 /// is the code to generate a Request struct field created from any segments
@@ -91,7 +163,6 @@ pub(crate) fn request_path_string_and_parse(
 pub(crate) fn build_query_string(request: &Request) -> TokenStream {
     if let Some(field) = request.query_map_field() {
         let field_name = field.ident.as_ref().expect("expected field to have identifier");
-        let field_type = &field.ty;
 
         quote!({
             // This function exists so that the compiler will throw an
@@ -104,13 +175,14 @@ pub(crate) fn build_query_string(request: &Request) -> TokenStream {
             //
             // By asserting that it implements the iterator trait, we can
             // ensure that it won't fail.
-            fn assert_trait_impl<T>()
+            fn assert_trait_impl<T>(_: &T)
             where
                 T: ::std::iter::IntoIterator<Item = (::std::string::String, ::std::string::String)>,
             {}
-            assert_trait_impl::<#field_type>();
 
             let request_query = RequestQuery(self.#field_name);
+            assert_trait_impl(&request_query.0);
+
             format_args!(
                 "?{}",
                 ::ruma_api::exports::ruma_serde::urlencoded::to_string(request_query)?
@@ -147,7 +219,7 @@ pub(crate) fn extract_request_query(request: &Request) -> TokenStream {
         }
     } else if request.has_query_fields() {
         quote! {
-            let request_query: RequestQuery = ::ruma_api::try_deserialize!(
+            let request_query: <RequestQuery as ::ruma_api::Outgoing>::Incoming = ::ruma_api::try_deserialize!(
                 request,
                 ::ruma_api::exports::ruma_serde::urlencoded::from_str(
                     &request.uri().query().unwrap_or("")


Test patch:
diff --git a/ruma-api/tests/outgoing.rs b/ruma-api/tests/outgoing.rs
index 41e8aa1c..94699b0c 100644
--- a/ruma-api/tests/outgoing.rs
+++ b/ruma-api/tests/outgoing.rs
@@ -13,9 +13,16 @@ pub struct IncomingThing<T> {
     t: T,
 }
 
+#[allow(unused)]
+#[derive(Copy, Clone, Debug, Outgoing, serde::Serialize)]
+pub struct OtherThing<'t> {
+    some: &'t str,
+    t: &'t [u8],
+}
+
 #[derive(Outgoing)]
 #[incoming_no_deserialize]
-pub struct Request<'a, T> {
+pub struct FakeRequest<'a, T> {
     pub abc: &'a str,
     pub thing: Thing<'a, T>,
     pub device_id: &'a ::ruma_identifiers::DeviceId,
@@ -23,6 +30,8 @@ pub struct Request<'a, T> {
     pub bytes: &'a [u8],
     pub recursive: &'a [Thing<'a, T>],
     pub option: Option<&'a [u8]>,
+    pub depth: Option<&'a [(&'a str, &'a str)]>,
+    pub arc_type: std::sync::Arc<&'a str>,
 }
 
 #[derive(Outgoing)]
diff --git a/ruma-api/tests/ruma_api_lifetime.rs b/ruma-api/tests/ruma_api_lifetime.rs
new file mode 100644
index 00000000..caba75a3
--- /dev/null
+++ b/ruma-api/tests/ruma_api_lifetime.rs
@@ -0,0 +1,157 @@
+use ruma_identifiers::{RoomAliasId, RoomId};
+
+#[allow(unused)]
+#[derive(Copy, Clone, Debug, ruma_api::Outgoing, serde::Serialize)]
+pub struct OtherThing<'t> {
+    some: &'t str,
+    t: &'t [u8],
+}
+
+mod empty_response {
+    use super::*;
+
+    ruma_api::ruma_api! {
+        metadata: {
+            description: "Add an alias to a room.",
+            method: PUT,
+            name: "create_alias",
+            path: "/_matrix/client/r0/directory/room/:room_alias",
+            rate_limited: false,
+            requires_authentication: true,
+        }
+
+        request: {
+            /// The room alias to set.
+            #[ruma_api(path)]
+            pub room_alias: &'a RoomAliasId,
+
+            /// The room ID to set.
+            pub room_id: &'a RoomId,
+        }
+
+        response: {}
+    }
+}
+
+mod nested_types {
+    use super::*;
+
+    ruma_api::ruma_api! {
+        metadata: {
+            description: "Add an alias to a room.",
+            method: PUT,
+            name: "create_alias",
+            path: "/_matrix/client/r0/directory/room/:room_alias",
+            rate_limited: false,
+            requires_authentication: true,
+        }
+
+        request: {
+            /// The room alias to set.
+            pub room_alias: &'a [Option<&'a RoomAliasId>],
+
+            /// The room ID to set.
+            pub room_id: &'b [Option<Option<&'a ruma_identifiers::DeviceId>>],
+        }
+
+        response: {}
+    }
+}
+
+mod full_request_response {
+    use super::*;
+
+    ruma_api::ruma_api! {
+        metadata: {
+            description: "Does something.",
+            method: POST,
+            name: "no_fields",
+            path: "/_matrix/my/endpoint/:thing",
+            rate_limited: false,
+            requires_authentication: false,
+        }
+
+        request: {
+            #[ruma_api(query)]
+            pub abc: &'a str,
+            #[ruma_api(path)]
+            pub thing: &'a str,
+            #[ruma_api(header = CONTENT_TYPE)]
+            pub stuff: &'a str,
+            pub more: OtherThing<'t>,
+        }
+
+        response: {
+            #[ruma_api(body)]
+            pub thing: OtherThing<'a>,
+            #[ruma_api(header = CONTENT_TYPE)]
+            pub stuff: &'a str,
+        }
+    }
+}
+
+mod full_request_response_with_query_map {
+    use super::*;
+
+    ruma_api::ruma_api! {
+        metadata: {
+            description: "Does something.",
+            method: GET,
+            name: "no_fields",
+            path: "/_matrix/my/endpoint/:thing",
+            rate_limited: false,
+            requires_authentication: false,
+        }
+
+        request: {
+            #[ruma_api(query_map)]
+            // pub abc: &'a [(&'a str, &'a str)], // TODO handle this use case
+            pub abc: Vec<(String, String)>,
+            #[ruma_api(path)]
+            pub thing: &'a str,
+            #[ruma_api(header = CONTENT_TYPE)]
+            pub stuff: &'a str,
+        }
+
+        response: {
+            #[ruma_api(body)]
+            pub thing: OtherThing<'a>,
+            #[ruma_api(header = CONTENT_TYPE)]
+            pub stuff: &'a str,
+        }
+    }
+}
+
+mod query_fields {
+    ruma_api::ruma_api! {
+        metadata: {
+            description: "Get the list of rooms in this homeserver's public directory.",
+            method: GET,
+            name: "get_public_rooms",
+            path: "/_matrix/client/r0/publicRooms",
+            rate_limited: false,
+            requires_authentication: false,
+        }
+
+        request: {
+            /// Limit for the number of results to return.
+            #[serde(skip_serializing_if = "Option::is_none")]
+            #[ruma_api(query)]
+            pub limit: Option<usize>,
+
+            /// Pagination token from a previous request.
+            #[serde(skip_serializing_if = "Option::is_none")]
+            #[ruma_api(query)]
+            pub since: Option<&'a str>,
+
+            /// The server to fetch the public room lists from.
+            ///
+            /// `None` means the server this request is sent to.
+            #[serde(skip_serializing_if = "Option::is_none")]
+            #[ruma_api(query)]
+            pub server: Option<&'a str>,
+        }
+
+        response: { }
+    }
+}