Skip to content

Commit 1e49daf

Browse files
authored
Merge pull request #33 from maartendeprez/main
Added support for container- and field-level TypeScript type parameters and container-level type overrides: - Introduced `type_override: Option<String>` and `type_params: Option<Vec<String>>` in `TsifyContainerAttrs` and `TsifyFieldAttrs`. - Parses `#[tsify(type = "T")]` and `#[tsify(type_params = "A, B")]` (comma‑separated, trimmed) and rejects duplicate attributes. - Allows specifying TypeScript generic/type-parameter text or a container-wide TS type override; field-level overrides remain authoritative when present.
2 parents 910187f + b2fa5c4 commit 1e49daf

File tree

3 files changed

+134
-9
lines changed

3 files changed

+134
-9
lines changed

tests/type_override.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,62 @@ fn test_generic_struct_with_type_override() {
116116

117117
assert_eq!(Foo::<()>::DECL, expected);
118118
}
119+
120+
#[test]
121+
fn test_generic_struct_with_container_param_override() {
122+
trait Trait {
123+
type Assoc;
124+
}
125+
126+
#[derive(Tsify)]
127+
#[tsify(type_params = "T")]
128+
pub struct Foo<T: Trait> {
129+
#[tsify(type = "T.Assoc")]
130+
bar: T::Assoc,
131+
}
132+
133+
#[derive(Tsify)]
134+
#[tsify(type = "{ Assoc: string }")]
135+
pub struct Bar;
136+
137+
impl Trait for Bar {
138+
type Assoc = String;
139+
}
140+
141+
let expected = indoc! {r#"
142+
export interface Foo<T> {
143+
bar: T.Assoc;
144+
}"#
145+
};
146+
147+
assert_eq!(Foo::<Bar>::DECL, expected);
148+
}
149+
150+
#[test]
151+
fn test_generic_struct_with_field_param_override() {
152+
trait Trait {
153+
type Assoc;
154+
}
155+
156+
#[derive(Tsify)]
157+
pub struct Foo<T: Trait> {
158+
#[tsify(type = "T.Assoc", type_params = "T")]
159+
bar: T::Assoc,
160+
}
161+
162+
#[derive(Tsify)]
163+
#[tsify(type = "{ Assoc: string }")]
164+
pub struct Bar;
165+
166+
impl Trait for Bar {
167+
type Assoc = String;
168+
}
169+
170+
let expected = indoc! {r#"
171+
export interface Foo<T> {
172+
bar: T.Assoc;
173+
}"#
174+
};
175+
176+
assert_eq!(Foo::<Bar>::DECL, expected);
177+
}

tsify-macros/src/attrs.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use serde_derive_internals::ast::Field;
44
/// E.g., through `#[tsify(into_wasm_abi)]`.
55
#[derive(Debug, Default)]
66
pub struct TsifyContainerAttrs {
7+
pub type_override: Option<String>,
8+
pub type_params: Option<Vec<String>>,
79
/// Implement `IntoWasmAbi` for the type.
810
pub into_wasm_abi: bool,
911
/// Implement `FromWasmAbi` for the type.
@@ -41,6 +43,8 @@ impl TypeGenerationConfig {
4143
impl TsifyContainerAttrs {
4244
pub fn from_derive_input(input: &syn::DeriveInput) -> syn::Result<Self> {
4345
let mut attrs = Self {
46+
type_override: None,
47+
type_params: None,
4448
into_wasm_abi: false,
4549
from_wasm_abi: false,
4650
namespace: false,
@@ -53,6 +57,24 @@ impl TsifyContainerAttrs {
5357
}
5458

5559
attr.parse_nested_meta(|meta| {
60+
if meta.path.is_ident("type") {
61+
if attrs.type_override.is_some() {
62+
return Err(meta.error("duplicate attribute"));
63+
}
64+
let lit = meta.value()?.parse::<syn::LitStr>()?;
65+
attrs.type_override = Some(lit.value());
66+
return Ok(());
67+
}
68+
69+
if meta.path.is_ident("type_params") {
70+
if attrs.type_params.is_some() {
71+
return Err(meta.error("duplicate attribute"));
72+
}
73+
let lit = meta.value()?.parse::<syn::LitStr>()?;
74+
attrs.type_params = Some(lit.value().split(',').map(|s| s.trim().to_string()).collect());
75+
return Ok(());
76+
}
77+
5678
if meta.path.is_ident("into_wasm_abi") {
5779
if attrs.into_wasm_abi {
5880
return Err(meta.error("duplicate attribute"));
@@ -137,7 +159,7 @@ impl TsifyContainerAttrs {
137159
return Ok(());
138160
}
139161

140-
Err(meta.error("unsupported tsify attribute, expected one of `into_wasm_abi`, `from_wasm_abi`, `namespace`, `type_prefix`, `type_suffix`, `missing_as_null`, `hashmap_as_object`, `large_number_types_as_bigints`"))
162+
Err(meta.error("unsupported tsify attribute, expected one of `type`, `type_params`, `into_wasm_abi`, `from_wasm_abi`, `namespace`, `type_prefix`, `type_suffix`, `missing_as_null`, `hashmap_as_object`, `large_number_types_as_bigints`"))
141163
})?;
142164
}
143165

@@ -148,13 +170,15 @@ impl TsifyContainerAttrs {
148170
#[derive(Debug, Default)]
149171
pub struct TsifyFieldAttrs {
150172
pub type_override: Option<String>,
173+
pub type_params: Option<Vec<String>>,
151174
pub optional: bool,
152175
}
153176

154177
impl TsifyFieldAttrs {
155178
pub fn from_serde_field(field: &Field) -> syn::Result<Self> {
156179
let mut attrs = Self {
157180
type_override: None,
181+
type_params: None,
158182
optional: false,
159183
};
160184

@@ -173,6 +197,20 @@ impl TsifyFieldAttrs {
173197
return Ok(());
174198
}
175199

200+
if meta.path.is_ident("type_params") {
201+
if attrs.type_params.is_some() {
202+
return Err(meta.error("duplicate attribute"));
203+
}
204+
let lit = meta.value()?.parse::<syn::LitStr>()?;
205+
attrs.type_params = Some(
206+
lit.value()
207+
.split(',')
208+
.map(|s| s.trim().to_string())
209+
.collect(),
210+
);
211+
return Ok(());
212+
}
213+
176214
if meta.path.is_ident("optional") {
177215
if attrs.optional {
178216
return Err(meta.error("duplicate attribute"));
@@ -181,7 +219,7 @@ impl TsifyFieldAttrs {
181219
return Ok(());
182220
}
183221

184-
Err(meta.error("unsupported tsify attribute, expected one of `type` or `optional`"))
222+
Err(meta.error("unsupported tsify attribute, expected one of `type`, `type_params` or `optional`"))
185223
})?;
186224
}
187225

tsify-macros/src/parser.rs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,21 @@ impl<'a> Parser<'a> {
5353
}
5454

5555
pub fn parse(&self) -> Decl {
56-
match self.container.serde_data() {
57-
Data::Struct(style, ref fields) => self.parse_struct(*style, fields),
58-
Data::Enum(ref variants) => self.parse_enum(variants),
56+
if let Some(decl) = &self.container.attrs.type_override {
57+
self.create_type_alias_decl(TsType::Override {
58+
type_override: decl.to_string(),
59+
type_params: self
60+
.container
61+
.generics()
62+
.type_params()
63+
.map(|p| p.ident.to_string())
64+
.collect(),
65+
})
66+
} else {
67+
match self.container.serde_data() {
68+
Data::Struct(style, ref fields) => self.parse_struct(*style, fields),
69+
Data::Enum(ref variants) => self.parse_enum(variants),
70+
}
5971
}
6072
}
6173

@@ -72,7 +84,13 @@ impl<'a> Parser<'a> {
7284
Decl::TsTypeAlias(TsTypeAliasDecl {
7385
id: self.container.ident_str(),
7486
export: true,
75-
type_params: self.create_relevant_type_params(type_ann.type_ref_names()),
87+
type_params: self
88+
.container
89+
.attrs
90+
.type_params
91+
.as_ref()
92+
.cloned()
93+
.unwrap_or_else(|| self.create_relevant_type_params(type_ann.type_ref_names())),
7694
type_ann,
7795
comments: extract_doc_comments(&self.container.serde_container.original.attrs),
7896
})
@@ -89,7 +107,13 @@ impl<'a> Parser<'a> {
89107
type_ref_names.extend(ty.type_ref_names());
90108
});
91109

92-
let type_params = self.create_relevant_type_params(type_ref_names);
110+
let type_params = self
111+
.container
112+
.attrs
113+
.type_params
114+
.as_ref()
115+
.cloned()
116+
.unwrap_or_else(|| self.create_relevant_type_params(type_ref_names));
93117

94118
Decl::TsInterface(TsInterfaceDecl {
95119
id: self.container.ident_str(),
@@ -192,8 +216,12 @@ impl<'a> Parser<'a> {
192216
let type_ann = TsType::from_syn_type(&self.container.attrs.ty_config, field.ty);
193217

194218
if let Some(t) = &ts_attrs.type_override {
195-
let type_ref_names = type_ann.type_ref_names();
196-
let type_params = self.create_relevant_type_params(type_ref_names);
219+
let type_params = if let Some(params) = &ts_attrs.type_params {
220+
params.clone()
221+
} else {
222+
let type_ref_names = type_ann.type_ref_names();
223+
self.create_relevant_type_params(type_ref_names)
224+
};
197225
(
198226
TsType::Override {
199227
type_override: t.clone(),

0 commit comments

Comments
 (0)