Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support #[serde(with = "...")] for struct fields #280

Merged
merged 10 commits into from
Apr 9, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Breaking

- `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]` ([#280](https://github.com/Aleph-Alpha/ts-rs/pull/280))
- Fix incompatibility with serde for `snake_case`, `kebab-case` and `SCREAMING_SNAKE_CASE` ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298))
- `#[ts(rename_all = "...")]` no longer accepts variations in the string's casing, dashes and underscores to make behavior consistent with serde ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298))

Expand Down
17 changes: 17 additions & 0 deletions macros/src/attr/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]`.
Expand Down Expand Up @@ -55,6 +58,8 @@ impl Attr for FieldAttr {
nullable: self.optional.nullable || other.optional.nullable,
},
flatten: self.flatten || other.flatten,
#[cfg(feature = "serde-compat")]
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
Expand All @@ -68,6 +73,14 @@ impl Attr for FieldAttr {
}

fn assert_validity(&self, field: &Self::Item) -> Result<()> {
#[cfg(feature = "serde-compat")]
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 = "...")]`"#
)
}

if self.type_override.is_some() {
if self.type_as.is_some() {
syn_err_spanned!(field; "`type` is not compatible with `as`")
Expand Down Expand Up @@ -183,5 +196,9 @@ impl_parse! {
parse_assign_str(input)?;
}
},
"with" => {
parse_assign_str(input)?;
out.0.using_serde_with = true;
},
}
}
3 changes: 3 additions & 0 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ fn format_field(
optional,
flatten,
docs,

#[cfg(feature = "serde-compat")]
using_serde_with: _,
} = field_attr;

if skip {
Expand Down
1 change: 1 addition & 0 deletions macros/src/types/newtype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
type_override,
inline,
skip,
docs: _,
..
} = field_attr;

Expand Down
3 changes: 3 additions & 0 deletions macros/src/types/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ fn format_field(
optional: _,
flatten: _,
docs: _,

#[cfg(feature = "serde-compat")]
using_serde_with: _,
} = field_attr;

if skip {
Expand Down
66 changes: 66 additions & 0 deletions ts-rs/tests/serde_with.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#![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, Deserializer, Serialize, Serializer};

use super::Foo;

pub fn serialize<S: Serializer>(foo: &Foo, serializer: S) -> Result<S::Ok, S::Error> {
foo.serialize(serializer)
}

pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Foo, D::Error> {
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 }, }")
}
Loading