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: ::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: ::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>> for #request_try_from_type { - type Error = ruma_api::error::FromHttpRequestError; + impl std::convert::TryFrom<::ruma_api::exports::http::Request>> for #request_try_from_type { + type Error = ::ruma_api::error::FromHttpRequestError; #[allow(unused_variables)] - fn try_from(request: ruma_api::exports::http::Request>) -> Result { + fn try_from(request: ::ruma_api::exports::http::Request>) -> Result { #extract_request_path #extract_request_query #extract_request_headers @@ -243,17 +245,17 @@ impl ToTokens for Api { } } - impl std::convert::TryFrom for ruma_api::exports::http::Request> { - type Error = ruma_api::error::IntoHttpError; + impl std::convert::TryFrom for ::ruma_api::exports::http::Request> { + type Error = ::ruma_api::error::IntoHttpError; #[allow(unused_mut, unused_variables)] fn try_from(request: Request) -> Result { 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 for ruma_api::exports::http::Response> { - type Error = ruma_api::error::IntoHttpError; + impl std::convert::TryFrom for ::ruma_api::exports::http::Response> { + type Error = ::ruma_api::error::IntoHttpError; #[allow(unused_variables)] fn try_from(response: Response) -> Result { - 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>> for #response_try_from_type { - type Error = ruma_api::error::FromHttpResponseError<#error>; + impl std::convert::TryFrom<::ruma_api::exports::http::Response>> for #response_try_from_type { + type Error = ::ruma_api::error::FromHttpResponseError<#error>; #[allow(unused_variables)] fn try_from( - response: ruma_api::exports::http::Response>, + response: ::ruma_api::exports::http::Response>, ) -> Result { 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 { + 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::>(); + + 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` since they are DST's. + *field_type = parse_quote! { Box<#path> }; + } else { + // &T -> T + *field_type = Type::Path(ty_path.clone()); + } + true + } + // &[T] -> Vec + 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, +/// #[wrap_incoming(with EventResult)] +/// pub x: XEvent, +/// #[wrap_incoming(YEvent with EventResult)] +/// pub ys: Vec, +/// } +/// +/// // generated +/// struct IncomingMyType { +/// pub foo: Foo, +/// pub bar: IncomingBar, +/// pub baz: Option, +/// pub x: EventResult, +/// pub ys: Vec>, +/// } +/// ``` +// 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>, Error = IntoHttpError> - + TryFrom>, Error = FromHttpRequestError> +pub trait Endpoint: Outgoing + TryInto>, Error = IntoHttpError> +where + ::Incoming: TryFrom>, Error = FromHttpRequestError>, + ::Incoming: TryFrom< + http::Response>, + Error = FromHttpResponseError<::ResponseError>, + >, { /// Data returned in a successful response from the endpoint. - type Response: TryInto>, Error = IntoHttpError> - + TryFrom>, Error = FromHttpResponseError>; - + type Response: Outgoing + TryInto>, 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 + ::Incoming: TryFrom>, Error = FromHttpRequestError>, + ::Incoming: TryFrom< + http::Response>, + Error = FromHttpResponseError<::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>> for Response { type Error = FromHttpResponseError; 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 { + 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>], +}