From 0b6b6f62d23a6835270304a17bc05c59d7463280 Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:29:39 -0800 Subject: [PATCH 1/7] feat: support for tuple structs closes issue #46 --- src/to_typescript/structs.rs | 86 +++++++++++++++++++++++++++++++---- test/issue-43/rust.rs | 17 +++++++ test/issue-43/tsync.sh | 8 ++++ test/issue-43/typescript.d.ts | 5 ++ test/issue-43/typescript.ts | 5 ++ 5 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 test/issue-43/rust.rs create mode 100755 test/issue-43/tsync.sh create mode 100644 test/issue-43/typescript.d.ts create mode 100644 test/issue-43/typescript.ts diff --git a/src/to_typescript/structs.rs b/src/to_typescript/structs.rs index 081bd65..8f59337 100644 --- a/src/to_typescript/structs.rs +++ b/src/to_typescript/structs.rs @@ -4,7 +4,11 @@ use convert_case::{Case, Casing}; impl super::ToTypescript for syn::ItemStruct { fn convert_to_ts(self, state: &mut BuildState, config: &crate::BuildSettings) { - let export = if config.uses_type_interface { "" } else { "export " }; + let export = if config.uses_type_interface { + "" + } else { + "export " + }; let casing = utils::get_attribute_arg("serde", "rename_all", &self.attrs); let casing = utils::parse_serde_case(casing); state.types.push('\n'); @@ -14,27 +18,53 @@ impl super::ToTypescript for syn::ItemStruct { let intersections = get_intersections(&self.fields); - match intersections { - Some(intersections) => { + match ( + intersections, + matches!(self.fields, syn::Fields::Unnamed(_)), + ) { + (Some(intersections), false) => { state.types.push_str(&format!( - "{export}type {struct_name}{generics} = {intersections} & {{\n", + "{export}type {struct_name}{generics} = {intersections} & ", export = export, struct_name = self.ident, generics = utils::extract_struct_generics(self.generics.clone()), intersections = intersections )); } - None => { + (None, false) => { state.types.push_str(&format!( - "{export}interface {interface_name}{generics} {{\n", + "{export}interface {interface_name}{generics} ", interface_name = self.ident, generics = utils::extract_struct_generics(self.generics.clone()) )); } + (None, true) => { + state.types.push_str(&format!( + "{export}type {struct_name}{generics} = ", + export = export, + struct_name = self.ident, + generics = utils::extract_struct_generics(self.generics.clone()), + )); + } + (Some(_), true) => { + if crate::DEBUG.try_get().is_some_and(|d| *d) { + println!( + "#[tsync] failed for struct {}. cannot flatten fields of tuple struct", + self.ident + ); + } + return; + } + } + + if let syn::Fields::Unnamed(unnamed) = self.fields { + process_tuple_fields(unnamed, state); + } else { + state.types.push_str("{\n"); + process_fields(self.fields, state, 2, casing); + state.types.push('}'); } - process_fields(self.fields, state, 2, casing); - state.types.push('}'); state.types.push('\n'); } } @@ -48,6 +78,11 @@ pub fn process_fields( let space = utils::build_indentation(indentation_amount); let case = case.into(); for field in fields { + debug_assert!( + field.ident.is_some(), + "struct fields should have names, found unnamed field" + ); + // Check if the field has the serde flatten attribute, if so, skip it let has_flatten_attr = utils::get_attribute_arg("serde", "flatten", &field.attrs).is_some(); if has_flatten_attr { @@ -77,6 +112,41 @@ pub fn process_fields( } } +/// Process tuple fields +/// +/// NOTE: Currently, this function does not handle comments or attributes on tuple fields. +/// +/// # Example +/// +/// ```ignore +/// struct Todo(String, u32); +/// ``` +/// +/// should become +/// +/// ```ignore +/// type Todo = [string, number]; +/// ``` +pub(crate) fn process_tuple_fields(fields: syn::FieldsUnnamed, state: &mut BuildState) { + let out = fields + .unnamed + .into_iter() + .map(|field| { + let field_type = convert_type(&field.ty); + format!("{field_type}", field_type = field_type.ts_type) + }) + .collect::>(); + + if out.is_empty() { + return; + } else if out.len() == 1 { + state.types.push_str(&format!("{}", out[0])); + return; + } else { + state.types.push_str(&format!("[ {} ]", out.join(", "))); + } +} + fn get_intersections(fields: &syn::Fields) -> Option { let mut types = Vec::new(); diff --git a/test/issue-43/rust.rs b/test/issue-43/rust.rs new file mode 100644 index 0000000..b9bf553 --- /dev/null +++ b/test/issue-43/rust.rs @@ -0,0 +1,17 @@ +// #[tsync] +// struct HasTuple1 { +// foo: i32, +// bar: Option<(String, i32)>, +// } + +// #[tsync] +// struct HasTuple2 { +// foo: i32, +// bar: (String, i32), +// } + +#[tsync] +struct IsTuple(i32, String); + +#[tsync] +struct IsTupleComplex(i32, String, (String, (i32, i32))); diff --git a/test/issue-43/tsync.sh b/test/issue-43/tsync.sh new file mode 100755 index 0000000..1764bd6 --- /dev/null +++ b/test/issue-43/tsync.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +cd $SCRIPT_DIR + +cargo run -- -i rust.rs -o typescript.d.ts +cargo run -- -i rust.rs -o typescript.ts \ No newline at end of file diff --git a/test/issue-43/typescript.d.ts b/test/issue-43/typescript.d.ts new file mode 100644 index 0000000..65034ab --- /dev/null +++ b/test/issue-43/typescript.d.ts @@ -0,0 +1,5 @@ +/* This file is generated and managed by tsync */ + +type IsTuple = [ number, string ] + +type IsTupleComplex = [ number, string, unknown ] diff --git a/test/issue-43/typescript.ts b/test/issue-43/typescript.ts new file mode 100644 index 0000000..ef38ac5 --- /dev/null +++ b/test/issue-43/typescript.ts @@ -0,0 +1,5 @@ +/* This file is generated and managed by tsync */ + +export type IsTuple = [ number, string ] + +export type IsTupleComplex = [ number, string, unknown ] From 97ca4cc0e4174e09b68313f39c5efbc7192a3209 Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:37:16 -0800 Subject: [PATCH 2/7] feat: support for tuple types closes [feature request] Tuples #43 --- src/typescript.rs | 30 +++++++++++++++++++++++++----- test/issue-43/rust.rs | 21 ++++++++++----------- test/issue-43/typescript.d.ts | 12 +++++++++++- test/issue-43/typescript.ts | 12 +++++++++++- test/test_all.sh | 3 ++- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/typescript.rs b/src/typescript.rs index 67ba412..c4ebf38 100644 --- a/src/typescript.rs +++ b/src/typescript.rs @@ -5,8 +5,8 @@ pub struct TsType { } impl From for TsType { - fn from(ts_type: String) -> TsType { - TsType { + fn from(ts_type: String) -> Self { + Self { ts_type, is_optional: false, } @@ -169,13 +169,33 @@ pub fn convert_type(ty: &syn::Type) -> TsType { if let Ok(ts_type) = try_match_ident_str(&identifier) { ts_type.into() } else if let Ok(ts_type) = try_match_with_args(&identifier, &segment.arguments) { - ts_type.into() - } else if let Ok(ts_type) = extract_custom_type(&segment) { - ts_type.into() + ts_type + } else if let Ok(ts_type) = extract_custom_type(segment) { + ts_type } else { "unknown".to_owned().into() } } + syn::Type::Tuple(t) => { + let types = t + .elems + .iter() + .map(convert_type) + .map(|ty| { + if ty.is_optional { + format!("{} | undefined", ty.ts_type) + } else { + ty.ts_type + } + }) + .collect::>() + .join(", "); + + TsType { + ts_type: format!("[{types}]"), + is_optional: false, + } + } _ => "unknown".to_owned().into(), } } diff --git a/test/issue-43/rust.rs b/test/issue-43/rust.rs index b9bf553..43767b4 100644 --- a/test/issue-43/rust.rs +++ b/test/issue-43/rust.rs @@ -1,14 +1,13 @@ -// #[tsync] -// struct HasTuple1 { -// foo: i32, -// bar: Option<(String, i32)>, -// } - -// #[tsync] -// struct HasTuple2 { -// foo: i32, -// bar: (String, i32), -// } +#[tsync] +struct HasTuple { + foo: i32, + bar: Option<(String, i32)>, + baz: (String, i32), + zip: (i32, String, (String, (i32, i32))), + qux: (Option, (i32, String)), + ping: (i32, String, Option<(String, (i32, i32))>), + pong: Option<(i32, String, Option<(String, Option<(i32, i32)>)>)>, +} #[tsync] struct IsTuple(i32, String); diff --git a/test/issue-43/typescript.d.ts b/test/issue-43/typescript.d.ts index 65034ab..a78d813 100644 --- a/test/issue-43/typescript.d.ts +++ b/test/issue-43/typescript.d.ts @@ -1,5 +1,15 @@ /* This file is generated and managed by tsync */ +interface HasTuple { + foo: number; + bar?: [string, number]; + baz: [string, number]; + zip: [number, string, [string, [number, number]]]; + qux: [string | undefined, [number, string]]; + ping: [number, string, [string, [number, number]] | undefined]; + pong?: [number, string, [string, [number, number] | undefined] | undefined]; +} + type IsTuple = [ number, string ] -type IsTupleComplex = [ number, string, unknown ] +type IsTupleComplex = [ number, string, [string, [number, number]] ] diff --git a/test/issue-43/typescript.ts b/test/issue-43/typescript.ts index ef38ac5..1e87c3a 100644 --- a/test/issue-43/typescript.ts +++ b/test/issue-43/typescript.ts @@ -1,5 +1,15 @@ /* This file is generated and managed by tsync */ +export interface HasTuple { + foo: number; + bar?: [string, number]; + baz: [string, number]; + zip: [number, string, [string, [number, number]]]; + qux: [string | undefined, [number, string]]; + ping: [number, string, [string, [number, number]] | undefined]; + pong?: [number, string, [string, [number, number] | undefined] | undefined]; +} + export type IsTuple = [ number, string ] -export type IsTupleComplex = [ number, string, unknown ] +export type IsTupleComplex = [ number, string, [string, [number, number]] ] diff --git a/test/test_all.sh b/test/test_all.sh index ffa633b..c362675 100755 --- a/test/test_all.sh +++ b/test/test_all.sh @@ -8,8 +8,9 @@ cd $SCRIPT_DIR ./struct/tsync.sh ./type/tsync.sh ./const/tsync.sh -./const_enum/tsync.sh +./const_enum_numeric/tsync.sh ./enum/tsync.sh ./enum_numeric/tsync.sh ./doc_comments/tsync.sh ./generic/tsync.sh +./issue-43/tsync.sh From 01f56bef0867122b139a8d5f0003ae5e8aa9bf8f Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:55:12 -0800 Subject: [PATCH 3/7] feat: support for tuple variants of enums closes Failing for Internally/Adjacently tagged #58 partially addresses Enhanced enum support and types #55 --- src/to_typescript/enums.rs | 127 +++++++++++++++++++--------------- src/to_typescript/structs.rs | 13 ++-- test/enum/rust.rs | 32 ++++++++- test/enum/typescript.d.ts | 47 ++++++++++++- test/enum/typescript.ts | 47 ++++++++++++- test/issue-55/rust.rs | 22 ++++++ test/issue-55/tsync.sh | 8 +++ test/issue-55/typescript.d.ts | 31 +++++++++ test/issue-55/typescript.ts | 31 +++++++++ test/issue-58/rust.rs | 21 ++++++ test/issue-58/tsync.sh | 8 +++ test/issue-58/typescript.d.ts | 17 +++++ test/issue-58/typescript.ts | 17 +++++ test/test_all.sh | 2 + 14 files changed, 355 insertions(+), 68 deletions(-) create mode 100644 test/issue-55/rust.rs create mode 100755 test/issue-55/tsync.sh create mode 100644 test/issue-55/typescript.d.ts create mode 100644 test/issue-55/typescript.ts create mode 100644 test/issue-58/rust.rs create mode 100755 test/issue-58/tsync.sh create mode 100644 test/issue-58/typescript.d.ts create mode 100644 test/issue-58/typescript.ts diff --git a/src/to_typescript/enums.rs b/src/to_typescript/enums.rs index 85b7953..897c4ce 100644 --- a/src/to_typescript/enums.rs +++ b/src/to_typescript/enums.rs @@ -1,4 +1,3 @@ -use crate::typescript::convert_type; use crate::{utils, BuildState}; use convert_case::{Case, Casing}; use syn::__private::ToTokens; @@ -9,26 +8,6 @@ use syn::__private::ToTokens; /// `rename_all` attributes for the name of the tag will also be adhered to. impl super::ToTypescript for syn::ItemEnum { fn convert_to_ts(self, state: &mut BuildState, config: &crate::BuildSettings) { - // check we don't have any tuple structs that could mess things up. - // if we do ignore this struct - for variant in self.variants.iter() { - // allow single-field tuple structs to pass through as newtype structs - let mut is_newtype = false; - for f in variant.fields.iter() { - if f.ident.is_none() { - // If we already marked this variant as a newtype, we have a multi-field tuple struct - if is_newtype { - if crate::DEBUG.try_get().is_some_and(|d| *d) { - println!("#[tsync] failed for enum {}", self.ident); - } - return; - } else { - is_newtype = true; - } - } - } - } - state.types.push('\n'); let comments = utils::get_comments(self.clone().attrs); @@ -42,7 +21,15 @@ impl super::ToTypescript for syn::ItemEnum { // always use output the internally_tagged representation if the tag is present if let Some(tag_name) = utils::get_attribute_arg("serde", "tag", &self.attrs) { - add_internally_tagged_enum(tag_name, self, state, casing, config.uses_type_interface) + let content_name = utils::get_attribute_arg("serde", "content", &self.attrs); + add_internally_tagged_enum( + tag_name, + content_name, + self, + state, + casing, + config.uses_type_interface, + ) } else if is_single { if utils::has_attribute_arg("derive", "Serialize_repr", &self.attrs) { add_numeric_enum(self, state, casing, config) @@ -208,6 +195,7 @@ fn add_numeric_enum( /// ``` fn add_internally_tagged_enum( tag_name: String, + content_name: Option, exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, @@ -222,7 +210,7 @@ fn add_internally_tagged_enum( for variant in exported_struct.variants.iter() { // Assumes that non-newtype tuple variants have already been filtered out - if variant.fields.iter().any(|v| v.ident.is_none()) { + if variant.fields.iter().any(|v| v.ident.is_none()) && content_name.is_none() { // TODO: Generate newtype structure // This should contain the discriminant plus all fields of the inner structure as a flat structure // TODO: Check for case where discriminant name matches an inner structure field name @@ -240,31 +228,62 @@ fn add_internally_tagged_enum( state.types.push_str(";\n"); for variant in exported_struct.variants { - // Assumes that non-newtype tuple variants have already been filtered out - if !variant.fields.iter().any(|v| v.ident.is_none()) { - state.types.push('\n'); - let comments = utils::get_comments(variant.attrs); - state.write_comments(&comments, 0); - state.types.push_str(&format!( - "type {interface_name}__{variant_name} = ", - interface_name = exported_struct.ident, - variant_name = variant.ident, - )); + match (&variant.fields, content_name.as_ref()) { + // adjacently tagged + (syn::Fields::Unnamed(fields), Some(content_name)) => { + state.types.push('\n'); + let comments = utils::get_comments(variant.attrs); + state.write_comments(&comments, 0); + state.types.push_str(&format!( + "type {interface_name}__{variant_name} = ", + interface_name = exported_struct.ident, + variant_name = variant.ident, + )); + // add discriminant + state.types.push_str(&format!( + "{{\n{indent}\"{tag_name}\": \"{}\";\n{indent}\"{content_name}\": ", + variant.ident, + indent = utils::build_indentation(2), + )); + super::structs::process_tuple_fields(fields.clone(), state); + state.types.push_str(";\n};"); + } + // missing content name + (syn::Fields::Unnamed(_), None) => { + if crate::DEBUG.try_get().is_some_and(|d: &bool| *d) { + println!( + "#[tsync] failed for {} variant of enum {}, missing content attribute, skipping", + variant.ident, + exported_struct.ident + ); + } + continue; + } + _ => { + state.types.push('\n'); + let comments = utils::get_comments(variant.attrs); + state.write_comments(&comments, 0); + state.types.push_str(&format!( + "type {interface_name}__{variant_name} = ", + interface_name = exported_struct.ident, + variant_name = variant.ident, + )); - let field_name = if let Some(casing) = casing { - variant.ident.to_string().to_case(casing) - } else { - variant.ident.to_string() - }; - // add discriminant - state.types.push_str(&format!( - "{{\n{}{}: \"{}\";\n", - utils::build_indentation(2), - tag_name, - field_name, - )); - super::structs::process_fields(variant.fields, state, 2, casing); - state.types.push_str("};"); + let field_name = if let Some(casing) = casing { + variant.ident.to_string().to_case(casing) + } else { + variant.ident.to_string() + }; + // add discriminant + state.types.push_str(&format!( + "{{\n{}{}: \"{}\";\n", + utils::build_indentation(2), + tag_name, + field_name, + )); + super::structs::process_fields(variant.fields, state, 2, casing); + state.types.push_str("};"); + } } } state.types.push('\n'); @@ -293,17 +312,13 @@ fn add_externally_tagged_enum( } else { variant.ident.to_string() }; - // Assumes that non-newtype tuple variants have already been filtered out - let is_newtype = variant.fields.iter().any(|v| v.ident.is_none()); - if is_newtype { + if let syn::Fields::Unnamed(fields) = &variant.fields { // add discriminant - state.types.push_str(&format!(" | {{ \"{}\":", field_name)); - for field in variant.fields { - state - .types - .push_str(&format!(" {}", convert_type(&field.ty).ts_type,)); - } + state + .types + .push_str(&format!(" | {{ \"{}\": ", field_name)); + super::structs::process_tuple_fields(fields.clone(), state); state.types.push_str(" }"); } else { // add discriminant diff --git a/src/to_typescript/structs.rs b/src/to_typescript/structs.rs index 8f59337..7e6bd16 100644 --- a/src/to_typescript/structs.rs +++ b/src/to_typescript/structs.rs @@ -127,22 +127,19 @@ pub fn process_fields( /// ```ignore /// type Todo = [string, number]; /// ``` -pub(crate) fn process_tuple_fields(fields: syn::FieldsUnnamed, state: &mut BuildState) { +pub fn process_tuple_fields(fields: syn::FieldsUnnamed, state: &mut BuildState) { let out = fields .unnamed .into_iter() .map(|field| { let field_type = convert_type(&field.ty); - format!("{field_type}", field_type = field_type.ts_type) + field_type.ts_type }) .collect::>(); - if out.is_empty() { - return; - } else if out.len() == 1 { - state.types.push_str(&format!("{}", out[0])); - return; - } else { + if out.len() == 1 { + state.types.push_str(&out[0].to_string()); + } else if !out.is_empty() { state.types.push_str(&format!("[ {} ]", out.join(", "))); } } diff --git a/test/enum/rust.rs b/test/enum/rust.rs index a3593ed..2581fe3 100644 --- a/test/enum/rust.rs +++ b/test/enum/rust.rs @@ -17,7 +17,32 @@ enum InternalTopping { ExtraCheese { kind: String }, /// Custom toppings /// May expire soon + /// Note: this test case will not be included in the generated typescript, + /// because it is a tuple variant Custom(CustomTopping), + /// two custom toppings + /// Note: this test case will not be included in the generated typescript, + /// because it is a tuple variant + CustomTwo(CustomTopping, CustomTopping), +} + +/// Adjacently tagged enums have a key-value pair +/// that discrimate which variant it belongs to, and +/// can support tuple variants +#[tsync] +#[serde(tag = "type", content = "value")] +enum AdjacentTopping { + /// Tasty! + /// Not vegetarian + Pepperoni, + /// For cheese lovers + ExtraCheese { kind: String }, + /// Custom toppings + /// May expire soon + Custom(CustomTopping), + /// two custom toppings + /// Note: this test case is specifically for specifying a tuple of types + CustomTwo(CustomTopping, CustomTopping), } /// Externally tagged enums ascribe the value to a key @@ -33,6 +58,9 @@ enum ExternalTopping { /// May expire soon /// Note: this test case is specifically for specifying a single type in the tuple Custom(CustomTopping), + /// two custom toppings + /// Note: this test case is specifically for specifying a tuple of types + CustomTwo(CustomTopping, CustomTopping), } #[tsync] @@ -69,5 +97,5 @@ enum AnimalTwo { #[tsync] #[serde(tag = "type")] enum Tagged { - Test // this should be { type: "Test" } in the TypeScript (not just the string "Test") -} \ No newline at end of file + Test, // this should be { type: "Test" } in the TypeScript (not just the string "Test") +} diff --git a/test/enum/typescript.d.ts b/test/enum/typescript.d.ts index 46f43e3..1da77eb 100644 --- a/test/enum/typescript.d.ts +++ b/test/enum/typescript.d.ts @@ -21,6 +21,46 @@ type InternalTopping__ExtraCheese = { KIND: string; }; +/** + * Adjacently tagged enums have a key-value pair + * that discrimate which variant it belongs to, and + * can support tuple variants + */ +type AdjacentTopping = + | AdjacentTopping__Pepperoni + | AdjacentTopping__ExtraCheese + | AdjacentTopping__Custom + | AdjacentTopping__CustomTwo; + +/** + * Tasty! + * Not vegetarian + */ +type AdjacentTopping__Pepperoni = { + type: "Pepperoni"; +}; +/** For cheese lovers */ +type AdjacentTopping__ExtraCheese = { + type: "ExtraCheese"; + kind: string; +}; +/** + * Custom toppings + * May expire soon + */ +type AdjacentTopping__Custom = { + "type": "Custom"; + "value": CustomTopping; +}; +/** + * two custom toppings + * Note: this test case is specifically for specifying a tuple of types + */ +type AdjacentTopping__CustomTwo = { + "type": "CustomTwo"; + "value": [ CustomTopping, CustomTopping ]; +}; + /** * Externally tagged enums ascribe the value to a key * that is the same as the variant name @@ -44,7 +84,12 @@ type ExternalTopping = * May expire soon * Note: this test case is specifically for specifying a single type in the tuple */ - | { "Custom": CustomTopping }; + | { "Custom": CustomTopping } + /** + * two custom toppings + * Note: this test case is specifically for specifying a tuple of types + */ + | { "CustomTwo": [ CustomTopping, CustomTopping ] }; interface CustomTopping { name: string; diff --git a/test/enum/typescript.ts b/test/enum/typescript.ts index c7a3cc2..62944e3 100644 --- a/test/enum/typescript.ts +++ b/test/enum/typescript.ts @@ -21,6 +21,46 @@ type InternalTopping__ExtraCheese = { KIND: string; }; +/** + * Adjacently tagged enums have a key-value pair + * that discrimate which variant it belongs to, and + * can support tuple variants + */ +export type AdjacentTopping = + | AdjacentTopping__Pepperoni + | AdjacentTopping__ExtraCheese + | AdjacentTopping__Custom + | AdjacentTopping__CustomTwo; + +/** + * Tasty! + * Not vegetarian + */ +type AdjacentTopping__Pepperoni = { + type: "Pepperoni"; +}; +/** For cheese lovers */ +type AdjacentTopping__ExtraCheese = { + type: "ExtraCheese"; + kind: string; +}; +/** + * Custom toppings + * May expire soon + */ +type AdjacentTopping__Custom = { + "type": "Custom"; + "value": CustomTopping; +}; +/** + * two custom toppings + * Note: this test case is specifically for specifying a tuple of types + */ +type AdjacentTopping__CustomTwo = { + "type": "CustomTwo"; + "value": [ CustomTopping, CustomTopping ]; +}; + /** * Externally tagged enums ascribe the value to a key * that is the same as the variant name @@ -44,7 +84,12 @@ export type ExternalTopping = * May expire soon * Note: this test case is specifically for specifying a single type in the tuple */ - | { "Custom": CustomTopping }; + | { "Custom": CustomTopping } + /** + * two custom toppings + * Note: this test case is specifically for specifying a tuple of types + */ + | { "CustomTwo": [ CustomTopping, CustomTopping ] }; export interface CustomTopping { name: string; diff --git a/test/issue-55/rust.rs b/test/issue-55/rust.rs new file mode 100644 index 0000000..2c34557 --- /dev/null +++ b/test/issue-55/rust.rs @@ -0,0 +1,22 @@ +#[tsync] +struct AppleData { + crunchy: bool, +} + +#[tsync] +struct BananaData { + size: i32, +} + +#[tsync] +struct CarrotData { + color: String, +} + +#[tsync] +#[serde(tag = "kind", content = "data")] +enum Fruit { + Apple(AppleData), + Banana(BananaData), + Carrot(CarrotData), +} diff --git a/test/issue-55/tsync.sh b/test/issue-55/tsync.sh new file mode 100755 index 0000000..1764bd6 --- /dev/null +++ b/test/issue-55/tsync.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +cd $SCRIPT_DIR + +cargo run -- -i rust.rs -o typescript.d.ts +cargo run -- -i rust.rs -o typescript.ts \ No newline at end of file diff --git a/test/issue-55/typescript.d.ts b/test/issue-55/typescript.d.ts new file mode 100644 index 0000000..863d6a1 --- /dev/null +++ b/test/issue-55/typescript.d.ts @@ -0,0 +1,31 @@ +/* This file is generated and managed by tsync */ + +interface AppleData { + crunchy: boolean; +} + +interface BananaData { + size: number; +} + +interface CarrotData { + color: string; +} + +type Fruit = + | Fruit__Apple + | Fruit__Banana + | Fruit__Carrot; + +type Fruit__Apple = { + "kind": "Apple"; + "data": AppleData; +}; +type Fruit__Banana = { + "kind": "Banana"; + "data": BananaData; +}; +type Fruit__Carrot = { + "kind": "Carrot"; + "data": CarrotData; +}; diff --git a/test/issue-55/typescript.ts b/test/issue-55/typescript.ts new file mode 100644 index 0000000..3b9dffe --- /dev/null +++ b/test/issue-55/typescript.ts @@ -0,0 +1,31 @@ +/* This file is generated and managed by tsync */ + +export interface AppleData { + crunchy: boolean; +} + +export interface BananaData { + size: number; +} + +export interface CarrotData { + color: string; +} + +export type Fruit = + | Fruit__Apple + | Fruit__Banana + | Fruit__Carrot; + +type Fruit__Apple = { + "kind": "Apple"; + "data": AppleData; +}; +type Fruit__Banana = { + "kind": "Banana"; + "data": BananaData; +}; +type Fruit__Carrot = { + "kind": "Carrot"; + "data": CarrotData; +}; diff --git a/test/issue-58/rust.rs b/test/issue-58/rust.rs new file mode 100644 index 0000000..280d6d9 --- /dev/null +++ b/test/issue-58/rust.rs @@ -0,0 +1,21 @@ +#[derive(Serialize, Deserialize)] +#[tsync] +pub struct CameraControl { + pub camera_uuid: String, + #[serde(flatten)] + pub action: Action, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "action", content = "json")] +#[tsync] +pub enum Action { + GetVideoParameterSettings(VideoParameterSettings), +} + +#[skip_serializing_none] +#[derive(Serialize, Deserialize)] +#[tsync] +pub struct VideoParameterSettings { + pub frame_rate: Option, +} diff --git a/test/issue-58/tsync.sh b/test/issue-58/tsync.sh new file mode 100755 index 0000000..1764bd6 --- /dev/null +++ b/test/issue-58/tsync.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +cd $SCRIPT_DIR + +cargo run -- -i rust.rs -o typescript.d.ts +cargo run -- -i rust.rs -o typescript.ts \ No newline at end of file diff --git a/test/issue-58/typescript.d.ts b/test/issue-58/typescript.d.ts new file mode 100644 index 0000000..2e868cf --- /dev/null +++ b/test/issue-58/typescript.d.ts @@ -0,0 +1,17 @@ +/* This file is generated and managed by tsync */ + +type CameraControl = Action & { + camera_uuid: string; +} + +type Action = + | Action__GetVideoParameterSettings; + +type Action__GetVideoParameterSettings = { + "action": "GetVideoParameterSettings"; + "json": VideoParameterSettings; +}; + +interface VideoParameterSettings { + frame_rate?: number; +} diff --git a/test/issue-58/typescript.ts b/test/issue-58/typescript.ts new file mode 100644 index 0000000..5273f50 --- /dev/null +++ b/test/issue-58/typescript.ts @@ -0,0 +1,17 @@ +/* This file is generated and managed by tsync */ + +export type CameraControl = Action & { + camera_uuid: string; +} + +export type Action = + | Action__GetVideoParameterSettings; + +type Action__GetVideoParameterSettings = { + "action": "GetVideoParameterSettings"; + "json": VideoParameterSettings; +}; + +export interface VideoParameterSettings { + frame_rate?: number; +} diff --git a/test/test_all.sh b/test/test_all.sh index c362675..abbbc5c 100755 --- a/test/test_all.sh +++ b/test/test_all.sh @@ -14,3 +14,5 @@ cd $SCRIPT_DIR ./doc_comments/tsync.sh ./generic/tsync.sh ./issue-43/tsync.sh +./issue-55/tsync.sh +./issue-58/tsync.sh \ No newline at end of file From 2f14fac73ef94c00910cf2907a04fe80b529c262 Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:37:14 -0700 Subject: [PATCH 4/7] feat: better support for generics in enums --- src/to_typescript/enums.rs | 36 +++++++++++++++++++++----- src/to_typescript/structs.rs | 8 +++--- src/utils.rs | 49 +++++++++++++++++++++++++++++------- test/generic/rust.rs | 31 +++++++++++++++++++++++ test/generic/typescript.d.ts | 31 +++++++++++++++++++++++ test/generic/typescript.ts | 31 +++++++++++++++++++++++ 6 files changed, 166 insertions(+), 20 deletions(-) diff --git a/src/to_typescript/enums.rs b/src/to_typescript/enums.rs index 897c4ce..f228855 100644 --- a/src/to_typescript/enums.rs +++ b/src/to_typescript/enums.rs @@ -202,13 +202,29 @@ fn add_internally_tagged_enum( uses_type_interface: bool, ) { let export = if uses_type_interface { "" } else { "export " }; + let generics = utils::extract_struct_generics(exported_struct.generics.clone()); state.types.push_str(&format!( "{export}type {interface_name}{generics} =", interface_name = exported_struct.ident, - generics = utils::extract_struct_generics(exported_struct.generics.clone()) + generics = utils::format_generics(&generics) )); + // a list of the generics for each variant, so we don't need to recalculate them + let mut variant_generics_list = Vec::new(); + for variant in exported_struct.variants.iter() { + let variant_field_types = variant.fields.iter().map(|f| f.ty.to_owned()); + let variant_generics = generics + .iter() + .filter(|gen| { + variant_field_types + .clone() + .any(|ty| utils::type_contains_ident(&ty, gen)) + }) + .cloned() + .collect::>(); + variant_generics_list.push(variant_generics.clone()); + // Assumes that non-newtype tuple variants have already been filtered out if variant.fields.iter().any(|v| v.ident.is_none()) && content_name.is_none() { // TODO: Generate newtype structure @@ -218,16 +234,23 @@ fn add_internally_tagged_enum( } else { state.types.push('\n'); state.types.push_str(&format!( - " | {interface_name}__{variant_name}", + " | {interface_name}__{variant_name}{generics}", interface_name = exported_struct.ident, variant_name = variant.ident, + generics = utils::format_generics(&variant_generics) )) } } state.types.push_str(";\n"); - for variant in exported_struct.variants { + for (variant, generics) in exported_struct + .variants + .into_iter() + .zip(variant_generics_list) + { + let generics = utils::format_generics(&generics); + match (&variant.fields, content_name.as_ref()) { // adjacently tagged (syn::Fields::Unnamed(fields), Some(content_name)) => { @@ -235,7 +258,7 @@ fn add_internally_tagged_enum( let comments = utils::get_comments(variant.attrs); state.write_comments(&comments, 0); state.types.push_str(&format!( - "type {interface_name}__{variant_name} = ", + "type {interface_name}__{variant_name}{generics} = ", interface_name = exported_struct.ident, variant_name = variant.ident, )); @@ -264,7 +287,7 @@ fn add_internally_tagged_enum( let comments = utils::get_comments(variant.attrs); state.write_comments(&comments, 0); state.types.push_str(&format!( - "type {interface_name}__{variant_name} = ", + "type {interface_name}__{variant_name}{generics} = ", interface_name = exported_struct.ident, variant_name = variant.ident, )); @@ -297,10 +320,11 @@ fn add_externally_tagged_enum( uses_type_interface: bool, ) { let export = if uses_type_interface { "" } else { "export " }; + let generics = utils::extract_struct_generics(exported_struct.generics.clone()); state.types.push_str(&format!( "{export}type {interface_name}{generics} =", interface_name = exported_struct.ident, - generics = utils::extract_struct_generics(exported_struct.generics.clone()) + generics = utils::format_generics(&generics) )); for variant in exported_struct.variants { diff --git a/src/to_typescript/structs.rs b/src/to_typescript/structs.rs index 7e6bd16..e5117be 100644 --- a/src/to_typescript/structs.rs +++ b/src/to_typescript/structs.rs @@ -18,6 +18,9 @@ impl super::ToTypescript for syn::ItemStruct { let intersections = get_intersections(&self.fields); + let generics = utils::extract_struct_generics(self.generics.clone()); + let generics = utils::format_generics(&generics); + match ( intersections, matches!(self.fields, syn::Fields::Unnamed(_)), @@ -25,9 +28,7 @@ impl super::ToTypescript for syn::ItemStruct { (Some(intersections), false) => { state.types.push_str(&format!( "{export}type {struct_name}{generics} = {intersections} & ", - export = export, struct_name = self.ident, - generics = utils::extract_struct_generics(self.generics.clone()), intersections = intersections )); } @@ -35,15 +36,12 @@ impl super::ToTypescript for syn::ItemStruct { state.types.push_str(&format!( "{export}interface {interface_name}{generics} ", interface_name = self.ident, - generics = utils::extract_struct_generics(self.generics.clone()) )); } (None, true) => { state.types.push_str(&format!( "{export}type {struct_name}{generics} = ", - export = export, struct_name = self.ident, - generics = utils::extract_struct_generics(self.generics.clone()), )); } (Some(_), true) => { diff --git a/src/utils.rs b/src/utils.rs index 7bd33a9..db2c210 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -188,23 +188,54 @@ pub fn build_indentation(indentation_amount: i8) -> String { (0..indentation_amount).map(|_| '\u{0020}').collect() } -pub fn extract_struct_generics(s: syn::Generics) -> String { - let out: Vec = s - .params +pub fn extract_struct_generics(s: syn::Generics) -> Vec { + s.params .into_iter() .filter_map(|gp| { if let syn::GenericParam::Type(ty) = gp { - Some(ty) + Some(ty.ident) } else { None } }) - .map(|ty| ty.ident.to_string()) - .collect(); + .collect() +} + +pub fn format_generics(generics: &[syn::Ident]) -> String { + if generics.is_empty() { + return String::new(); + } - out.is_empty() - .then(Default::default) - .unwrap_or(format!("<{}>", out.join(", "))) + let generics = generics + .iter() + .map(|g| g.to_string()) + .collect::>() + .join(", "); + format!("<{}>", generics) +} + +/// Determine whether a type contains the given ident. +pub fn type_contains_ident(ty: &syn::Type, ident: &syn::Ident) -> bool { + match ty { + syn::Type::Path(ty_path) => ty_path.path.segments.iter().any(|segment| { + (match segment.arguments { + syn::PathArguments::AngleBracketed(ref angle_bracketed) => { + angle_bracketed.args.iter().any(|arg| { + if let syn::GenericArgument::Type(ref ty) = arg { + type_contains_ident(ty, ident) + } else { + false + } + }) + } + _ => false, + }) || &segment.ident == ident + }), + syn::Type::Slice(ty_slice) => type_contains_ident(&ty_slice.elem, ident), + syn::Type::Array(ty_array) => type_contains_ident(&ty_array.elem, ident), + syn::Type::Reference(ty_ref) => type_contains_ident(&ty_ref.elem, ident), + _ => false, + } } /// Get the attribute matching needle name. diff --git a/test/generic/rust.rs b/test/generic/rust.rs index 50ca04b..aeed2af 100644 --- a/test/generic/rust.rs +++ b/test/generic/rust.rs @@ -17,3 +17,34 @@ struct Paginated { page: u32, total_pages: u32, } + +#[tsync] +struct Flatten { + name: String, + #[serde(flatten)] + data: Vec, +} + +/** + * Test enum represenations w/ generics + */ + +#[tsync] +enum ExternalEnum { + Bar(T), + Waz(U), +} + +#[tsync] +#[serde(tag = "type", content = "value")] +enum AdjacentEnum { + Bar(T), + Waz(U), +} + +#[tsync] +#[serde(tag = "type")] +enum InternalEnum { + Bar { value: T, alias: String }, + Waz(U), +} diff --git a/test/generic/typescript.d.ts b/test/generic/typescript.d.ts index 6f40c65..222a906 100644 --- a/test/generic/typescript.d.ts +++ b/test/generic/typescript.d.ts @@ -10,3 +10,34 @@ interface Paginated { page: number; total_pages: number; } + +type Flatten = Array & { + name: string; +} + +/** \n * Test enum represenations w/ generics\n */ +type ExternalEnum = + | { "Bar": T } + | { "Waz": U }; + +type AdjacentEnum = + | AdjacentEnum__Bar + | AdjacentEnum__Waz; + +type AdjacentEnum__Bar = { + "type": "Bar"; + "value": T; +}; +type AdjacentEnum__Waz = { + "type": "Waz"; + "value": U; +}; + +type InternalEnum = + | InternalEnum__Bar; + +type InternalEnum__Bar = { + type: "Bar"; + value: T; + alias: string; +}; diff --git a/test/generic/typescript.ts b/test/generic/typescript.ts index adba890..3af3999 100644 --- a/test/generic/typescript.ts +++ b/test/generic/typescript.ts @@ -10,3 +10,34 @@ export interface Paginated { page: number; total_pages: number; } + +export type Flatten = Array & { + name: string; +} + +/** \n * Test enum represenations w/ generics\n */ +export type ExternalEnum = + | { "Bar": T } + | { "Waz": U }; + +export type AdjacentEnum = + | AdjacentEnum__Bar + | AdjacentEnum__Waz; + +type AdjacentEnum__Bar = { + "type": "Bar"; + "value": T; +}; +type AdjacentEnum__Waz = { + "type": "Waz"; + "value": U; +}; + +export type InternalEnum = + | InternalEnum__Bar; + +type InternalEnum__Bar = { + type: "Bar"; + value: T; + alias: string; +}; From c4a7aa7ce0953ddd994850db9a4842b7be8cfb39 Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:05:53 -0700 Subject: [PATCH 5/7] feat: supprt internally tagged newtype variants --- src/to_typescript/enums.rs | 65 +++++++++++++++++++++++-------- test/doc_comments/typescript.d.ts | 4 ++ test/doc_comments/typescript.ts | 4 ++ test/enum/rust.rs | 3 +- test/enum/typescript.d.ts | 10 ++++- test/enum/typescript.ts | 10 ++++- test/enum_newtype/rust.rs | 53 +++++++++++++++++++++++++ test/enum_newtype/tsync.sh | 8 ++++ test/enum_newtype/typescript.d.ts | 52 +++++++++++++++++++++++++ test/enum_newtype/typescript.ts | 52 +++++++++++++++++++++++++ test/generic/typescript.d.ts | 5 ++- test/generic/typescript.ts | 5 ++- test/test_all.sh | 1 + 13 files changed, 250 insertions(+), 22 deletions(-) create mode 100644 test/enum_newtype/rust.rs create mode 100755 test/enum_newtype/tsync.sh create mode 100644 test/enum_newtype/typescript.d.ts create mode 100644 test/enum_newtype/typescript.ts diff --git a/src/to_typescript/enums.rs b/src/to_typescript/enums.rs index f228855..2c6f412 100644 --- a/src/to_typescript/enums.rs +++ b/src/to_typescript/enums.rs @@ -1,4 +1,4 @@ -use crate::{utils, BuildState}; +use crate::{typescript::convert_type, utils, BuildState}; use convert_case::{Case, Casing}; use syn::__private::ToTokens; @@ -223,22 +223,24 @@ fn add_internally_tagged_enum( }) .cloned() .collect::>(); - variant_generics_list.push(variant_generics.clone()); // Assumes that non-newtype tuple variants have already been filtered out - if variant.fields.iter().any(|v| v.ident.is_none()) && content_name.is_none() { - // TODO: Generate newtype structure - // This should contain the discriminant plus all fields of the inner structure as a flat structure - // TODO: Check for case where discriminant name matches an inner structure field name - // We should reject clashes - } else { - state.types.push('\n'); - state.types.push_str(&format!( - " | {interface_name}__{variant_name}{generics}", - interface_name = exported_struct.ident, - variant_name = variant.ident, - generics = utils::format_generics(&variant_generics) - )) + // TODO: Check for case where discriminant name matches an inner structure field name + // We should reject clashes + match &variant.fields { + syn::Fields::Unnamed(fields) if fields.unnamed.len() > 1 && content_name.is_none() => { + continue; + } + _ => { + variant_generics_list.push(variant_generics.clone()); + state.types.push('\n'); + state.types.push_str(&format!( + " | {interface_name}__{variant_name}{generics}", + interface_name = exported_struct.ident, + variant_name = variant.ident, + generics = utils::format_generics(&variant_generics) + )) + } } } @@ -271,7 +273,38 @@ fn add_internally_tagged_enum( super::structs::process_tuple_fields(fields.clone(), state); state.types.push_str(";\n};"); } - // missing content name + // missing content name, but is a newtype variant + (syn::Fields::Unnamed(fields), None) if fields.unnamed.len() <= 1 => { + state.types.push('\n'); + let comments = utils::get_comments(variant.attrs); + state.write_comments(&comments, 0); + state.types.push_str(&format!( + "type {interface_name}__{variant_name}{generics} = ", + interface_name = exported_struct.ident, + variant_name = variant.ident, + )); + + let field_name = if let Some(casing) = casing { + variant.ident.to_string().to_case(casing) + } else { + variant.ident.to_string() + }; + // add discriminant + state.types.push_str(&format!( + "{{\n{}{}: \"{}\"}}", + utils::build_indentation(2), + tag_name, + field_name, + )); + + // add the newtype field + let newtype = convert_type(&fields.unnamed.first().unwrap().ty); + state.types.push_str(&format!( + " & {content_name}", + content_name = newtype.ts_type + )); + } + // missing content name, and is not a newtype, this is an error case (syn::Fields::Unnamed(_), None) => { if crate::DEBUG.try_get().is_some_and(|d: &bool| *d) { println!( diff --git a/test/doc_comments/typescript.d.ts b/test/doc_comments/typescript.d.ts index e168fba..ee3973f 100644 --- a/test/doc_comments/typescript.d.ts +++ b/test/doc_comments/typescript.d.ts @@ -3,12 +3,16 @@ /** enum comment */ type EnumTest = | EnumTest__One + | EnumTest__Two | EnumTest__Three; /** enum property comment */ type EnumTest__One = { type: "ONE"; }; +/** enum tuple comment */ +type EnumTest__Two = { + type: "TWO"} & StructTest /** enum struct comment */ type EnumTest__Three = { type: "THREE"; diff --git a/test/doc_comments/typescript.ts b/test/doc_comments/typescript.ts index df1256e..8043c9a 100644 --- a/test/doc_comments/typescript.ts +++ b/test/doc_comments/typescript.ts @@ -3,12 +3,16 @@ /** enum comment */ export type EnumTest = | EnumTest__One + | EnumTest__Two | EnumTest__Three; /** enum property comment */ type EnumTest__One = { type: "ONE"; }; +/** enum tuple comment */ +type EnumTest__Two = { + type: "TWO"} & StructTest /** enum struct comment */ type EnumTest__Three = { type: "THREE"; diff --git a/test/enum/rust.rs b/test/enum/rust.rs index 2581fe3..4c0dc3b 100644 --- a/test/enum/rust.rs +++ b/test/enum/rust.rs @@ -17,8 +17,7 @@ enum InternalTopping { ExtraCheese { kind: String }, /// Custom toppings /// May expire soon - /// Note: this test case will not be included in the generated typescript, - /// because it is a tuple variant + /// Note: because this is a newtype variant, it should be included in the typescript Custom(CustomTopping), /// two custom toppings /// Note: this test case will not be included in the generated typescript, diff --git a/test/enum/typescript.d.ts b/test/enum/typescript.d.ts index 1da77eb..1d9e441 100644 --- a/test/enum/typescript.d.ts +++ b/test/enum/typescript.d.ts @@ -6,7 +6,8 @@ */ type InternalTopping = | InternalTopping__Pepperoni - | InternalTopping__ExtraCheese; + | InternalTopping__ExtraCheese + | InternalTopping__Custom; /** * Tasty! @@ -20,6 +21,13 @@ type InternalTopping__ExtraCheese = { type: "EXTRA CHEESE"; KIND: string; }; +/** + * Custom toppings + * May expire soon + * Note: because this is a newtype variant, it should be included in the typescript + */ +type InternalTopping__Custom = { + type: "CUSTOM"} & CustomTopping /** * Adjacently tagged enums have a key-value pair diff --git a/test/enum/typescript.ts b/test/enum/typescript.ts index 62944e3..f7ffc9e 100644 --- a/test/enum/typescript.ts +++ b/test/enum/typescript.ts @@ -6,7 +6,8 @@ */ export type InternalTopping = | InternalTopping__Pepperoni - | InternalTopping__ExtraCheese; + | InternalTopping__ExtraCheese + | InternalTopping__Custom; /** * Tasty! @@ -20,6 +21,13 @@ type InternalTopping__ExtraCheese = { type: "EXTRA CHEESE"; KIND: string; }; +/** + * Custom toppings + * May expire soon + * Note: because this is a newtype variant, it should be included in the typescript + */ +type InternalTopping__Custom = { + type: "CUSTOM"} & CustomTopping /** * Adjacently tagged enums have a key-value pair diff --git a/test/enum_newtype/rust.rs b/test/enum_newtype/rust.rs new file mode 100644 index 0000000..6d1c2b0 --- /dev/null +++ b/test/enum_newtype/rust.rs @@ -0,0 +1,53 @@ +/// test/rust.rs +use tsync::tsync; + +#[derive(Serialize, Deserialize)] +#[tsync] +#[serde(tag = "type")] +enum Message { + Request(Request), + Response(Response), +} + +#[derive(Serialize, Deserialize)] +#[tsync] +struct Request { + id: String, + method_type: String, + params: Params, +} + +#[derive(Serialize, Deserialize)] +#[tsync] +struct Response { + id: String, + result: Value, +} + +#[derive(Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +#[tsync] +pub enum CaptureConfigurationStruct { + Video { pub height: u32, pub width: u32 }, + Redirect, +} + +/// cases below were provided by joaoantoniocardoso on github in the discussion for issue #58 +#[derive(Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +#[tsync] +pub enum CaptureConfigurationNewtype { + Video(VideoCaptureConfiguration), + Redirect(RedirectCaptureConfiguration), +} + +#[derive(Deserialize, Serialize)] +#[tsync] +pub struct VideoCaptureConfiguration { + pub height: u32, + pub width: u32, +} + +#[derive(Deserialize, Serialize)] +#[tsync] +pub struct RedirectCaptureConfiguration {} diff --git a/test/enum_newtype/tsync.sh b/test/enum_newtype/tsync.sh new file mode 100755 index 0000000..1764bd6 --- /dev/null +++ b/test/enum_newtype/tsync.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +cd $SCRIPT_DIR + +cargo run -- -i rust.rs -o typescript.d.ts +cargo run -- -i rust.rs -o typescript.ts \ No newline at end of file diff --git a/test/enum_newtype/typescript.d.ts b/test/enum_newtype/typescript.d.ts new file mode 100644 index 0000000..b277913 --- /dev/null +++ b/test/enum_newtype/typescript.d.ts @@ -0,0 +1,52 @@ +/* This file is generated and managed by tsync */ + +type Message = + | Message__Request + | Message__Response; + +type Message__Request = { + type: "Request"} & Request +type Message__Response = { + type: "Response"} & Response + +interface Request { + id: string; + method_type: string; + params: Params; +} + +interface Response { + id: string; + result: Value; +} + +type CaptureConfigurationStruct = + | CaptureConfigurationStruct__Video + | CaptureConfigurationStruct__Redirect; + +type CaptureConfigurationStruct__Video = { + type: "video"; + height: number; + width: number; +}; +type CaptureConfigurationStruct__Redirect = { + type: "redirect"; +}; + +/** cases below were provided by joaoantoniocardoso on github in the discussion for issue #58 */ +type CaptureConfigurationNewtype = + | CaptureConfigurationNewtype__Video + | CaptureConfigurationNewtype__Redirect; + +type CaptureConfigurationNewtype__Video = { + type: "video"} & VideoCaptureConfiguration +type CaptureConfigurationNewtype__Redirect = { + type: "redirect"} & RedirectCaptureConfiguration + +interface VideoCaptureConfiguration { + height: number; + width: number; +} + +interface RedirectCaptureConfiguration { +} diff --git a/test/enum_newtype/typescript.ts b/test/enum_newtype/typescript.ts new file mode 100644 index 0000000..46ef286 --- /dev/null +++ b/test/enum_newtype/typescript.ts @@ -0,0 +1,52 @@ +/* This file is generated and managed by tsync */ + +export type Message = + | Message__Request + | Message__Response; + +type Message__Request = { + type: "Request"} & Request +type Message__Response = { + type: "Response"} & Response + +export interface Request { + id: string; + method_type: string; + params: Params; +} + +export interface Response { + id: string; + result: Value; +} + +export type CaptureConfigurationStruct = + | CaptureConfigurationStruct__Video + | CaptureConfigurationStruct__Redirect; + +type CaptureConfigurationStruct__Video = { + type: "video"; + height: number; + width: number; +}; +type CaptureConfigurationStruct__Redirect = { + type: "redirect"; +}; + +/** cases below were provided by joaoantoniocardoso on github in the discussion for issue #58 */ +export type CaptureConfigurationNewtype = + | CaptureConfigurationNewtype__Video + | CaptureConfigurationNewtype__Redirect; + +type CaptureConfigurationNewtype__Video = { + type: "video"} & VideoCaptureConfiguration +type CaptureConfigurationNewtype__Redirect = { + type: "redirect"} & RedirectCaptureConfiguration + +export interface VideoCaptureConfiguration { + height: number; + width: number; +} + +export interface RedirectCaptureConfiguration { +} diff --git a/test/generic/typescript.d.ts b/test/generic/typescript.d.ts index 222a906..79302e6 100644 --- a/test/generic/typescript.d.ts +++ b/test/generic/typescript.d.ts @@ -34,10 +34,13 @@ type AdjacentEnum__Waz = { }; type InternalEnum = - | InternalEnum__Bar; + | InternalEnum__Bar + | InternalEnum__Waz; type InternalEnum__Bar = { type: "Bar"; value: T; alias: string; }; +type InternalEnum__Waz = { + type: "Waz"} & U diff --git a/test/generic/typescript.ts b/test/generic/typescript.ts index 3af3999..9ac4e91 100644 --- a/test/generic/typescript.ts +++ b/test/generic/typescript.ts @@ -34,10 +34,13 @@ type AdjacentEnum__Waz = { }; export type InternalEnum = - | InternalEnum__Bar; + | InternalEnum__Bar + | InternalEnum__Waz; type InternalEnum__Bar = { type: "Bar"; value: T; alias: string; }; +type InternalEnum__Waz = { + type: "Waz"} & U diff --git a/test/test_all.sh b/test/test_all.sh index abbbc5c..9ecad5e 100755 --- a/test/test_all.sh +++ b/test/test_all.sh @@ -10,6 +10,7 @@ cd $SCRIPT_DIR ./const/tsync.sh ./const_enum_numeric/tsync.sh ./enum/tsync.sh +./enum_newtype/tsync.sh ./enum_numeric/tsync.sh ./doc_comments/tsync.sh ./generic/tsync.sh From b0f4ef8d9b2b142a7cb03cf7209d2cc0a1b25d65 Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:33:42 -0700 Subject: [PATCH 6/7] feat: add support for empty object type in struct and enum processing --- src/to_typescript/enums.rs | 14 ++++++-------- src/to_typescript/structs.rs | 16 +++++++++++++++- test/enum/typescript.d.ts | 4 +++- test/enum/typescript.ts | 4 +++- test/enum_newtype/typescript.d.ts | 1 + test/enum_newtype/typescript.ts | 1 + 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/to_typescript/enums.rs b/src/to_typescript/enums.rs index 2c6f412..b93ad70 100644 --- a/src/to_typescript/enums.rs +++ b/src/to_typescript/enums.rs @@ -337,7 +337,7 @@ fn add_internally_tagged_enum( tag_name, field_name, )); - super::structs::process_fields(variant.fields, state, 2, casing); + super::structs::process_fields(variant.fields, state, 2, casing, false); state.types.push_str("};"); } } @@ -385,13 +385,11 @@ fn add_externally_tagged_enum( field_name, )); let prepend; - if variant.fields.is_empty() { - prepend = "".into(); - } else { - prepend = utils::build_indentation(6); - state.types.push('\n'); - super::structs::process_fields(variant.fields, state, 8, casing); - } + + prepend = utils::build_indentation(6); + state.types.push('\n'); + super::structs::process_fields(variant.fields, state, 8, casing, true); + state .types .push_str(&format!("{}}}\n{}}}", prepend, utils::build_indentation(4))); diff --git a/src/to_typescript/structs.rs b/src/to_typescript/structs.rs index e5117be..f9c7a0b 100644 --- a/src/to_typescript/structs.rs +++ b/src/to_typescript/structs.rs @@ -59,7 +59,7 @@ impl super::ToTypescript for syn::ItemStruct { process_tuple_fields(unnamed, state); } else { state.types.push_str("{\n"); - process_fields(self.fields, state, 2, casing); + process_fields(self.fields, state, 2, casing, true); state.types.push('}'); } @@ -67,14 +67,28 @@ impl super::ToTypescript for syn::ItemStruct { } } +static EMPTY_OBJECT_TYPE: &'static str = "[key: PropertyKey]: never;\n"; + +/// # arguments +/// +/// - `use_empty_object_type` - if true, will use the empty object type as the type of the struct if it has no fields pub fn process_fields( fields: syn::Fields, state: &mut BuildState, indentation_amount: i8, case: impl Into>, + use_empty_object_type: bool, ) { let space = utils::build_indentation(indentation_amount); let case = case.into(); + + // handle empty objects + if fields.is_empty() && use_empty_object_type { + state.types.push_str(&space); + state.types.push_str(EMPTY_OBJECT_TYPE); + return; + } + for field in fields { debug_assert!( field.ident.is_some(), diff --git a/test/enum/typescript.d.ts b/test/enum/typescript.d.ts index 1d9e441..5d132bb 100644 --- a/test/enum/typescript.d.ts +++ b/test/enum/typescript.d.ts @@ -79,7 +79,9 @@ type ExternalTopping = * Not vegetarian */ | { - "Pepperoni": {} + "Pepperoni": { + [key: PropertyKey]: never; + } } /** For cheese lovers */ | { diff --git a/test/enum/typescript.ts b/test/enum/typescript.ts index f7ffc9e..9d8afe0 100644 --- a/test/enum/typescript.ts +++ b/test/enum/typescript.ts @@ -79,7 +79,9 @@ export type ExternalTopping = * Not vegetarian */ | { - "Pepperoni": {} + "Pepperoni": { + [key: PropertyKey]: never; + } } /** For cheese lovers */ | { diff --git a/test/enum_newtype/typescript.d.ts b/test/enum_newtype/typescript.d.ts index b277913..40980a2 100644 --- a/test/enum_newtype/typescript.d.ts +++ b/test/enum_newtype/typescript.d.ts @@ -49,4 +49,5 @@ interface VideoCaptureConfiguration { } interface RedirectCaptureConfiguration { + [key: PropertyKey]: never; } diff --git a/test/enum_newtype/typescript.ts b/test/enum_newtype/typescript.ts index 46ef286..0302c1a 100644 --- a/test/enum_newtype/typescript.ts +++ b/test/enum_newtype/typescript.ts @@ -49,4 +49,5 @@ export interface VideoCaptureConfiguration { } export interface RedirectCaptureConfiguration { + [key: PropertyKey]: never; } From 7c75aa53f90b747bd68d0018dad493663d0522f2 Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Sat, 15 Mar 2025 21:48:43 -0700 Subject: [PATCH 7/7] fix(ci): fix cache step also, use Swatinem/rust-cache instead of actions/cache --- .github/workflows/CI.yml | 46 +++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c9feace..782b689 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -27,20 +27,19 @@ jobs: name: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4 - uses: rui314/setup-mold@v1 - - uses: actions-rs/toolchain@v1.0.7 + - name: Install Rust toolchain + run: | + rustup show + rustup -V + rustup set profile minimal + rustup toolchain install stable + rustup override set stable + - name: Setup cache + uses: Swatinem/rust-cache@v2 with: - profile: minimal - toolchain: stable - override: true - - uses: actions/cache@v3.2.4 - with: - path: | - ./.cargo/.build - ./target - ~/.cargo - key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }} + shared-key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }} - run: cargo check --all-targets --all-features test: @@ -48,19 +47,18 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4 - uses: rui314/setup-mold@v1 - - uses: actions-rs/toolchain@v1.0.7 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions/cache@v3.2.4 + - name: Install Rust toolchain + run: | + rustup show + rustup -V + rustup set profile minimal + rustup toolchain install stable + rustup override set stable + - name: Setup cache + uses: Swatinem/rust-cache@v2 with: - path: | - ./.cargo/.build - ./target - ~/.cargo - key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }} + shared-key: ${{ runner.os }}-cargo-dev-${{ hashFiles('**/Cargo.lock') }} - run: bash test/test_all.sh - run: git diff --exit-code --quiet || exit 1