api: Disallow #[serde(flatten)] for single-body-fields of requests and responses
`#[ruma_api(body)]` must be used instead.
This commit is contained in:
parent
3ca8adaadf
commit
f05d0e03a1
@ -37,7 +37,7 @@ pub mod v3 {
|
||||
pub user_id: OwnedUserId,
|
||||
|
||||
/// Whether the user is typing within a length of time or not.
|
||||
#[serde(flatten)]
|
||||
#[ruma_api(body)]
|
||||
pub state: Typing,
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,10 @@
|
||||
# [unreleased]
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `#[serde(flatten)]` on the only body field of a `#[request]` or `#[response]`
|
||||
struct is disallowed. `#[ruma_api(body)]` must be used instead.
|
||||
|
||||
Improvements:
|
||||
|
||||
- The `ruma_identifiers_storage` compile-time `cfg` setting can also be
|
||||
|
@ -7,4 +7,6 @@ fn ui() {
|
||||
t.pass("tests/it/api/ui/response-only.rs");
|
||||
t.compile_fail("tests/it/api/ui/deprecated-without-added.rs");
|
||||
t.compile_fail("tests/it/api/ui/removed-without-deprecated.rs");
|
||||
t.compile_fail("tests/it/api/ui/serde-flatten-request-body.rs");
|
||||
t.compile_fail("tests/it/api/ui/serde-flatten-response-body.rs");
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
metadata,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomRequestBody {
|
||||
pub bar: String,
|
||||
}
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: POST, // An `http::Method` constant. No imports required.
|
||||
rate_limited: false,
|
||||
authentication: None,
|
||||
history: {
|
||||
unstable => "/_matrix/some/endpoint",
|
||||
}
|
||||
};
|
||||
|
||||
#[request]
|
||||
pub struct Request {
|
||||
#[serde(flatten)]
|
||||
pub foo: CustomRequestBody,
|
||||
}
|
||||
|
||||
#[response]
|
||||
pub struct Response;
|
||||
|
||||
fn main() {}
|
@ -0,0 +1,6 @@
|
||||
error: Use `#[ruma_api(body)]` to represent the JSON body as a single field
|
||||
--> tests/it/api/ui/serde-flatten-request-body.rs:23:5
|
||||
|
|
||||
23 | / #[serde(flatten)]
|
||||
24 | | pub foo: CustomRequestBody,
|
||||
| |______________________________^
|
@ -0,0 +1,30 @@
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
metadata,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomResponseBody {
|
||||
pub bar: String,
|
||||
}
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: GET, // An `http::Method` constant. No imports required.
|
||||
rate_limited: false,
|
||||
authentication: None,
|
||||
history: {
|
||||
unstable => "/_matrix/some/endpoint",
|
||||
}
|
||||
};
|
||||
|
||||
#[request]
|
||||
pub struct Request;
|
||||
|
||||
#[response]
|
||||
pub struct Response {
|
||||
#[serde(flatten)]
|
||||
pub foo: CustomResponseBody,
|
||||
}
|
||||
|
||||
fn main() {}
|
@ -0,0 +1,33 @@
|
||||
error: Use `#[ruma_api(body)]` to represent the JSON body as a single field
|
||||
--> tests/it/api/ui/serde-flatten-response-body.rs:26:5
|
||||
|
|
||||
26 | / #[serde(flatten)]
|
||||
27 | | pub foo: CustomResponseBody,
|
||||
| |_______________________________^
|
||||
|
||||
error[E0277]: the trait bound `Response: IncomingResponse` is not satisfied
|
||||
--> tests/it/api/ui/serde-flatten-response-body.rs:21:1
|
||||
|
|
||||
21 | #[request]
|
||||
| ^^^^^^^^^^ the trait `IncomingResponse` is not implemented for `Response`
|
||||
|
|
||||
note: required by a bound in `ruma_common::api::OutgoingRequest::IncomingResponse`
|
||||
--> src/api.rs
|
||||
|
|
||||
| type IncomingResponse: IncomingResponse<EndpointError = Self::EndpointError>;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `OutgoingRequest::IncomingResponse`
|
||||
= note: this error originates in the derive macro `::ruma_common::exports::ruma_macros::Request` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0277]: the trait bound `Response: OutgoingResponse` is not satisfied
|
||||
--> tests/it/api/ui/serde-flatten-response-body.rs:21:1
|
||||
|
|
||||
21 | #[request]
|
||||
| ^^^^^^^^^^ the trait `OutgoingResponse` is not implemented for `Response`
|
||||
|
|
||||
= help: the trait `OutgoingResponse` is implemented for `MatrixError`
|
||||
note: required by a bound in `ruma_common::api::IncomingRequest::OutgoingResponse`
|
||||
--> src/api.rs
|
||||
|
|
||||
| type OutgoingResponse: OutgoingResponse;
|
||||
| ^^^^^^^^^^^^^^^^ required by this bound in `IncomingRequest::OutgoingResponse`
|
||||
= note: this error originates in the derive macro `::ruma_common::exports::ruma_macros::Request` (in Nightly builds, run with -Z macro-backtrace for more info)
|
@ -11,7 +11,7 @@ use super::{
|
||||
attribute::{DeriveRequestMeta, RequestMeta},
|
||||
ensure_feature_presence,
|
||||
};
|
||||
use crate::util::{import_ruma_common, PrivateField};
|
||||
use crate::util::{field_has_serde_flatten_attribute, import_ruma_common, PrivateField};
|
||||
|
||||
mod incoming;
|
||||
mod outgoing;
|
||||
@ -251,9 +251,10 @@ impl Request {
|
||||
}
|
||||
};
|
||||
|
||||
let has_body_fields = self.fields.iter().any(|f| matches!(&f.kind, RequestFieldKind::Body));
|
||||
let has_query_fields =
|
||||
self.fields.iter().any(|f| matches!(&f.kind, RequestFieldKind::Query));
|
||||
let mut body_fields =
|
||||
self.fields.iter().filter(|f| matches!(f.kind, RequestFieldKind::Body));
|
||||
let first_body_field = body_fields.next();
|
||||
let has_body_fields = first_body_field.is_some();
|
||||
|
||||
if has_newtype_body_field && has_body_fields {
|
||||
return Err(syn::Error::new_spanned(
|
||||
@ -262,6 +263,18 @@ impl Request {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(first_body_field) = first_body_field {
|
||||
let is_single_body_field = body_fields.next().is_none();
|
||||
|
||||
if is_single_body_field && field_has_serde_flatten_attribute(&first_body_field.inner) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
first_body_field,
|
||||
"Use `#[ruma_api(body)]` to represent the JSON body as a single field",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let has_query_fields = self.has_query_fields();
|
||||
if has_query_all_field && has_query_fields {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&self.ident,
|
||||
|
@ -14,7 +14,7 @@ use super::{
|
||||
attribute::{DeriveResponseMeta, ResponseMeta},
|
||||
ensure_feature_presence,
|
||||
};
|
||||
use crate::util::{import_ruma_common, PrivateField};
|
||||
use crate::util::{field_has_serde_flatten_attribute, import_ruma_common, PrivateField};
|
||||
|
||||
mod incoming;
|
||||
mod outgoing;
|
||||
@ -213,8 +213,11 @@ impl Response {
|
||||
}
|
||||
};
|
||||
|
||||
let has_body_fields =
|
||||
self.fields.iter().any(|f| matches!(&f.kind, ResponseFieldKind::Body));
|
||||
let mut body_fields =
|
||||
self.fields.iter().filter(|f| matches!(f.kind, ResponseFieldKind::Body));
|
||||
let first_body_field = body_fields.next();
|
||||
let has_body_fields = first_body_field.is_some();
|
||||
|
||||
if has_newtype_body_field && has_body_fields {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&self.ident,
|
||||
@ -222,6 +225,17 @@ impl Response {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(first_body_field) = first_body_field {
|
||||
let is_single_body_field = body_fields.next().is_none();
|
||||
|
||||
if is_single_body_field && field_has_serde_flatten_attribute(&first_body_field.inner) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
first_body_field,
|
||||
"Use `#[ruma_api(body)]` to represent the JSON body as a single field",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use proc_macro_crate::{crate_name, FoundCrate};
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use syn::{Field, Ident, LitStr};
|
||||
use syn::{Attribute, Field, Ident, LitStr};
|
||||
|
||||
pub(crate) fn import_ruma_common() -> TokenStream {
|
||||
if let Ok(FoundCrate::Name(name)) = crate_name("ruma-common") {
|
||||
@ -202,3 +202,28 @@ pub fn cfg_expand_struct(item: &mut syn::ItemStruct) {
|
||||
fields.named.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the given field has a `#[serde(flatten)]` attribute.
|
||||
pub fn field_has_serde_flatten_attribute(field: &Field) -> bool {
|
||||
field.attrs.iter().any(is_serde_flatten_attribute)
|
||||
}
|
||||
|
||||
/// Whether the given attribute is a `#[serde(flatten)]` attribute.
|
||||
fn is_serde_flatten_attribute(attr: &Attribute) -> bool {
|
||||
if !attr.path().is_ident("serde") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut contains_flatten = false;
|
||||
let _ = attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("flatten") {
|
||||
contains_flatten = true;
|
||||
// Return an error to stop the parsing early.
|
||||
return Err(meta.error("found"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
contains_flatten
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user