diff --git a/README.md b/README.md index 14a6818f..6e37f940 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ export type User = { user_id: number, first_name: string, last_name: string, }; ### Features - generate type declarations from rust structs -- generate union declarations from rust enums +- generate union declarations (and native typescript enums) from rust enums - inline types - flatten structs/types - generate necessary imports when exporting to multiple files diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index 074c6dc0..fbe5dea2 100644 --- a/macros/src/attr/enum.rs +++ b/macros/src/attr/enum.rs @@ -13,6 +13,7 @@ pub struct EnumAttr { crate_rename: Option, pub type_as: Option, pub type_override: Option, + pub use_ts_enum: bool, pub rename_all: Option, pub rename_all_fields: Option, pub rename: Option, @@ -75,6 +76,7 @@ impl Attr for EnumAttr { crate_rename: self.crate_rename.or(other.crate_rename), type_as: self.type_as.or(other.type_as), type_override: self.type_override.or(other.type_override), + use_ts_enum: self.use_ts_enum || other.use_ts_enum, rename: self.rename.or(other.rename), rename_all: self.rename_all.or(other.rename_all), rename_all_fields: self.rename_all_fields.or(other.rename_all_fields), @@ -94,6 +96,38 @@ impl Attr for EnumAttr { } fn assert_validity(&self, item: &Self::Item) -> Result<()> { + if self.use_ts_enum { + if self.tag.is_some() { + syn_err_spanned!( + item; + "`tag` is not compatible with `use_ts_enum`" + ); + } + if self.type_override.is_some() { + syn_err_spanned!( + item; + "`type_override` is not compatible with `use_ts_enum`" + ); + } + if self.type_as.is_some() { + syn_err_spanned!( + item; + "`type_as` is not compatible with `use_ts_enum`" + ); + } + + match (&self.rename_all, &self.rename_all_fields) { + (Some(Inflection::Kebab | Inflection::ScreamingKebab), _) | + (_, Some(Inflection::Kebab | Inflection::ScreamingKebab)) => { + syn_err_spanned!( + item; + "`use_ts_enum` is not compatible with kebab case renaming" + ); + }, + _ => {}, + } + } + if self.type_override.is_some() { if self.type_as.is_some() { syn_err_spanned!( @@ -208,6 +242,7 @@ impl_parse! { "crate" => out.crate_rename = Some(parse_assign_from_str(input)?), "as" => out.type_as = Some(parse_assign_from_str(input)?), "type" => out.type_override = Some(parse_assign_str(input)?), + "use_ts_enum" => out.use_ts_enum = true, "rename" => out.rename = Some(parse_assign_expr(input)?), "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), "rename_all_fields" => out.rename_all_fields = Some(parse_assign_inflection(input)?), diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 95c7f50d..51a0c0e4 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -29,7 +29,7 @@ struct DerivedTS { dependencies: Dependencies, concrete: HashMap, bound: Option>, - + is_ts_enum: bool, export: bool, export_to: Option, } @@ -253,9 +253,17 @@ impl DerivedTS { } }, ); - let inline = quote! { - fn inline() -> String { - #inline + let inline = if self.is_ts_enum { + quote! { + fn inline() -> String { + ::inline_flattened() + } + } + } else { + quote! { + fn inline() -> String { + #inline + } } }; quote! { @@ -295,6 +303,19 @@ impl DerivedTS { // use instead. This might be something to change in the future. G::Const(ConstParam { ident, .. }) => Some(quote!(#ident)), }); + + if self.is_ts_enum { + let inline = &self.inline; + return quote! { + fn decl_concrete() -> String { + format!("enum {} {{ {} }}", #name, #inline) + } + fn decl() -> String { + <#rust_ty<#(#generic_idents,)*> as #crate_rename::TS>::decl_concrete() + } + }; + } + quote! { fn decl_concrete() -> String { format!("type {} = {};", #name, ::inline()) diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 737efffa..c43c802f 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -45,13 +45,37 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { variant, )?; } + + let (inline, inline_flattened) = if enum_attr.use_ts_enum { + let pairs = formatted_variants + .iter() + .map(|name| quote!(format!("{} = \"{}\"", #name, #name))) + .collect::>(); + let strings = formatted_variants + .iter() + .map(|name| quote!(format!("\"{}\"", #name))) + .collect::>(); + + ( + quote!([#(#pairs),*].join(", ")), + Some(quote!( + format!("({})", [#(#strings),*].join(" | ")) + )), + ) + } + else { + ( + quote!([#(#formatted_variants),*].join(" | ")), + Some(quote!( + format!("({})", [#(#formatted_variants),*].join(" | ")) + )), + ) + }; Ok(DerivedTS { crate_rename, - inline: quote!([#(#formatted_variants),*].join(" | ")), - inline_flattened: Some(quote!( - format!("({})", [#(#formatted_variants),*].join(" | ")) - )), + inline, + inline_flattened, dependencies, docs: enum_attr.docs, export: enum_attr.export, @@ -59,6 +83,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { ts_name: name, concrete: enum_attr.concrete, bound: enum_attr.bound, + is_ts_enum: enum_attr.use_ts_enum, }) } @@ -92,6 +117,14 @@ fn format_variant( variant.ident.span(), ), }; + + if enum_attr.use_ts_enum { + if !variant.fields.is_empty() { + syn_err_spanned!(variant; "`use_ts_enum` requires plain enum fields"); + } + formatted_variants.push(quote!(#ts_name)); + return Ok(()); + } let struct_attr = StructAttr::from_variant(enum_attr, &variant_attr, &variant.fields); let variant_type = types::type_def( @@ -213,5 +246,6 @@ fn empty_enum(ts_name: Expr, enum_attr: EnumAttr) -> DerivedTS { ts_name, concrete: enum_attr.concrete, bound: enum_attr.bound, + is_ts_enum: false, } } diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 9d265640..0b44e1f0 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -72,6 +72,7 @@ pub(crate) fn named(attr: &StructAttr, ts_name: Expr, fields: &FieldsNamed) -> R ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, }) } diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index 722dde68..2db462b6 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -50,5 +50,6 @@ pub(crate) fn newtype( ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, }) } diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index 27cca5d7..eefc149d 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -39,6 +39,7 @@ pub(crate) fn tuple(attr: &StructAttr, ts_name: Expr, fields: &FieldsUnnamed) -> ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, }) } diff --git a/macros/src/types/type_as.rs b/macros/src/types/type_as.rs index ccdce144..eab493b1 100644 --- a/macros/src/types/type_as.rs +++ b/macros/src/types/type_as.rs @@ -28,6 +28,7 @@ pub(crate) fn type_as_struct( ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, }) } @@ -48,5 +49,6 @@ pub(crate) fn type_as_enum(attr: &EnumAttr, ts_name: Expr, type_as: &Type) -> Re ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, }) } diff --git a/macros/src/types/type_override.rs b/macros/src/types/type_override.rs index 173f6f03..9cd1edcb 100644 --- a/macros/src/types/type_override.rs +++ b/macros/src/types/type_override.rs @@ -25,6 +25,7 @@ pub(crate) fn type_override_struct( ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, }) } @@ -46,5 +47,6 @@ pub(crate) fn type_override_enum( ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, }) } diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs index 740e5fbc..b307d65d 100644 --- a/macros/src/types/unit.rs +++ b/macros/src/types/unit.rs @@ -21,6 +21,7 @@ pub(crate) fn empty_object(attr: &StructAttr, ts_name: Expr) -> DerivedTS { ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, } } @@ -38,6 +39,7 @@ pub(crate) fn empty_array(attr: &StructAttr, ts_name: Expr) -> DerivedTS { ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, } } @@ -55,5 +57,6 @@ pub(crate) fn null(attr: &StructAttr, ts_name: Expr) -> DerivedTS { ts_name, concrete: attr.concrete.clone(), bound: attr.bound.clone(), + is_ts_enum: false, } } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 459645c2..06d4042b 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -64,7 +64,7 @@ //! //! ## Features //! - generate type declarations from rust structs -//! - generate union declarations from rust enums +//! - generate union declarations (and native typescript enums) from rust enums //! - inline types //! - flatten structs/types //! - generate necessary imports when exporting to multiple files @@ -364,6 +364,11 @@ mod tokio; /// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE" ///

/// +/// - **`#[ts(use_ts_enum)]`** +/// Exports a typescript enum with string values instead of a union type. +/// Typescript enums have simple names and values; and therefore cannot be used with `tag`, `type_override`, `type_as`, or kebab-case renaming on the same enum. +///

+/// /// ### enum variant attributes /// /// - **`#[ts(rename = "..")]`** diff --git a/ts-rs/tests/integration/enum_typescript.rs b/ts-rs/tests/integration/enum_typescript.rs new file mode 100644 index 00000000..7dace59a --- /dev/null +++ b/ts-rs/tests/integration/enum_typescript.rs @@ -0,0 +1,134 @@ +#![allow(dead_code)] + +#[cfg(feature = "serde-compat")] +use serde::Serialize; +use ts_rs::TS; + +#[derive(TS)] +#[ts(export, export_to = "enum_typescript/", use_ts_enum)] +#[cfg_attr(feature = "serde-compat", derive(Serialize))] +#[cfg_attr(feature = "serde-compat", serde(rename_all = "camelCase"))] +#[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "camelCase"))] +enum A { + MessageOne, + MessageTwo, +} + +#[test] +fn test_use_typescript_enum() { + // Let inline funcs still return the normal unions, + // so as to not break other types that might inline an enum in their definitions + assert_eq!( + A::inline_flattened(), + r#"("messageOne" | "messageTwo")"#, + ); + assert_eq!( + A::inline(), + r#"("messageOne" | "messageTwo")"#, + ); + // The decl function return the proper typescript enum + assert_eq!( + A::decl(), + r#"enum A { messageOne = "messageOne", messageTwo = "messageTwo" }"#, + ); + assert_eq!( + A::decl_concrete(), + r#"enum A { messageOne = "messageOne", messageTwo = "messageTwo" }"#, + ); +} + +#[derive(TS)] +#[ts(export, export_to = "enum_typescript/", use_ts_enum)] +#[cfg_attr(feature = "serde-compat", derive(Serialize))] +#[cfg_attr(feature = "serde-compat", serde(rename_all = "snake_case"))] +#[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "snake_case"))] +enum B { + MessageOne, + MessageTwo, +} + +#[test] +fn test_use_typescript_enum_snake_case() { + assert_eq!( + B::inline_flattened(), + r#"("message_one" | "message_two")"#, + ); + assert_eq!( + B::inline(), + r#"("message_one" | "message_two")"#, + ); + assert_eq!( + B::decl(), + r#"enum B { message_one = "message_one", message_two = "message_two" }"#, + ); + assert_eq!( + B::decl_concrete(), + r#"enum B { message_one = "message_one", message_two = "message_two" }"#, + ); +} + +#[derive(TS)] +#[ts(export, export_to = "enum_typescript/")] +#[cfg_attr(feature = "serde-compat", derive(Serialize))] +#[cfg_attr(feature = "serde-compat", serde(rename_all = "camelCase"))] +#[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "camelCase"))] +struct Hello { + hello_there: A, + #[ts(inline)] + good_night: B, +} + +#[test] +fn test_use_typescript_enum_within_struct() { + assert_eq!( + Hello::inline_flattened(), + r#"{ helloThere: A, goodNight: ("message_one" | "message_two"), }"#, + ); + assert_eq!( + Hello::inline(), + r#"{ helloThere: A, goodNight: ("message_one" | "message_two"), }"#, + ); + assert_eq!( + Hello::decl(), + r#"type Hello = { helloThere: A, goodNight: ("message_one" | "message_two"), };"#, + ); + assert_eq!( + Hello::decl_concrete(), + r#"type Hello = { helloThere: A, goodNight: ("message_one" | "message_two"), };"#, + ); +} + +#[derive(TS)] +#[ts(export, export_to = "enum_typescript/", use_ts_enum)] +#[cfg_attr(feature = "serde-compat", derive(Serialize))] +#[cfg_attr(feature = "serde-compat", serde(rename_all = "PascalCase"))] +#[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "PascalCase"))] +enum C { + MessageOne, + #[cfg_attr(feature = "serde-compat", serde(rename = "boo_yah"))] + #[cfg_attr(not(feature = "serde-compat"), ts(rename = "boo_yah"))] + MessageTwo, +} + +#[test] +fn test_use_typescript_enum_with_rename_variant() { + // Let inline funcs still return the normal unions, + // so as to not break other types that might inline an enum in their definitions + assert_eq!( + C::inline_flattened(), + r#"("MessageOne" | "boo_yah")"#, + ); + assert_eq!( + C::inline(), + r#"("MessageOne" | "boo_yah")"#, + ); + // The decl function return the proper typescript enum + assert_eq!( + C::decl(), + r#"enum C { MessageOne = "MessageOne", boo_yah = "boo_yah" }"#, + ); + assert_eq!( + C::decl_concrete(), + r#"enum C { MessageOne = "MessageOne", boo_yah = "boo_yah" }"#, + ); +} diff --git a/ts-rs/tests/integration/main.rs b/ts-rs/tests/integration/main.rs index f5d2e921..fc7f40d4 100644 --- a/ts-rs/tests/integration/main.rs +++ b/ts-rs/tests/integration/main.rs @@ -72,3 +72,4 @@ mod union_with_data; mod union_with_internal_tag; mod unit; mod r#unsized; +mod enum_typescript;