Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions specta-macros/src/type/serde.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use proc_macro2::{TokenStream, TokenTree};
use quote::quote;
use syn::{Attribute, LitStr, Meta, Result, Type, meta::ParseNestedMeta};
use syn::{Attribute, Expr, LitStr, Meta, Result, Token, Type, meta::ParseNestedMeta, token};

use super::AttributeScope;

Expand Down Expand Up @@ -210,11 +210,26 @@ fn parse_container_meta(target: &mut ContainerAttrs, meta: ParseNestedMeta<'_>)
target.variant_identifier = true;
} else if meta.path.is_ident("field_identifier") {
target.field_identifier = true;
} else {
skip_unknown_meta(&meta)?;
}

Ok(())
}

/// serde owns and validates its attribute namespace, so keys specta does not model (e.g.
/// container `remote`/`bound`/`expecting`, variant `bound`, field `getter`) are ignored - but
/// their value must still be consumed, or `parse_nested_meta` fails the whole derive with
/// "expected `,`" on the leftover tokens.
fn skip_unknown_meta(meta: &ParseNestedMeta<'_>) -> Result<()> {
if meta.input.peek(Token![=]) {
meta.value()?.parse::<Expr>()?;
} else if meta.input.peek(token::Paren) {
meta.input.parse::<TokenTree>()?;
}
Ok(())
}

fn parse_variant_meta(target: &mut VariantAttrs, meta: ParseNestedMeta<'_>) -> Result<()> {
if meta.path.is_ident("rename") {
parse_rename(
Expand Down Expand Up @@ -250,6 +265,8 @@ fn parse_variant_meta(target: &mut VariantAttrs, meta: ParseNestedMeta<'_>) -> R
target.other = true;
} else if meta.path.is_ident("untagged") {
target.untagged = true;
} else {
skip_unknown_meta(&meta)?;
}

Ok(())
Expand Down Expand Up @@ -287,6 +304,8 @@ fn parse_field_meta(target: &mut FieldAttrs, meta: ParseNestedMeta<'_>) -> Resul
} else if meta.path.is_ident("with") {
target.has_with = true;
parse_string_assignment(&meta)?;
} else {
skip_unknown_meta(&meta)?;
}

Ok(())
Expand Down
1 change: 1 addition & 0 deletions tests/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod semantic;
mod serde_conversions;
mod serde_identifiers;
mod serde_other;
mod serde_unknown_attrs;
mod swift;
mod types;
mod typescript;
Expand Down
106 changes: 106 additions & 0 deletions tests/tests/serde_unknown_attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! specta models only a subset of serde's attribute namespace, and serde validates that
//! namespace itself - so attributes specta does not model must be ignored, not rejected.
//! Ignoring a name-value or list attribute still requires consuming its tokens: returning
//! from the `parse_nested_meta` callback without doing so makes syn fail the whole derive
//! with "expected `,`".
//!
//! The trigger case was `#[serde(remote = "Self")]`, which makes the serde derives emit
//! inherent `serialize`/`deserialize` functions so the trait impls can be written by hand
//! (e.g. to migrate a document before parsing it). Container `bound`/`expecting`, variant
//! `bound`, and field `getter` hit the same paths in each attribute scope.

use serde::{Deserialize, Serialize};
use specta::{Type, Types};
use specta_typescript::Typescript;

#[derive(Type, Serialize, Deserialize)]
#[specta(collect = false)]
#[serde(remote = "Self")]
struct RemoteSelf {
value: i32,
#[serde(default)]
label: Option<String>,
}

#[derive(Type, Serialize)]
#[specta(collect = false)]
#[serde(bound = "T: Serialize + Clone")]
struct ContainerBound<T> {
inner: T,
}

#[derive(Type, Deserialize)]
#[specta(collect = false)]
#[serde(expecting = "a remote self")]
struct ContainerExpecting {
id: u32,
}

#[derive(Type, Serialize)]
#[specta(collect = false)]
enum VariantBound<T> {
#[serde(bound = "T: Serialize")]
Value(T),
Empty,
}

struct Seconds(i32);

impl Seconds {
fn value(&self) -> i32 {
self.0
}
}

// The remote-definition pattern: serde reads the real type's fields through getters.
#[derive(Type, Serialize)]
#[specta(collect = false)]
#[serde(remote = "Seconds")]
struct SecondsDef {
#[serde(getter = "Seconds::value")]
value: i32,
}

#[test]
fn unknown_serde_attributes_are_ignored() {
let rendered = Typescript::default()
.export(
&Types::default().register::<RemoteSelf>(),
specta_serde::Format,
)
.expect("remote = \"Self\" must not affect the exported type");
assert!(rendered.contains("value: number"), "got: {rendered}");
assert!(rendered.contains("label"), "got: {rendered}");

let rendered = Typescript::default()
.export(
&Types::default().register::<ContainerBound<String>>(),
specta_serde::Format,
)
.expect("container bound must not affect the exported type");
assert!(rendered.contains("inner"), "got: {rendered}");

let rendered = Typescript::default()
.export(
&Types::default().register::<ContainerExpecting>(),
specta_serde::Format,
)
.expect("container expecting must not affect the exported type");
assert!(rendered.contains("id: number"), "got: {rendered}");

let rendered = Typescript::default()
.export(
&Types::default().register::<VariantBound<String>>(),
specta_serde::Format,
)
.expect("variant bound must not affect the exported type");
assert!(rendered.contains("Value"), "got: {rendered}");

let rendered = Typescript::default()
.export(
&Types::default().register::<SecondsDef>(),
specta_serde::Format,
)
.expect("field getter must not affect the exported type");
assert!(rendered.contains("value: number"), "got: {rendered}");
}
Loading