diff --git a/core/README.md b/core/README.md index 4eb9fba7..2356595d 100644 --- a/core/README.md +++ b/core/README.md @@ -34,7 +34,7 @@ The test suite can of course be run normally without updating any expectations: cargo test -p typeshare-core ``` -If you find yourself needing to update expectations for a specific test only, run the following (subsituting the name of your test in for the last arg): +If you find yourself needing to update expectations for a specific test only, run the following (substituting the name of your test in for the last arg): ``` env UPDATE_EXPECT=1 cargo test -p typeshare-core --test snapshot_tests -- can_handle_serde_rename_all::swift diff --git a/core/data/tests/can_generate_unit_ts_union_enum/input.rs b/core/data/tests/can_generate_unit_ts_union_enum/input.rs new file mode 100644 index 00000000..46daeded --- /dev/null +++ b/core/data/tests/can_generate_unit_ts_union_enum/input.rs @@ -0,0 +1,18 @@ +#[typeshare(ts_union)] +pub enum UnitEnumMultiple { + VariantA, + VariantB, + VariantC, +} + +#[typeshare(ts_union)] +pub enum UnitEnumOne { + VariantA, +} + +#[typeshare(ts_union)] +pub enum UnitEnumSkip { + #[typeshare(skip)] + VariantA, + VariantB, +} diff --git a/core/data/tests/can_generate_unit_ts_union_enum/output.ts b/core/data/tests/can_generate_unit_ts_union_enum/output.ts new file mode 100644 index 00000000..2e7c9025 --- /dev/null +++ b/core/data/tests/can_generate_unit_ts_union_enum/output.ts @@ -0,0 +1,6 @@ +export type UnitEnumMultiple = "VariantA" | "VariantB" | "VariantC"; + +export type UnitEnumOne = "VariantA"; + +export type UnitEnumSkip = "VariantB"; + diff --git a/core/src/language/go.rs b/core/src/language/go.rs index 8c43d940..c6f34141 100644 --- a/core/src/language/go.rs +++ b/core/src/language/go.rs @@ -183,7 +183,7 @@ impl Go { write_comments(w, 0, &e.shared().comments)?; match e { - RustEnum::Unit(shared) => { + RustEnum::Unit { shared, .. } => { writeln!( w, "type {} string", diff --git a/core/src/language/kotlin.rs b/core/src/language/kotlin.rs index cd0e22b2..e58a6a82 100644 --- a/core/src/language/kotlin.rs +++ b/core/src/language/kotlin.rs @@ -165,7 +165,7 @@ impl Language for Kotlin { .unwrap_or_default(); match e { - RustEnum::Unit(shared) => { + RustEnum::Unit { shared, .. } => { write!( w, "enum class {}{}(val string: String) ", @@ -192,7 +192,7 @@ impl Language for Kotlin { impl Kotlin { fn write_enum_variants(&mut self, w: &mut dyn Write, e: &RustEnum) -> std::io::Result<()> { match e { - RustEnum::Unit(shared) => { + RustEnum::Unit { shared, .. } => { for v in &shared.variants { self.write_comments(w, 1, &v.shared().comments)?; writeln!(w, "\t@SerialName({:?})", &v.shared().id.renamed)?; diff --git a/core/src/language/scala.rs b/core/src/language/scala.rs index c086bd4b..2f9274a2 100644 --- a/core/src/language/scala.rs +++ b/core/src/language/scala.rs @@ -193,7 +193,7 @@ impl Language for Scala { .unwrap_or_default(); match e { - RustEnum::Unit(shared) => { + RustEnum::Unit { shared, .. } => { writeln!( w, "sealed trait {}{} {{", @@ -220,7 +220,7 @@ impl Language for Scala { impl Scala { fn write_enum_variants(&mut self, w: &mut dyn Write, e: &RustEnum) -> std::io::Result<()> { match e { - RustEnum::Unit(shared) => { + RustEnum::Unit { shared, .. } => { for v in shared.variants.iter() { self.write_comments(w, 1, &v.shared().comments)?; writeln!( diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 06c23c89..f0d83adb 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -436,7 +436,7 @@ impl Language for Swift { let enum_name = swift_keyword_aware_rename(&format!("{}{}", self.prefix, shared.id.renamed)); let always_present = match e { - RustEnum::Unit(_) => { + RustEnum::Unit { .. } => { let mut always_present = vec!["String".into()]; always_present.append(&mut self.get_default_decorators()); always_present @@ -547,7 +547,7 @@ impl Swift { let mut coding_keys = Vec::new(); match e { - RustEnum::Unit(shared) => { + RustEnum::Unit { shared, .. } => { for v in &shared.variants { let variant_name = v.shared().id.original.to_camel_case(); diff --git a/core/src/language/typescript.rs b/core/src/language/typescript.rs index a5560ef1..0f8e3a55 100644 --- a/core/src/language/typescript.rs +++ b/core/src/language/typescript.rs @@ -139,7 +139,14 @@ impl Language for TypeScript { .unwrap_or_default(); match e { - RustEnum::Unit(shared) => { + RustEnum::Unit { shared, ts_union } if *ts_union => { + write!(w, "export type {} = ", shared.id.renamed)?; + + self.write_enum_variants(w, e)?; + + writeln!(w, ";\n") + } + RustEnum::Unit { shared, .. } => { write!( w, "export enum {}{} {{", @@ -170,9 +177,25 @@ impl Language for TypeScript { impl TypeScript { fn write_enum_variants(&mut self, w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { match e { + // Write all the unit variants out (there can only be unit variants in + // this case). Format it as ts union. + RustEnum::Unit { shared, ts_union } if *ts_union => { + let variants = shared + .variants + .iter() + .filter_map(|v| match v { + RustEnumVariant::Unit(shared) => Some(format!("\"{}\"", shared.id.renamed)), + _ => None, + }) + .collect::>() + .join(" | "); + + write!(w, "{}", variants) + } + // Write all the unit variants out (there can only be unit variants in // this case) - RustEnum::Unit(shared) => shared.variants.iter().try_for_each(|v| match v { + RustEnum::Unit { shared, .. } => shared.variants.iter().try_for_each(|v| match v { RustEnumVariant::Unit(shared) => { writeln!(w)?; self.write_comments(w, 1, &shared.comments)?; diff --git a/core/src/parser.rs b/core/src/parser.rs index 54b21d37..af0c961e 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -263,6 +263,8 @@ fn parse_enum(e: &ItemEnum) -> Result { let maybe_tag_key = get_tag_key(&e.attrs); let maybe_content_key = get_content_key(&e.attrs); + let ts_union = get_ts_union(&e.attrs); + // Parse all of the enum's variants let variants = e .variants @@ -309,7 +311,7 @@ fn parse_enum(e: &ItemEnum) -> Result { }); } - Ok(RustItem::Enum(RustEnum::Unit(shared))) + Ok(RustItem::Enum(RustEnum::Unit { shared, ts_union })) } else { // At least one enum variant is either a tuple or an anonymous struct @@ -643,6 +645,25 @@ fn get_content_key(attrs: &[syn::Attribute]) -> Option { .and_then(literal_as_string) } +fn get_ts_union(attrs: &[syn::Attribute]) -> bool { + let ts_union = Ident::new("ts_union", Span::call_site()); + + attrs.iter().any(|attr| { + get_typeshare_meta_items(attr) + .into_iter() + .any(|arg| match arg { + NestedMeta::Meta(Meta::Path(path)) => { + if let Some(ident) = path.get_ident() { + *ident == ts_union + } else { + false + } + } + _ => false, + }) + }) +} + fn serde_rename(attrs: &[syn::Attribute]) -> Option { get_serde_name_value_meta_items(attrs, "rename") .next() diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index cf8aa2e1..36150572 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -482,7 +482,11 @@ pub enum RustEnum { /// Yay, /// } /// ``` - Unit(RustEnumShared), + Unit { + /// Represents this enum in typescript as union instead of ts enum + ts_union: bool, + shared: RustEnumShared, + }, /// An algebraic enum /// /// An example of such an enum: @@ -513,7 +517,7 @@ impl RustEnum { /// Get a reference to the inner shared content pub fn shared(&self) -> &RustEnumShared { match self { - Self::Unit(shared) | Self::Algebraic { shared, .. } => shared, + Self::Unit { shared, .. } | Self::Algebraic { shared, .. } => shared, } } } diff --git a/core/src/topsort.rs b/core/src/topsort.rs index 42381541..7da60c6d 100644 --- a/core/src/topsort.rs +++ b/core/src/topsort.rs @@ -63,7 +63,7 @@ fn get_enum_dependencies( seen: &mut HashSet, ) { match enm { - RustEnum::Unit(_) => {} + RustEnum::Unit { .. } => {} RustEnum::Algebraic { tag_key: _, content_key: _, @@ -185,12 +185,8 @@ pub(crate) fn topsort(things: Vec<&RustItem>) -> Vec<&RustItem> { let types = HashMap::from_iter(things.iter().map(|&thing| { let id = match thing { RustItem::Enum(e) => match e { - RustEnum::Algebraic { - tag_key: _, - content_key: _, - shared, - } => shared.id.original.clone(), - RustEnum::Unit(shared) => shared.id.original.clone(), + RustEnum::Algebraic { shared, .. } => shared.id.original.clone(), + RustEnum::Unit { shared, .. } => shared.id.original.clone(), }, RustItem::Struct(strct) => strct.id.original.clone(), RustItem::Alias(ta) => ta.id.original.clone(), diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 8220fde5..d43b45e1 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -374,6 +374,7 @@ tests! { scala, typescript ]; + can_generate_unit_ts_union_enum: [typescript]; can_generate_generic_struct: [ swift { prefix: "Core".into(), diff --git a/docs/src/usage/annotations.md b/docs/src/usage/annotations.md index 7227619f..f27eaba9 100644 --- a/docs/src/usage/annotations.md +++ b/docs/src/usage/annotations.md @@ -103,7 +103,24 @@ This would generate the following Kotlin code: typealias Options = String ``` +### TypeScript enum representation +For unit enums, you can choose to generate a TypeScript union type instead +of a TypeScript enum. + +```rust +#[typeshare(ts_union)] +pub enum UnitEnum { + VariantA, + VariantB, + VariantC, +} +``` + +This would generate the following TypeScript code: +```ts +export type UnitEnum = "VariantA" | "VariantB" | "VariantC"; +``` ## The `#[serde]` Attribute @@ -155,4 +172,4 @@ export interface MyStruct { a: number; c: number; } -``` +``` \ No newline at end of file