From fbcfe61a03a797529cffe11522c500b9ef75fbe0 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 21 Mar 2024 10:55:40 -0300 Subject: [PATCH 1/8] Initial support for serde(with) --- macros/src/attr/field.rs | 12 ++++++++++++ macros/src/attr/struct.rs | 3 +-- macros/src/types/named.rs | 10 ++++++++++ macros/src/types/newtype.rs | 11 +++++++++++ macros/src/types/tuple.rs | 11 +++++++++++ ts-rs/src/lib.rs | 4 ++-- ts-rs/tests/serde_json.rs | 2 +- 7 files changed, 48 insertions(+), 5 deletions(-) diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index 7345eb4a..1a302523 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -13,6 +13,9 @@ pub struct FieldAttr { pub optional: Optional, pub flatten: bool, pub docs: String, + + #[cfg(feature = "serde-compat")] + pub using_serde_with: bool, } /// Indicates whether the field is marked with `#[ts(optional)]`. @@ -52,6 +55,9 @@ impl FieldAttr { optional: Optional { optional, nullable }, flatten, docs, + + #[cfg(feature = "serde-compat")] + using_serde_with, }: FieldAttr, ) { self.rename = self.rename.take().or(rename); @@ -65,6 +71,11 @@ impl FieldAttr { }; self.flatten |= flatten; self.docs.push_str(&docs); + + #[cfg(feature = "serde-compat")] + { + self.using_serde_with = self.using_serde_with || using_serde_with; + } } } @@ -110,5 +121,6 @@ impl_parse! { parse_assign_str(input)?; } }, + "with" => out.0.using_serde_with = true, } } diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index ce58a0e5..bd177f6c 100644 --- a/macros/src/attr/struct.rs +++ b/macros/src/attr/struct.rs @@ -3,9 +3,8 @@ use std::collections::HashMap; use syn::{parse_quote, Attribute, Ident, Path, Result, Type, WherePredicate}; use super::{parse_assign_from_str, parse_bound, parse_concrete}; -use crate::attr::EnumAttr; use crate::{ - attr::{parse_assign_str, Inflection, VariantAttr}, + attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr}, utils::{parse_attrs, parse_docs}, }; diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index c84fa6c1..8ed8bfaf 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -90,12 +90,22 @@ fn format_field( optional, flatten, docs, + #[cfg(feature = "serde-compat")] + using_serde_with, } = FieldAttr::from_attrs(&field.attrs)?; if skip { return Ok(()); } + #[cfg(feature = "serde-compat")] + if using_serde_with && !(type_as.is_some() || type_override.is_some()) { + syn_err_spanned!( + field; + r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"# + ) + } + if type_as.is_some() && type_override.is_some() { syn_err_spanned!(field; "`type` is not compatible with `as`") } diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index df198da5..f87c68c8 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -24,10 +24,21 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> optional, flatten, docs: _, + + #[cfg(feature = "serde-compat")] + using_serde_with, } = FieldAttr::from_attrs(&inner.attrs)?; let crate_rename = attr.crate_rename(); + #[cfg(feature = "serde-compat")] + if using_serde_with && !(type_as.is_some() || type_override.is_some()) { + syn_err_spanned!( + fields; + r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"# + ) + } + match (&rename_inner, skip, optional.optional, flatten) { (Some(_), ..) => syn_err_spanned!(fields; "`rename` is not applicable to newtype fields"), (_, true, ..) => return super::unit::null(attr, name), diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index e85ca1a3..4c8fd5f5 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -62,6 +62,9 @@ fn format_field( optional, flatten, docs: _, + + #[cfg(feature = "serde-compat")] + using_serde_with, } = FieldAttr::from_attrs(&field.attrs)?; if skip { @@ -70,6 +73,14 @@ fn format_field( let ty = type_as.as_ref().unwrap_or(&field.ty).clone(); + #[cfg(feature = "serde-compat")] + if using_serde_with && !(type_as.is_some() || type_override.is_some()) { + syn_err_spanned!( + field; + r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"# + ) + } + if type_as.is_some() && type_override.is_some() { syn_err_spanned!(field; "`type` is not compatible with `as`") } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 5294013c..8b8cda08 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -138,11 +138,11 @@ pub use ts_rs_macros::TS; pub use crate::export::ExportError; use crate::typelist::TypeList; -#[cfg(feature = "serde-json-impl")] -mod serde_json; #[cfg(feature = "chrono-impl")] mod chrono; mod export; +#[cfg(feature = "serde-json-impl")] +mod serde_json; pub mod typelist; /// A type which can be represented in TypeScript. diff --git a/ts-rs/tests/serde_json.rs b/ts-rs/tests/serde_json.rs index 08598fe0..29e32c78 100644 --- a/ts-rs/tests/serde_json.rs +++ b/ts-rs/tests/serde_json.rs @@ -61,5 +61,5 @@ fn inlined_value() { #[derive(TS)] #[ts(export, export_to = "serde_json_impl/")] struct Simple { - json: serde_json::Value + json: serde_json::Value, } From b4298413a190444e76c18fda86ecb91b2d9da672 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 21 Mar 2024 11:02:01 -0300 Subject: [PATCH 2/8] Properly parse serde(with) --- macros/src/attr/field.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index 1a302523..871e16d6 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -121,6 +121,9 @@ impl_parse! { parse_assign_str(input)?; } }, - "with" => out.0.using_serde_with = true, + "with" => { + parse_assign_str(input)?; + out.0.using_serde_with = true; + }, } } From 54abcd5119a59820d8dbe3ebf01058877552ec8b Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 21 Mar 2024 11:27:41 -0300 Subject: [PATCH 3/8] Add tests --- ts-rs/tests/serde_with.rs | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 ts-rs/tests/serde_with.rs diff --git a/ts-rs/tests/serde_with.rs b/ts-rs/tests/serde_with.rs new file mode 100644 index 00000000..1edf0f0b --- /dev/null +++ b/ts-rs/tests/serde_with.rs @@ -0,0 +1,65 @@ +#![allow(unused, dead_code, clippy::disallowed_names)] + +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, TS)] +struct Foo { + a: i32, +} + +#[derive(Serialize, Deserialize, TS)] +struct Bar { + a: i32, +} + +mod deser { + use serde::{Deserialize, Serialize, Serializer, Deserializer}; + use super::Foo; + + pub fn serialize(foo: &Foo, serializer: S) -> Result { + foo.serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + Foo::deserialize(deserializer) + } +} + +// This test should pass when serde-compat is disabled, +// otherwise, it should fail to compile +#[test] +#[cfg(not(feature = "serde-compat"))] +fn no_serde_compat() { + #[derive(Serialize, Deserialize, TS)] + struct Baz { + #[serde(with = "deser")] + a: Foo, + } + + assert_eq!(Baz::inline(), "{ a: Foo, }") +} + +#[test] +fn serde_compat_as() { + #[derive(Serialize, Deserialize, TS)] + struct Baz { + #[serde(with = "deser")] + #[ts(as = "Bar")] + a: Foo, + } + + assert_eq!(Baz::inline(), "{ a: Bar, }") +} + +#[test] +fn serde_compat_type() { + #[derive(Serialize, Deserialize, TS)] + struct Baz { + #[serde(with = "deser")] + #[ts(type = "{ a: number }")] + a: Foo, + } + + assert_eq!(Baz::inline(), "{ a: { a: number }, }") +} From 5579fd951bf9dcdc8b1f2f0d10f9fe10e3e66fd9 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 21 Mar 2024 11:39:14 -0300 Subject: [PATCH 4/8] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- ts-rs/tests/serde_with.rs | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ee3f0c..7aef5db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # master + ### Breaking + +- `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]` ([#280](https://github.com/Aleph-Alpha/ts-rs/pull/280)) + ### Features -- Add `#[ts(crate = "..")]` to allow usage of `#[derive(TS)]` from other proc-macro crates ([#274](https://github.com/Aleph-Alpha/ts-rs/pull/274)) + +- Add `#[ts(crate = "..")]` to allow usage of `#[derive(TS)]` from other proc-macro crates ([#274](https://github.com/Aleph-Alpha/ts-rs/pull/274)) - Add support types from `serde_json` behind cargo feature `serde-json-impl` ([#276](https://github.com/Aleph-Alpha/ts-rs/pull/276)) ### Fixes diff --git a/ts-rs/tests/serde_with.rs b/ts-rs/tests/serde_with.rs index 1edf0f0b..edc13494 100644 --- a/ts-rs/tests/serde_with.rs +++ b/ts-rs/tests/serde_with.rs @@ -14,7 +14,8 @@ struct Bar { } mod deser { - use serde::{Deserialize, Serialize, Serializer, Deserializer}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use super::Foo; pub fn serialize(foo: &Foo, serializer: S) -> Result { From e89685bfc9680282aa39e7deae44a92f10a03e44 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:36:50 -0300 Subject: [PATCH 5/8] Fix compiler error --- macros/src/attr/field.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index 92c57e5b..74f4ec07 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -59,7 +59,7 @@ impl Attr for FieldAttr { }, flatten: self.flatten || other.flatten, #[cfg(feature = "serde-compat")] - using_serde_with = self.using_serde_with || other.using_serde_with; + using_serde_with: self.using_serde_with || other.using_serde_with, // We can't emit TSDoc for a flattened field // and we cant make this invalid in assert_validity because From 8b792dfeeea82f033e77851323b7b1e5c499807d Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:38:07 -0300 Subject: [PATCH 6/8] Fix compiler error --- macros/src/attr/field.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index 74f4ec07..4dc4d2aa 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -74,7 +74,7 @@ impl Attr for FieldAttr { fn assert_validity(&self, field: &Self::Item) -> Result<()> { #[cfg(feature = "serde-compat")] - if using_serde_with && !(type_as.is_some() || type_override.is_some()) { + if self.using_serde_with && !(self.type_as.is_some() || self.type_override.is_some()) { syn_err_spanned!( field; r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"# From 2527c50441dfdcb00bfaf58945be21b161cb3c08 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:39:44 -0300 Subject: [PATCH 7/8] Discard unused value --- macros/src/types/newtype.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index 9bc0ae3a..e993386f 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -19,9 +19,6 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> inline, skip, docs: _, - - #[cfg(feature = "serde-compat")] - using_serde_with, .. } = field_attr; From ebee29100cfc30c5a1afc9695161dbea14bdb662 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:40:53 -0300 Subject: [PATCH 8/8] Discard unused value --- macros/src/types/named.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 7a248509..10ac25db 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -103,7 +103,7 @@ fn format_field( docs, #[cfg(feature = "serde-compat")] - using_serde_with, + using_serde_with: _, } = field_attr; if skip {