Skip to content

Commit 01f56be

Browse files
feat: support for tuple variants of enums
closes Failing for Internally/Adjacently tagged #58 partially addresses Enhanced enum support and types #55
1 parent 97ca4cc commit 01f56be

File tree

14 files changed

+355
-68
lines changed

14 files changed

+355
-68
lines changed

Diff for: src/to_typescript/enums.rs

+71-56
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::typescript::convert_type;
21
use crate::{utils, BuildState};
32
use convert_case::{Case, Casing};
43
use syn::__private::ToTokens;
@@ -9,26 +8,6 @@ use syn::__private::ToTokens;
98
/// `rename_all` attributes for the name of the tag will also be adhered to.
109
impl super::ToTypescript for syn::ItemEnum {
1110
fn convert_to_ts(self, state: &mut BuildState, config: &crate::BuildSettings) {
12-
// check we don't have any tuple structs that could mess things up.
13-
// if we do ignore this struct
14-
for variant in self.variants.iter() {
15-
// allow single-field tuple structs to pass through as newtype structs
16-
let mut is_newtype = false;
17-
for f in variant.fields.iter() {
18-
if f.ident.is_none() {
19-
// If we already marked this variant as a newtype, we have a multi-field tuple struct
20-
if is_newtype {
21-
if crate::DEBUG.try_get().is_some_and(|d| *d) {
22-
println!("#[tsync] failed for enum {}", self.ident);
23-
}
24-
return;
25-
} else {
26-
is_newtype = true;
27-
}
28-
}
29-
}
30-
}
31-
3211
state.types.push('\n');
3312

3413
let comments = utils::get_comments(self.clone().attrs);
@@ -42,7 +21,15 @@ impl super::ToTypescript for syn::ItemEnum {
4221

4322
// always use output the internally_tagged representation if the tag is present
4423
if let Some(tag_name) = utils::get_attribute_arg("serde", "tag", &self.attrs) {
45-
add_internally_tagged_enum(tag_name, self, state, casing, config.uses_type_interface)
24+
let content_name = utils::get_attribute_arg("serde", "content", &self.attrs);
25+
add_internally_tagged_enum(
26+
tag_name,
27+
content_name,
28+
self,
29+
state,
30+
casing,
31+
config.uses_type_interface,
32+
)
4633
} else if is_single {
4734
if utils::has_attribute_arg("derive", "Serialize_repr", &self.attrs) {
4835
add_numeric_enum(self, state, casing, config)
@@ -208,6 +195,7 @@ fn add_numeric_enum(
208195
/// ```
209196
fn add_internally_tagged_enum(
210197
tag_name: String,
198+
content_name: Option<String>,
211199
exported_struct: syn::ItemEnum,
212200
state: &mut BuildState,
213201
casing: Option<Case>,
@@ -222,7 +210,7 @@ fn add_internally_tagged_enum(
222210

223211
for variant in exported_struct.variants.iter() {
224212
// Assumes that non-newtype tuple variants have already been filtered out
225-
if variant.fields.iter().any(|v| v.ident.is_none()) {
213+
if variant.fields.iter().any(|v| v.ident.is_none()) && content_name.is_none() {
226214
// TODO: Generate newtype structure
227215
// This should contain the discriminant plus all fields of the inner structure as a flat structure
228216
// TODO: Check for case where discriminant name matches an inner structure field name
@@ -240,31 +228,62 @@ fn add_internally_tagged_enum(
240228
state.types.push_str(";\n");
241229

242230
for variant in exported_struct.variants {
243-
// Assumes that non-newtype tuple variants have already been filtered out
244-
if !variant.fields.iter().any(|v| v.ident.is_none()) {
245-
state.types.push('\n');
246-
let comments = utils::get_comments(variant.attrs);
247-
state.write_comments(&comments, 0);
248-
state.types.push_str(&format!(
249-
"type {interface_name}__{variant_name} = ",
250-
interface_name = exported_struct.ident,
251-
variant_name = variant.ident,
252-
));
231+
match (&variant.fields, content_name.as_ref()) {
232+
// adjacently tagged
233+
(syn::Fields::Unnamed(fields), Some(content_name)) => {
234+
state.types.push('\n');
235+
let comments = utils::get_comments(variant.attrs);
236+
state.write_comments(&comments, 0);
237+
state.types.push_str(&format!(
238+
"type {interface_name}__{variant_name} = ",
239+
interface_name = exported_struct.ident,
240+
variant_name = variant.ident,
241+
));
242+
// add discriminant
243+
state.types.push_str(&format!(
244+
"{{\n{indent}\"{tag_name}\": \"{}\";\n{indent}\"{content_name}\": ",
245+
variant.ident,
246+
indent = utils::build_indentation(2),
247+
));
248+
super::structs::process_tuple_fields(fields.clone(), state);
249+
state.types.push_str(";\n};");
250+
}
251+
// missing content name
252+
(syn::Fields::Unnamed(_), None) => {
253+
if crate::DEBUG.try_get().is_some_and(|d: &bool| *d) {
254+
println!(
255+
"#[tsync] failed for {} variant of enum {}, missing content attribute, skipping",
256+
variant.ident,
257+
exported_struct.ident
258+
);
259+
}
260+
continue;
261+
}
262+
_ => {
263+
state.types.push('\n');
264+
let comments = utils::get_comments(variant.attrs);
265+
state.write_comments(&comments, 0);
266+
state.types.push_str(&format!(
267+
"type {interface_name}__{variant_name} = ",
268+
interface_name = exported_struct.ident,
269+
variant_name = variant.ident,
270+
));
253271

254-
let field_name = if let Some(casing) = casing {
255-
variant.ident.to_string().to_case(casing)
256-
} else {
257-
variant.ident.to_string()
258-
};
259-
// add discriminant
260-
state.types.push_str(&format!(
261-
"{{\n{}{}: \"{}\";\n",
262-
utils::build_indentation(2),
263-
tag_name,
264-
field_name,
265-
));
266-
super::structs::process_fields(variant.fields, state, 2, casing);
267-
state.types.push_str("};");
272+
let field_name = if let Some(casing) = casing {
273+
variant.ident.to_string().to_case(casing)
274+
} else {
275+
variant.ident.to_string()
276+
};
277+
// add discriminant
278+
state.types.push_str(&format!(
279+
"{{\n{}{}: \"{}\";\n",
280+
utils::build_indentation(2),
281+
tag_name,
282+
field_name,
283+
));
284+
super::structs::process_fields(variant.fields, state, 2, casing);
285+
state.types.push_str("};");
286+
}
268287
}
269288
}
270289
state.types.push('\n');
@@ -293,17 +312,13 @@ fn add_externally_tagged_enum(
293312
} else {
294313
variant.ident.to_string()
295314
};
296-
// Assumes that non-newtype tuple variants have already been filtered out
297-
let is_newtype = variant.fields.iter().any(|v| v.ident.is_none());
298315

299-
if is_newtype {
316+
if let syn::Fields::Unnamed(fields) = &variant.fields {
300317
// add discriminant
301-
state.types.push_str(&format!(" | {{ \"{}\":", field_name));
302-
for field in variant.fields {
303-
state
304-
.types
305-
.push_str(&format!(" {}", convert_type(&field.ty).ts_type,));
306-
}
318+
state
319+
.types
320+
.push_str(&format!(" | {{ \"{}\": ", field_name));
321+
super::structs::process_tuple_fields(fields.clone(), state);
307322
state.types.push_str(" }");
308323
} else {
309324
// add discriminant

Diff for: src/to_typescript/structs.rs

+5-8
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,19 @@ pub fn process_fields(
127127
/// ```ignore
128128
/// type Todo = [string, number];
129129
/// ```
130-
pub(crate) fn process_tuple_fields(fields: syn::FieldsUnnamed, state: &mut BuildState) {
130+
pub fn process_tuple_fields(fields: syn::FieldsUnnamed, state: &mut BuildState) {
131131
let out = fields
132132
.unnamed
133133
.into_iter()
134134
.map(|field| {
135135
let field_type = convert_type(&field.ty);
136-
format!("{field_type}", field_type = field_type.ts_type)
136+
field_type.ts_type
137137
})
138138
.collect::<Vec<String>>();
139139

140-
if out.is_empty() {
141-
return;
142-
} else if out.len() == 1 {
143-
state.types.push_str(&format!("{}", out[0]));
144-
return;
145-
} else {
140+
if out.len() == 1 {
141+
state.types.push_str(&out[0].to_string());
142+
} else if !out.is_empty() {
146143
state.types.push_str(&format!("[ {} ]", out.join(", ")));
147144
}
148145
}

Diff for: test/enum/rust.rs

+30-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,32 @@ enum InternalTopping {
1717
ExtraCheese { kind: String },
1818
/// Custom toppings
1919
/// May expire soon
20+
/// Note: this test case will not be included in the generated typescript,
21+
/// because it is a tuple variant
2022
Custom(CustomTopping),
23+
/// two custom toppings
24+
/// Note: this test case will not be included in the generated typescript,
25+
/// because it is a tuple variant
26+
CustomTwo(CustomTopping, CustomTopping),
27+
}
28+
29+
/// Adjacently tagged enums have a key-value pair
30+
/// that discrimate which variant it belongs to, and
31+
/// can support tuple variants
32+
#[tsync]
33+
#[serde(tag = "type", content = "value")]
34+
enum AdjacentTopping {
35+
/// Tasty!
36+
/// Not vegetarian
37+
Pepperoni,
38+
/// For cheese lovers
39+
ExtraCheese { kind: String },
40+
/// Custom toppings
41+
/// May expire soon
42+
Custom(CustomTopping),
43+
/// two custom toppings
44+
/// Note: this test case is specifically for specifying a tuple of types
45+
CustomTwo(CustomTopping, CustomTopping),
2146
}
2247

2348
/// Externally tagged enums ascribe the value to a key
@@ -33,6 +58,9 @@ enum ExternalTopping {
3358
/// May expire soon
3459
/// Note: this test case is specifically for specifying a single type in the tuple
3560
Custom(CustomTopping),
61+
/// two custom toppings
62+
/// Note: this test case is specifically for specifying a tuple of types
63+
CustomTwo(CustomTopping, CustomTopping),
3664
}
3765

3866
#[tsync]
@@ -69,5 +97,5 @@ enum AnimalTwo {
6997
#[tsync]
7098
#[serde(tag = "type")]
7199
enum Tagged {
72-
Test // this should be { type: "Test" } in the TypeScript (not just the string "Test")
73-
}
100+
Test, // this should be { type: "Test" } in the TypeScript (not just the string "Test")
101+
}

Diff for: test/enum/typescript.d.ts

+46-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,46 @@ type InternalTopping__ExtraCheese = {
2121
KIND: string;
2222
};
2323

24+
/**
25+
* Adjacently tagged enums have a key-value pair
26+
* that discrimate which variant it belongs to, and
27+
* can support tuple variants
28+
*/
29+
type AdjacentTopping =
30+
| AdjacentTopping__Pepperoni
31+
| AdjacentTopping__ExtraCheese
32+
| AdjacentTopping__Custom
33+
| AdjacentTopping__CustomTwo;
34+
35+
/**
36+
* Tasty!
37+
* Not vegetarian
38+
*/
39+
type AdjacentTopping__Pepperoni = {
40+
type: "Pepperoni";
41+
};
42+
/** For cheese lovers */
43+
type AdjacentTopping__ExtraCheese = {
44+
type: "ExtraCheese";
45+
kind: string;
46+
};
47+
/**
48+
* Custom toppings
49+
* May expire soon
50+
*/
51+
type AdjacentTopping__Custom = {
52+
"type": "Custom";
53+
"value": CustomTopping;
54+
};
55+
/**
56+
* two custom toppings
57+
* Note: this test case is specifically for specifying a tuple of types
58+
*/
59+
type AdjacentTopping__CustomTwo = {
60+
"type": "CustomTwo";
61+
"value": [ CustomTopping, CustomTopping ];
62+
};
63+
2464
/**
2565
* Externally tagged enums ascribe the value to a key
2666
* that is the same as the variant name
@@ -44,7 +84,12 @@ type ExternalTopping =
4484
* May expire soon
4585
* Note: this test case is specifically for specifying a single type in the tuple
4686
*/
47-
| { "Custom": CustomTopping };
87+
| { "Custom": CustomTopping }
88+
/**
89+
* two custom toppings
90+
* Note: this test case is specifically for specifying a tuple of types
91+
*/
92+
| { "CustomTwo": [ CustomTopping, CustomTopping ] };
4893

4994
interface CustomTopping {
5095
name: string;

Diff for: test/enum/typescript.ts

+46-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,46 @@ type InternalTopping__ExtraCheese = {
2121
KIND: string;
2222
};
2323

24+
/**
25+
* Adjacently tagged enums have a key-value pair
26+
* that discrimate which variant it belongs to, and
27+
* can support tuple variants
28+
*/
29+
export type AdjacentTopping =
30+
| AdjacentTopping__Pepperoni
31+
| AdjacentTopping__ExtraCheese
32+
| AdjacentTopping__Custom
33+
| AdjacentTopping__CustomTwo;
34+
35+
/**
36+
* Tasty!
37+
* Not vegetarian
38+
*/
39+
type AdjacentTopping__Pepperoni = {
40+
type: "Pepperoni";
41+
};
42+
/** For cheese lovers */
43+
type AdjacentTopping__ExtraCheese = {
44+
type: "ExtraCheese";
45+
kind: string;
46+
};
47+
/**
48+
* Custom toppings
49+
* May expire soon
50+
*/
51+
type AdjacentTopping__Custom = {
52+
"type": "Custom";
53+
"value": CustomTopping;
54+
};
55+
/**
56+
* two custom toppings
57+
* Note: this test case is specifically for specifying a tuple of types
58+
*/
59+
type AdjacentTopping__CustomTwo = {
60+
"type": "CustomTwo";
61+
"value": [ CustomTopping, CustomTopping ];
62+
};
63+
2464
/**
2565
* Externally tagged enums ascribe the value to a key
2666
* that is the same as the variant name
@@ -44,7 +84,12 @@ export type ExternalTopping =
4484
* May expire soon
4585
* Note: this test case is specifically for specifying a single type in the tuple
4686
*/
47-
| { "Custom": CustomTopping };
87+
| { "Custom": CustomTopping }
88+
/**
89+
* two custom toppings
90+
* Note: this test case is specifically for specifying a tuple of types
91+
*/
92+
| { "CustomTwo": [ CustomTopping, CustomTopping ] };
4893

4994
export interface CustomTopping {
5095
name: string;

0 commit comments

Comments
 (0)