Skip to content

Commit 8ca7a1a

Browse files
Feat: support for tuple types, structs, and enum variants, as well as generics in tuples (#59)
* feat: support for tuple structs closes issue #46 * feat: support for tuple types closes [feature request] Tuples #43 * feat: support for tuple variants of enums closes Failing for Internally/Adjacently tagged #58 partially addresses Enhanced enum support and types #55 * feat: better support for generics in enums * feat: supprt internally tagged newtype variants * feat: add support for empty object type in struct and enum processing * fix(ci): fix cache step also, use Swatinem/rust-cache instead of actions/cache
1 parent a106fe0 commit 8ca7a1a

30 files changed

+958
-136
lines changed

.github/workflows/CI.yml

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,38 @@ jobs:
2727
name: build
2828
runs-on: ubuntu-latest
2929
steps:
30-
- uses: actions/checkout@v3.3.0
30+
- uses: actions/checkout@v4
3131
- uses: rui314/setup-mold@v1
32-
- uses: actions-rs/toolchain@v1.0.7
32+
- name: Install Rust toolchain
33+
run: |
34+
rustup show
35+
rustup -V
36+
rustup set profile minimal
37+
rustup toolchain install stable
38+
rustup override set stable
39+
- name: Setup cache
40+
uses: Swatinem/rust-cache@v2
3341
with:
34-
profile: minimal
35-
toolchain: stable
36-
override: true
37-
- uses: actions/cache@v3.2.4
38-
with:
39-
path: |
40-
./.cargo/.build
41-
./target
42-
~/.cargo
43-
key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }}
42+
shared-key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }}
4443
- run: cargo check --all-targets --all-features
4544

4645
test:
4746
name: test
4847
needs: [build]
4948
runs-on: ubuntu-latest
5049
steps:
51-
- uses: actions/checkout@v3.3.0
50+
- uses: actions/checkout@v4
5251
- uses: rui314/setup-mold@v1
53-
- uses: actions-rs/toolchain@v1.0.7
54-
with:
55-
profile: minimal
56-
toolchain: stable
57-
override: true
58-
- uses: actions/cache@v3.2.4
52+
- name: Install Rust toolchain
53+
run: |
54+
rustup show
55+
rustup -V
56+
rustup set profile minimal
57+
rustup toolchain install stable
58+
rustup override set stable
59+
- name: Setup cache
60+
uses: Swatinem/rust-cache@v2
5961
with:
60-
path: |
61-
./.cargo/.build
62-
./target
63-
~/.cargo
64-
key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }}
62+
shared-key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }}
6563
- run: bash test/test_all.sh
6664
- run: git diff --exit-code --quiet || exit 1

src/to_typescript/enums.rs

Lines changed: 148 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::typescript::convert_type;
2-
use crate::{utils, BuildState};
1+
use crate::{typescript::convert_type, utils, BuildState};
32
use convert_case::{Case, Casing};
43
use syn::__private::ToTokens;
54

@@ -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,63 +195,151 @@ 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>,
214202
uses_type_interface: bool,
215203
) {
216204
let export = if uses_type_interface { "" } else { "export " };
205+
let generics = utils::extract_struct_generics(exported_struct.generics.clone());
217206
state.types.push_str(&format!(
218207
"{export}type {interface_name}{generics} =",
219208
interface_name = exported_struct.ident,
220-
generics = utils::extract_struct_generics(exported_struct.generics.clone())
209+
generics = utils::format_generics(&generics)
221210
));
222211

212+
// a list of the generics for each variant, so we don't need to recalculate them
213+
let mut variant_generics_list = Vec::new();
214+
223215
for variant in exported_struct.variants.iter() {
216+
let variant_field_types = variant.fields.iter().map(|f| f.ty.to_owned());
217+
let variant_generics = generics
218+
.iter()
219+
.filter(|gen| {
220+
variant_field_types
221+
.clone()
222+
.any(|ty| utils::type_contains_ident(&ty, gen))
223+
})
224+
.cloned()
225+
.collect::<Vec<_>>();
226+
224227
// Assumes that non-newtype tuple variants have already been filtered out
225-
if variant.fields.iter().any(|v| v.ident.is_none()) {
226-
// TODO: Generate newtype structure
227-
// This should contain the discriminant plus all fields of the inner structure as a flat structure
228-
// TODO: Check for case where discriminant name matches an inner structure field name
229-
// We should reject clashes
230-
} else {
231-
state.types.push('\n');
232-
state.types.push_str(&format!(
233-
" | {interface_name}__{variant_name}",
234-
interface_name = exported_struct.ident,
235-
variant_name = variant.ident,
236-
))
228+
// TODO: Check for case where discriminant name matches an inner structure field name
229+
// We should reject clashes
230+
match &variant.fields {
231+
syn::Fields::Unnamed(fields) if fields.unnamed.len() > 1 && content_name.is_none() => {
232+
continue;
233+
}
234+
_ => {
235+
variant_generics_list.push(variant_generics.clone());
236+
state.types.push('\n');
237+
state.types.push_str(&format!(
238+
" | {interface_name}__{variant_name}{generics}",
239+
interface_name = exported_struct.ident,
240+
variant_name = variant.ident,
241+
generics = utils::format_generics(&variant_generics)
242+
))
243+
}
237244
}
238245
}
239246

240247
state.types.push_str(";\n");
241248

242-
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-
));
249+
for (variant, generics) in exported_struct
250+
.variants
251+
.into_iter()
252+
.zip(variant_generics_list)
253+
{
254+
let generics = utils::format_generics(&generics);
253255

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("};");
256+
match (&variant.fields, content_name.as_ref()) {
257+
// adjacently tagged
258+
(syn::Fields::Unnamed(fields), Some(content_name)) => {
259+
state.types.push('\n');
260+
let comments = utils::get_comments(variant.attrs);
261+
state.write_comments(&comments, 0);
262+
state.types.push_str(&format!(
263+
"type {interface_name}__{variant_name}{generics} = ",
264+
interface_name = exported_struct.ident,
265+
variant_name = variant.ident,
266+
));
267+
// add discriminant
268+
state.types.push_str(&format!(
269+
"{{\n{indent}\"{tag_name}\": \"{}\";\n{indent}\"{content_name}\": ",
270+
variant.ident,
271+
indent = utils::build_indentation(2),
272+
));
273+
super::structs::process_tuple_fields(fields.clone(), state);
274+
state.types.push_str(";\n};");
275+
}
276+
// missing content name, but is a newtype variant
277+
(syn::Fields::Unnamed(fields), None) if fields.unnamed.len() <= 1 => {
278+
state.types.push('\n');
279+
let comments = utils::get_comments(variant.attrs);
280+
state.write_comments(&comments, 0);
281+
state.types.push_str(&format!(
282+
"type {interface_name}__{variant_name}{generics} = ",
283+
interface_name = exported_struct.ident,
284+
variant_name = variant.ident,
285+
));
286+
287+
let field_name = if let Some(casing) = casing {
288+
variant.ident.to_string().to_case(casing)
289+
} else {
290+
variant.ident.to_string()
291+
};
292+
// add discriminant
293+
state.types.push_str(&format!(
294+
"{{\n{}{}: \"{}\"}}",
295+
utils::build_indentation(2),
296+
tag_name,
297+
field_name,
298+
));
299+
300+
// add the newtype field
301+
let newtype = convert_type(&fields.unnamed.first().unwrap().ty);
302+
state.types.push_str(&format!(
303+
" & {content_name}",
304+
content_name = newtype.ts_type
305+
));
306+
}
307+
// missing content name, and is not a newtype, this is an error case
308+
(syn::Fields::Unnamed(_), None) => {
309+
if crate::DEBUG.try_get().is_some_and(|d: &bool| *d) {
310+
println!(
311+
"#[tsync] failed for {} variant of enum {}, missing content attribute, skipping",
312+
variant.ident,
313+
exported_struct.ident
314+
);
315+
}
316+
continue;
317+
}
318+
_ => {
319+
state.types.push('\n');
320+
let comments = utils::get_comments(variant.attrs);
321+
state.write_comments(&comments, 0);
322+
state.types.push_str(&format!(
323+
"type {interface_name}__{variant_name}{generics} = ",
324+
interface_name = exported_struct.ident,
325+
variant_name = variant.ident,
326+
));
327+
328+
let field_name = if let Some(casing) = casing {
329+
variant.ident.to_string().to_case(casing)
330+
} else {
331+
variant.ident.to_string()
332+
};
333+
// add discriminant
334+
state.types.push_str(&format!(
335+
"{{\n{}{}: \"{}\";\n",
336+
utils::build_indentation(2),
337+
tag_name,
338+
field_name,
339+
));
340+
super::structs::process_fields(variant.fields, state, 2, casing, false);
341+
state.types.push_str("};");
342+
}
268343
}
269344
}
270345
state.types.push('\n');
@@ -278,10 +353,11 @@ fn add_externally_tagged_enum(
278353
uses_type_interface: bool,
279354
) {
280355
let export = if uses_type_interface { "" } else { "export " };
356+
let generics = utils::extract_struct_generics(exported_struct.generics.clone());
281357
state.types.push_str(&format!(
282358
"{export}type {interface_name}{generics} =",
283359
interface_name = exported_struct.ident,
284-
generics = utils::extract_struct_generics(exported_struct.generics.clone())
360+
generics = utils::format_generics(&generics)
285361
));
286362

287363
for variant in exported_struct.variants {
@@ -293,17 +369,13 @@ fn add_externally_tagged_enum(
293369
} else {
294370
variant.ident.to_string()
295371
};
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());
298372

299-
if is_newtype {
373+
if let syn::Fields::Unnamed(fields) = &variant.fields {
300374
// 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-
}
375+
state
376+
.types
377+
.push_str(&format!(" | {{ \"{}\": ", field_name));
378+
super::structs::process_tuple_fields(fields.clone(), state);
307379
state.types.push_str(" }");
308380
} else {
309381
// add discriminant
@@ -313,13 +385,11 @@ fn add_externally_tagged_enum(
313385
field_name,
314386
));
315387
let prepend;
316-
if variant.fields.is_empty() {
317-
prepend = "".into();
318-
} else {
319-
prepend = utils::build_indentation(6);
320-
state.types.push('\n');
321-
super::structs::process_fields(variant.fields, state, 8, casing);
322-
}
388+
389+
prepend = utils::build_indentation(6);
390+
state.types.push('\n');
391+
super::structs::process_fields(variant.fields, state, 8, casing, true);
392+
323393
state
324394
.types
325395
.push_str(&format!("{}}}\n{}}}", prepend, utils::build_indentation(4)));

0 commit comments

Comments
 (0)