Re-add the Outgoing trait and derive macro

The Outgoing trait now has no attributes except incoming_no_deserialize,
it looks for any references and lifetimes in a struct declaration and
removes them. The point of the Outgoing trait is to allow references to
be serialized and owned values to come out of deserialization. It has no
ability to wrap values in Raw (then EventResult).
This commit is contained in:
Devin Ragotzy 2020-07-29 14:17:07 -04:00
parent 8650ae00da
commit c86f0b106b
7 changed files with 394 additions and 87 deletions

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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,
}
}

View File

@ -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()
}

View File

@ -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>;

View File

@ -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>],
}