diff --git a/specta-macros/src/type/serde.rs b/specta-macros/src/type/serde.rs index 1783e687..28aa5529 100644 --- a/specta-macros/src/type/serde.rs +++ b/specta-macros/src/type/serde.rs @@ -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; @@ -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::()?; + } else if meta.input.peek(token::Paren) { + meta.input.parse::()?; + } + Ok(()) +} + fn parse_variant_meta(target: &mut VariantAttrs, meta: ParseNestedMeta<'_>) -> Result<()> { if meta.path.is_ident("rename") { parse_rename( @@ -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(()) @@ -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(()) diff --git a/tests/tests/mod.rs b/tests/tests/mod.rs index 1c6314c5..e35cbe6e 100644 --- a/tests/tests/mod.rs +++ b/tests/tests/mod.rs @@ -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; diff --git a/tests/tests/serde_unknown_attrs.rs b/tests/tests/serde_unknown_attrs.rs new file mode 100644 index 00000000..78e6e7ee --- /dev/null +++ b/tests/tests/serde_unknown_attrs.rs @@ -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, +} + +#[derive(Type, Serialize)] +#[specta(collect = false)] +#[serde(bound = "T: Serialize + Clone")] +struct ContainerBound { + 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 { + #[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::(), + 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::>(), + 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::(), + 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::>(), + 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::(), + specta_serde::Format, + ) + .expect("field getter must not affect the exported type"); + assert!(rendered.contains("value: number"), "got: {rendered}"); +}