Repository: ruma/ruma
Pull request #170 (2020-07-30 12:06:41)

Re-implement Outgoing trait with Incoming associated type

mujx: Maybe you need to run `ldconfig` after the libsodium compilation in the Dockerfile?
jimmycuadra: That did the trick! Thanks!
DevinR528: I could move the `ruma_api/tests/outgoing.rs` into the `trybuild` test section I left it to show what was compatible and it was helpful to have something to `cargo expand`.

Patch:
diff --git a/ruma-api-macros/src/api.rs b/ruma-api-macros/src/api.rs
index 0390fb88..db87d774 100644
--- a/ruma-api-macros/src/api.rs
+++ b/ruma-api-macros/src/api.rs
@@ -157,17 +157,18 @@ 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 = ::ruma_api::try_deserialize!(
-                        request,
-                        ::ruma_api::exports::serde_json::from_slice(request.body().as_slice())
-                    );
-                }
-            } else {
-                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())
+                );
+            }
+        } else {
+            TokenStream::new()
+        };
 
         let parse_request_headers = if self.request.has_header_fields() {
             self.request.parse_headers_from_request()
@@ -187,17 +188,18 @@ 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 = ::ruma_api::try_deserialize!(
-                        response,
-                        ::ruma_api::exports::serde_json::from_slice(response.body().as_slice()),
-                    );
-                }
-            } else {
-                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()),
+                );
+            }
+        } else {
+            TokenStream::new()
+        };
 
         let response_init_fields = self.response.init_fields();
 
@@ -215,20 +217,20 @@ impl ToTokens for Api {
         let error = &self.error;
 
         let api = quote! {
-            use ruma_api::exports::serde::de::Error as _;
-            use ruma_api::exports::serde::Deserialize as _;
-            use ruma_api::Endpoint as _;
+            use ::ruma_api::exports::serde::de::Error as _;
+            use ::ruma_api::exports::serde::Deserialize as _;
+            use ::ruma_api::Endpoint as _;
 
             use std::convert::TryInto as _;
 
             #[doc = #request_doc]
             #request_type
 
-            impl std::convert::TryFrom<ruma_api::exports::http::Request<Vec<u8>>> for #request_try_from_type {
-                type Error = ruma_api::error::FromHttpRequestError;
+            impl std::convert::TryFrom<::ruma_api::exports::http::Request<Vec<u8>>> for #request_try_from_type {
+                type Error = ::ruma_api::error::FromHttpRequestError;
 
                 #[allow(unused_variables)]
-                fn try_from(request: ruma_api::exports::http::Request<Vec<u8>>) -> Result<Self, Self::Error> {
+                fn try_from(request: ::ruma_api::exports::http::Request<Vec<u8>>) -> Result<Self, Self::Error> {
                     #extract_request_path
                     #extract_request_query
                     #extract_request_headers
@@ -243,17 +245,17 @@ impl ToTokens for Api {
                 }
             }
 
-            impl std::convert::TryFrom<Request> for ruma_api::exports::http::Request<Vec<u8>> {
-                type Error = ruma_api::error::IntoHttpError;
+            impl std::convert::TryFrom<Request> for ::ruma_api::exports::http::Request<Vec<u8>> {
+                type Error = ::ruma_api::error::IntoHttpError;
 
                 #[allow(unused_mut, unused_variables)]
                 fn try_from(request: Request) -> Result<Self, Self::Error> {
                     let metadata = Request::METADATA;
                     let path_and_query = #request_path_string + &#request_query_string;
-                    let mut http_request = ruma_api::exports::http::Request::new(#request_body);
+                    let mut http_request = ::ruma_api::exports::http::Request::new(#request_body);
 
-                    *http_request.method_mut() = ruma_api::exports::http::Method::#method;
-                    *http_request.uri_mut() = ruma_api::exports::http::uri::Builder::new()
+                    *http_request.method_mut() = ::ruma_api::exports::http::Method::#method;
+                    *http_request.uri_mut() = ::ruma_api::exports::http::uri::Builder::new()
                         .path_and_query(path_and_query.as_str())
                         .build()
                         // The ruma_api! macro guards against invalid path input, but if there are
@@ -269,13 +271,13 @@ impl ToTokens for Api {
             #[doc = #response_doc]
             #response_type
 
-            impl std::convert::TryFrom<Response> for ruma_api::exports::http::Response<Vec<u8>> {
-                type Error = ruma_api::error::IntoHttpError;
+            impl std::convert::TryFrom<Response> for ::ruma_api::exports::http::Response<Vec<u8>> {
+                type Error = ::ruma_api::error::IntoHttpError;
 
                 #[allow(unused_variables)]
                 fn try_from(response: Response) -> Result<Self, Self::Error> {
-                    let response = ruma_api::exports::http::Response::builder()
-                        .header(ruma_api::exports::http::header::CONTENT_TYPE, "application/json")
+                    let response = ::ruma_api::exports::http::Response::builder()
+                        .header(::ruma_api::exports::http::header::CONTENT_TYPE, "application/json")
                         #serialize_response_headers
                         .body(#body)
                         // Since we require header names to come from the `http::header` module,
@@ -285,12 +287,12 @@ impl ToTokens for Api {
                 }
             }
 
-            impl std::convert::TryFrom<ruma_api::exports::http::Response<Vec<u8>>> for #response_try_from_type {
-                type Error = ruma_api::error::FromHttpResponseError<#error>;
+            impl std::convert::TryFrom<::ruma_api::exports::http::Response<Vec<u8>>> for #response_try_from_type {
+                type Error = ::ruma_api::error::FromHttpResponseError<#error>;
 
                 #[allow(unused_variables)]
                 fn try_from(
-                    response: ruma_api::exports::http::Response<Vec<u8>>,
+                    response: ::ruma_api::exports::http::Response<Vec<u8>>,
                 ) -> Result<Self, Self::Error> {
                     if response.status().as_u16() < 400 {
                         #extract_response_headers
@@ -301,22 +303,22 @@ impl ToTokens for Api {
                             #response_init_fields
                         })
                     } else {
-                        match <#error as ruma_api::EndpointError>::try_from_response(response) {
-                            Ok(err) => Err(ruma_api::error::ServerError::Known(err).into()),
-                            Err(response_err) => Err(ruma_api::error::ServerError::Unknown(response_err).into())
+                        match <#error as ::ruma_api::EndpointError>::try_from_response(response) {
+                            Ok(err) => Err(::ruma_api::error::ServerError::Known(err).into()),
+                            Err(response_err) => Err(::ruma_api::error::ServerError::Unknown(response_err).into())
                         }
                     }
                 }
             }
 
-            impl ruma_api::Endpoint for Request {
+            impl ::ruma_api::Endpoint for Request {
                 type Response = Response;
                 type ResponseError = #error;
 
                 /// Metadata for the `#name` endpoint.
-                const METADATA: ruma_api::Metadata = ruma_api::Metadata {
+                const METADATA: ::ruma_api::Metadata = ::ruma_api::Metadata {
                     description: #description,
-                    method: ruma_api::exports::http::Method::#method,
+                    method: ::ruma_api::exports::http::Method::#method,
                     name: #name,
                     path: #path,
                     rate_limited: #rate_limited,
diff --git a/ruma-api-macros/src/api/request.rs b/ruma-api-macros/src/api/request.rs
index 483b9c0e..c60c6cc0 100644
--- a/ruma-api-macros/src/api/request.rs
+++ b/ruma-api-macros/src/api/request.rs
@@ -33,8 +33,8 @@ impl Request {
 
             quote! {
                 headers.append(
-                    ruma_api::exports::http::header::#header_name,
-                    ruma_api::exports::http::header::HeaderValue::from_str(request.#field_name.as_ref())?,
+                    ::ruma_api::exports::http::header::#header_name,
+                    ::ruma_api::exports::http::header::HeaderValue::from_str(request.#field_name.as_ref())?,
                 );
             }
         });
@@ -56,13 +56,13 @@ impl Request {
             let header_name_string = header_name.to_string();
 
             quote! {
-                #field_name: match headers.get(ruma_api::exports::http::header::#header_name)
+                #field_name: match headers.get(::ruma_api::exports::http::header::#header_name)
                     .and_then(|v| v.to_str().ok()) {
                         Some(header) => header.to_owned(),
                         None => {
                             return Err(
-                                ruma_api::error::RequestDeserializationError::new(
-                                    ruma_api::exports::serde_json::Error::missing_field(
+                                ::ruma_api::error::RequestDeserializationError::new(
+                                    ::ruma_api::exports::serde_json::Error::missing_field(
                                         #header_name_string
                                     ),
                                     request,
@@ -310,21 +310,34 @@ impl ToTokens for Request {
         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() };
-                Some(quote! { (#field); })
+                let derive_deserialize = if body_field.has_wrap_incoming_attr() {
+                    TokenStream::new()
+                } else {
+                    quote!(::ruma_api::exports::serde::Deserialize)
+                };
+
+                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 fields = fields.map(RequestField::field);
-                Some(quote! { { #(#fields),* } })
+
+                Some((derive_deserialize, quote! { { #(#fields),* } }))
             } else {
                 None
             }
-            .map(|def| {
+            .map(|(derive_deserialize, def)| {
                 quote! {
                     /// Data in the request body.
                     #[derive(
                         Debug,
-                        ruma_api::exports::serde::Deserialize,
-                        ruma_api::exports::serde::Serialize,
+                        ::ruma_api::Outgoing,
+                        ::ruma_api::exports::serde::Serialize,
+                        #derive_deserialize
                     )]
                     struct RequestBody #def
                 }
@@ -337,8 +350,8 @@ impl ToTokens for Request {
                 /// Data in the request's query string.
                 #[derive(
                     Debug,
-                    ruma_api::exports::serde::Deserialize,
-                    ruma_api::exports::serde::Serialize,
+                    ::ruma_api::exports::serde::Deserialize,
+                    ::ruma_api::exports::serde::Serialize,
                 )]
                 struct RequestQuery(#field);
             }
@@ -349,8 +362,8 @@ impl ToTokens for Request {
                 /// Data in the request's query string.
                 #[derive(
                     Debug,
-                    ruma_api::exports::serde::Deserialize,
-                    ruma_api::exports::serde::Serialize,
+                    ::ruma_api::exports::serde::Deserialize,
+                    ::ruma_api::exports::serde::Serialize,
                 )]
                 struct RequestQuery {
                     #(#fields),*
@@ -361,7 +374,8 @@ impl ToTokens for Request {
         };
 
         let request = quote! {
-            #[derive(Debug, Clone)]
+            #[derive(Debug, Clone, ::ruma_api::Outgoing)]
+            #[incoming_no_deserialize]
             pub struct Request #request_def
 
             #request_body_struct
diff --git a/ruma-api-macros/src/api/response.rs b/ruma-api-macros/src/api/response.rs
index ab450b80..866789ee 100644
--- a/ruma-api-macros/src/api/response.rs
+++ b/ruma-api-macros/src/api/response.rs
@@ -56,9 +56,9 @@ impl Response {
                 }
                 ResponseField::Header(_, header_name) => {
                     quote_spanned! {span=>
-                        #field_name: ruma_api::try_deserialize!(
+                        #field_name: ::ruma_api::try_deserialize!(
                             response,
-                            headers.remove(ruma_api::exports::http::header::#header_name)
+                            headers.remove(::ruma_api::exports::http::header::#header_name)
                                 .expect("response missing expected header")
                                 .to_str()
                             )
@@ -98,7 +98,7 @@ impl Response {
                 let span = field.span();
 
                 Some(quote_spanned! {span=>
-                    .header(ruma_api::exports::http::header::#header_name, response.#field_name)
+                    .header(::ruma_api::exports::http::header::#header_name, response.#field_name)
                 })
             } else {
                 None
@@ -143,7 +143,7 @@ impl Response {
             }
         };
 
-        quote!(ruma_api::exports::serde_json::to_vec(&#body)?)
+        quote!(::ruma_api::exports::serde_json::to_vec(&#body)?)
     }
 
     /// Gets the newtype body field, if this response has one.
@@ -245,28 +245,44 @@ impl ToTokens for Response {
             quote! { { #(#fields),* } }
         };
 
-        let 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() };
-            quote! { (#field); }
-        } else if self.has_body_fields() {
-            let fields = self.fields.iter().filter_map(|f| f.as_body_field());
-            quote!({ #(#fields),* })
-        } else {
-            quote!({})
-        };
+        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)
+                };
+
+                (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),* }))
+            } else {
+                (TokenStream::new(), quote!({}))
+            };
 
         let response_body_struct = quote! {
             /// Data in the response body.
             #[derive(
                 Debug,
-                ruma_api::exports::serde::Deserialize,
-                ruma_api::exports::serde::Serialize,
+                ::ruma_api::Outgoing,
+                ::ruma_api::exports::serde::Serialize,
+                #derive_deserialize
             )]
             struct ResponseBody #def
         };
 
         let response = quote! {
-            #[derive(Debug, Clone)]
+            #[derive(Debug, Clone, ::ruma_api::Outgoing)]
+            #[incoming_no_deserialize]
             pub struct Response #response_def
 
             #response_body_struct
diff --git a/ruma-api-macros/src/derive_outgoing.rs b/ruma-api-macros/src/derive_outgoing.rs
new file mode 100644
index 00000000..1364247d
--- /dev/null
+++ b/ruma-api-macros/src/derive_outgoing.rs
@@ -0,0 +1,162 @@
+use proc_macro2::{Ident, Span, TokenStream};
+use quote::{format_ident, quote};
+use syn::{
+    parse_quote, AngleBracketedGenericArguments, Attribute, Data, DeriveInput, Fields,
+    GenericArgument, GenericParam, Generics, ImplGenerics, PathArguments, Type, TypeGenerics,
+    TypePath, TypeReference, TypeSlice,
+};
+
+enum StructKind {
+    Struct,
+    Tuple,
+}
+
+pub fn expand_derive_outgoing(input: DeriveInput) -> syn::Result<TokenStream> {
+    let derive_deserialize = if no_deserialize_in_attrs(&input.attrs) {
+        TokenStream::new()
+    } else {
+        quote!(::ruma_api::exports::serde::Deserialize)
+    };
+
+    let (mut fields, struct_kind): (Vec<_>, _) = match input.data {
+        Data::Enum(_) | Data::Union(_) => {
+            panic!("#[derive(Outgoing)] is only supported for structs")
+        }
+        Data::Struct(s) => match s.fields {
+            Fields::Named(fs) => (fs.named.into_iter().collect(), StructKind::Struct),
+            Fields::Unnamed(fs) => (fs.unnamed.into_iter().collect(), StructKind::Tuple),
+            Fields::Unit => return Ok(impl_outgoing_with_incoming_self(input.ident)),
+        },
+    };
+
+    let mut any_attribute = false;
+
+    for field in &mut fields {
+        if strip_lifetimes(&mut field.ty) {
+            any_attribute = true;
+        }
+    }
+
+    if !any_attribute {
+        return Ok(impl_outgoing_with_incoming_self(input.ident));
+    }
+
+    let original_ident = &input.ident;
+    let (original_impl_gen, original_ty_gen, _) = input.generics.split_for_impl();
+
+    let vis = input.vis;
+    let doc = format!("'Incoming' variant of [{ty}](struct.{ty}.html).", ty = &input.ident);
+    let incoming_ident = format_ident!("Incoming{}", original_ident, span = Span::call_site());
+    let mut gen_copy = input.generics.clone();
+    let (impl_gen, ty_gen) = split_for_impl_lifetime_less(&mut gen_copy);
+
+    let struct_def = match struct_kind {
+        StructKind::Struct => quote! { { #(#fields,)* } },
+        StructKind::Tuple => quote! { ( #(#fields,)* ); },
+    };
+
+    Ok(quote! {
+        #[doc = #doc]
+        #[derive(Debug, #derive_deserialize)]
+        #vis struct #incoming_ident #ty_gen #struct_def
+
+        impl #original_impl_gen ::ruma_api::Outgoing for #original_ident #original_ty_gen {
+            type Incoming = #incoming_ident #impl_gen;
+        }
+    })
+}
+
+fn no_deserialize_in_attrs(attrs: &[Attribute]) -> bool {
+    attrs.iter().any(|attr| attr.path.is_ident("incoming_no_deserialize"))
+}
+
+fn impl_outgoing_with_incoming_self(ident: Ident) -> TokenStream {
+    quote! {
+        impl ::ruma_api::Outgoing for #ident {
+            type Incoming = Self;
+        }
+    }
+}
+
+fn split_for_impl_lifetime_less(generics: &mut Generics) -> (ImplGenerics, TypeGenerics) {
+    generics.params = generics
+        .params
+        .clone()
+        .into_iter()
+        .filter(|param| !matches!(param, GenericParam::Lifetime(_)))
+        .collect();
+
+    let (impl_gen, ty_gen, _) = generics.split_for_impl();
+    (impl_gen, ty_gen)
+}
+
+fn strip_lifetimes(field_type: &mut Type) -> bool {
+    match field_type {
+        // T<'a> -> IncomingT
+        // The IncomingT has to be declared by the user of this derive macro.
+        Type::Path(TypePath { path, .. }) => {
+            let mut has_lifetimes = false;
+            for seg in &mut path.segments {
+                // strip generic lifetimes
+                if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+                    args, ..
+                }) = &mut seg.arguments
+                {
+                    *args = args
+                        .clone()
+                        .into_iter()
+                        .filter(|arg| {
+                            if let GenericArgument::Lifetime(_) = arg {
+                                has_lifetimes = true;
+                                false
+                            } else {
+                                true
+                            }
+                        })
+                        .collect();
+                }
+            }
+
+            if has_lifetimes {
+                if let Some(name) = path.segments.last_mut() {
+                    let incoming_ty_ident = format_ident!("Incoming{}", name.ident);
+                    name.ident = incoming_ty_ident;
+                }
+            }
+
+            has_lifetimes
+        }
+        Type::Reference(TypeReference { elem, .. }) => match &mut **elem {
+            Type::Path(ty_path) => {
+                let TypePath { path, .. } = ty_path;
+                let segs = path
+                    .segments
+                    .clone()
+                    .into_iter()
+                    .map(|seg| seg.ident.to_string())
+                    .collect::<Vec<_>>();
+
+                if path.is_ident("str") {
+                    // &str -> String
+                    *field_type = parse_quote! { String };
+                } else if segs.contains(&"DeviceId".into()) || segs.contains(&"ServerName".into()) {
+                    // The identifiers that need to be boxed `Box<T>` since they are DST's.
+                    *field_type = parse_quote! { Box<#path> };
+                } else {
+                    // &T -> T
+                    *field_type = Type::Path(ty_path.clone());
+                }
+                true
+            }
+            // &[T] -> Vec<T>
+            Type::Slice(TypeSlice { elem, .. }) => {
+                // Recursively strip the lifetimes of the slice's elements.
+                strip_lifetimes(&mut *elem);
+                *field_type = parse_quote! { Vec<#elem> };
+                true
+            }
+            _ => false,
+        },
+        _ => false,
+    }
+}
diff --git a/ruma-api-macros/src/lib.rs b/ruma-api-macros/src/lib.rs
index 076defa9..51797c8b 100644
--- a/ruma-api-macros/src/lib.rs
+++ b/ruma-api-macros/src/lib.rs
@@ -15,11 +15,15 @@ use std::convert::TryFrom as _;
 
 use proc_macro::TokenStream;
 use quote::ToTokens;
-use syn::parse_macro_input;
+use syn::{parse_macro_input, DeriveInput};
 
-use self::api::{Api, RawApi};
+use self::{
+    api::{Api, RawApi},
+    derive_outgoing::expand_derive_outgoing,
+};
 
 mod api;
+mod derive_outgoing;
 mod util;
 
 #[proc_macro]
@@ -30,3 +34,48 @@ pub fn ruma_api(input: TokenStream) -> TokenStream {
         Err(err) => err.to_compile_error().into(),
     }
 }
+
+/// Derive the `Outgoing` trait, possibly generating an 'Incoming' version of the struct this
+/// derive macro is used on. Specifically, if no `#[wrap_incoming]` attribute is used on any of the
+/// fields of the struct, this simple implementation will be generated:
+///
+/// ```ignore
+/// impl Outgoing for MyType {
+///     type Incoming = Self;
+/// }
+/// ```
+///
+/// If, however, `#[wrap_incoming]` is used (which is the only reason you should ever use this
+/// derive macro manually), a new struct `IncomingT` (where `T` is the type this derive is used on)
+/// is generated, with all of the fields with `#[wrap_incoming]` replaced:
+///
+/// ```ignore
+/// #[derive(Outgoing)]
+/// struct MyType {
+///     pub foo: Foo,
+///     #[wrap_incoming]
+///     pub bar: Bar,
+///     #[wrap_incoming(Baz)]
+///     pub baz: Option<Baz>,
+///     #[wrap_incoming(with EventResult)]
+///     pub x: XEvent,
+///     #[wrap_incoming(YEvent with EventResult)]
+///     pub ys: Vec<YEvent>,
+/// }
+///
+/// // generated
+/// struct IncomingMyType {
+///     pub foo: Foo,
+///     pub bar: IncomingBar,
+///     pub baz: Option<IncomingBaz>,
+///     pub x: EventResult<XEvent>,
+///     pub ys: Vec<EventResult<YEvent>>,
+/// }
+/// ```
+// TODO: Make it clear that `#[wrap_incoming]` and `#[wrap_incoming(Type)]` without the "with" part
+// are (only) useful for fallible deserialization of nested structures.
+#[proc_macro_derive(Outgoing, attributes(wrap_incoming, incoming_no_deserialize))]
+pub fn derive_outgoing(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    expand_derive_outgoing(input).unwrap_or_else(|err| err.to_compile_error()).into()
+}
diff --git a/ruma-api/src/lib.rs b/ruma-api/src/lib.rs
index 13880a15..aba96d46 100644
--- a/ruma-api/src/lib.rs
+++ b/ruma-api/src/lib.rs
@@ -194,8 +194,17 @@ use http::Method;
 ///     }
 /// }
 /// ```
+///
+/// ## Fallible deserialization
+///
+/// All request and response types also derive [`Outgoing`][Outgoing]. As such, to allow fallible
+/// deserialization, you can use the `#[wrap_incoming]` attribute. For details, see the
+/// documentation for [the derive macro](derive.Outgoing.html).
+// TODO: Explain the concept of fallible deserialization before jumping to `ruma_api::Outgoing`
 pub use ruma_api_macros::ruma_api;
 
+pub use ruma_api_macros::Outgoing;
+
 pub mod error;
 /// This module is used to support the generated code from ruma-api-macros.
 /// It is not considered part of ruma-api's public API.
@@ -210,6 +219,18 @@ pub mod exports {
 
 use error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError};
 
+/// A type that can be sent to another party that understands the matrix protocol. If any of the
+/// fields of `Self` don't implement serde's `Deserialize`, you can derive this trait to generate a
+/// corresponding 'Incoming' type that supports deserialization. This is useful for things like
+/// ruma_events' `EventResult` type. For more details, see the [derive macro's documentation][doc].
+///
+/// [doc]: derive.Outgoing.html
+// TODO: Better explain how this trait relates to serde's traits
+pub trait Outgoing {
+    /// The 'Incoming' variant of `Self`.
+    type Incoming;
+}
+
 /// Gives users the ability to define their own serializable/deserializable errors.
 pub trait EndpointError: Sized {
     /// Tries to construct `Self` from an `http::Response`.
@@ -224,14 +245,16 @@ pub trait EndpointError: Sized {
 /// A Matrix API endpoint.
 ///
 /// The type implementing this trait contains any data needed to make a request to the endpoint.
-pub trait Endpoint:
-    TryInto<http::Request<Vec<u8>>, Error = IntoHttpError>
-    + TryFrom<http::Request<Vec<u8>>, Error = FromHttpRequestError>
+pub trait Endpoint: Outgoing + TryInto<http::Request<Vec<u8>>, Error = IntoHttpError>
+where
+    <Self as Outgoing>::Incoming: TryFrom<http::Request<Vec<u8>>, Error = FromHttpRequestError>,
+    <Self::Response as Outgoing>::Incoming: TryFrom<
+        http::Response<Vec<u8>>,
+        Error = FromHttpResponseError<<Self as Endpoint>::ResponseError>,
+    >,
 {
     /// Data returned in a successful response from the endpoint.
-    type Response: TryInto<http::Response<Vec<u8>>, Error = IntoHttpError>
-        + TryFrom<http::Response<Vec<u8>>, Error = FromHttpResponseError<Self::ResponseError>>;
-
+    type Response: Outgoing + TryInto<http::Response<Vec<u8>>, Error = IntoHttpError>;
     /// Error type returned when response from endpoint fails.
     type ResponseError: EndpointError;
 
@@ -242,7 +265,15 @@ pub trait Endpoint:
 /// A Matrix API endpoint that doesn't require authentication.
 ///
 /// This marker trait is to indicate that a type implementing `Endpoint` doesn't require any authentication.
-pub trait NonAuthEndpoint: Endpoint {}
+pub trait NonAuthEndpoint: Endpoint
+where
+    <Self as Outgoing>::Incoming: TryFrom<http::Request<Vec<u8>>, Error = FromHttpRequestError>,
+    <Self::Response as Outgoing>::Incoming: TryFrom<
+        http::Response<Vec<u8>>,
+        Error = FromHttpResponseError<<Self as Endpoint>::ResponseError>,
+    >,
+{
+}
 
 /// Metadata about an API endpoint.
 #[derive(Clone, Debug)]
@@ -302,7 +333,7 @@ mod tests {
                 FromHttpRequestError, FromHttpResponseError, IntoHttpError,
                 RequestDeserializationError, ServerError, Void,
             },
-            Endpoint, Metadata,
+            Endpoint, Metadata, Outgoing,
         };
 
         /// A request to create a new room alias.
@@ -312,6 +343,10 @@ mod tests {
             pub room_alias: RoomAliasId, // path
         }
 
+        impl Outgoing for Request {
+            type Incoming = Self;
+        }
+
         impl Endpoint for Request {
             type Response = Response;
             type ResponseError = Void;
@@ -394,6 +429,10 @@ mod tests {
         #[derive(Clone, Copy, Debug)]
         pub struct Response;
 
+        impl Outgoing for Response {
+            type Incoming = Self;
+        }
+
         impl TryFrom<http::Response<Vec<u8>>> for Response {
             type Error = FromHttpResponseError<Void>;
 


Test patch:
diff --git a/ruma-api/tests/outgoing.rs b/ruma-api/tests/outgoing.rs
new file mode 100644
index 00000000..f793f042
--- /dev/null
+++ b/ruma-api/tests/outgoing.rs
@@ -0,0 +1,25 @@
+use ruma_api::Outgoing;
+use ruma_identifiers::UserId;
+
+#[allow(unused)]
+pub struct Thing<'t, T> {
+    some: &'t str,
+    t: &'t T,
+}
+
+#[derive(Debug)]
+pub struct IncomingThing<T> {
+    some: String,
+    t: T,
+}
+
+#[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>],
+}